November 3rd, 2023

What’s new with identity in .NET 8

Jeremy Likness
Principal Program Manager - .NET Web Frameworks

In April 2023, I wrote about the commitment by the ASP.NET Core team to improve authentication, authorization, and identity management in .NET 8. The plan we presented included three key deliverables:

  • New APIs to simplify login and identity management for client apps like Single Page Apps (SPA) and Blazor WebAssembly.
  • Enablement of token-based authentication and authorization in ASP.NET Core Identity for clients that can’t use cookies.
  • Improvements to documentation.

All three deliverables will ship with .NET 8. In addition, we were able to add a new identity UI for Blazor web apps that works with both of the new rendering modes, server and WebAssembly.

Let’s look at a few scenarios that are enabled by the new changes in .NET 8. In this blog post we’ll cover:

Let’s look at the simplest scenario for using the new identity features.

Basic Web API backend

An easy way to use the new authorization is to enable it in a basic Web API app. The same app may also be used as the backend for Blazor WebAssembly, Angular, React, and other Single Page Web apps (SPA). Assuming you’re starting with an ASP.NET Core Web API project in .NET 8 that includes OpenAPI, you can add authentication with a few steps.

Identity is “opt-in,” so there are a few packages to add:

  • Microsoft.AspNetCore.Identity.EntityFrameworkCore – the package that enables EF Core integration
  • A package for the database you wish to use, such as Microsoft.EntityFrameworkCore.SqlServer (we’ll use the in-memory database for this example)

You can add these packages using the NuGet package manager or the command line. For example, to add the packages using the command line, navigate to the project folder and run the following dotnet commands:

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory

Identity allows you to customize both the user information and the user database in case you have requirements beyond what is provided in the .NET Core framework. For our basic example, we’ll just use the default user information and database. To do that, we’ll add a new class to the project called MyUser that inherits from IdentityUser:

class MyUser : IdentityUser {}

Add a new class called AppDbContext that inherits from IdentityDbContext<MyUser>:

class AppDbContext(DbContextOptions<AppDbContext> options) : 
        IdentityDbContext<MyUser>(options)
{
}

Providing the special constructor makes it possible to configure the database for different environments.

To set up identity for an app, open the Program.cs file. Configure identity to use cookie-based authentication and to enable authorization checks by adding the following code after the call to WebApplication.CreateBuilder(args):

builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddIdentityCookies();
builder.Services.AddAuthorizationBuilder();

Configure the EF Core database. Here we’ll use the in-memory database and name it “AppDb.” It’s usedhere for the demo so it is easy to restart the application and test the flow to register and login (each run will start with a fresh database). Changing to SQLite will save users between sessions but requires the database to be properly created through migrations as shown in this EF Core getting started tutorial. You can use other relational providers such as SQL Server for your production code.

builder.Services.AddDbContext<AppDbContext>(
    options => options.UseInMemoryDatabase("AppDb"));

Configure identity to use the EF Core database and expose the identity endpoints:

builder.Services.AddIdentityCore<MyUser>()
    .AddEntityFrameworkStores<AppDbContext>()
    .AddApiEndpoints();

Map the routes for the identity endpoints. This code should be placed after the call to builder.Build():

app.MapIdentityApi<MyUser>();

The app is now ready for authentication and authorization! To secure an endpoint, use the .RequireAuthorization() extension method where you define the auth route. If you are using a controller-based solution, you can add the [Authorize] attribute to the controller or action.

To test the app, run it and navigate to the Swagger UI. Expand the secured endpoint, select try it out, and select execute. The endpoint is reported as 404 - not found, which is arguably more secure than reporting a 401 - not authorized because it doesn’t reveal that the endpoint exists.

Swagger UI with 404

Now expand /register and fill in your credentials. If you enter an invalid email address or a bad password, the result includes the validation errors.

Swagger UI with validation errors

The errors in this example are returned in the ProblemDetails format so your client can easily parse them and display validation errors as needed. I’ll show an example of that in the standalone Blazor WebAssembly app.

A successful registration results in a 200 - OK response. You can now expand /login and enter the same credentials. Note, there are additional parameters that aren’t needed for this example and can be deleted. Be sure to set useCookies to true. A successful login results in a 200 - OK response with a cookie in the response header.

Now you can rerun the secured endpoint and it should return a valid result. This is because cookie-based authentication is securely built-in to your browser and “just works.” You’ve just secured your first endpoint with identity!

Some web clients may not include cookies in the header by default. If you are using a tool for testing APIs, you may need to enable cookies in the settings. The JavaScript fetch API does not include cookies by default. You can enable them by setting credentials to the value include in the options. Similarly, an HttpClient running in a Blazor WebAssembly app needs the HttpRequestMessage to include credentials, like the following:

request.SetBrowserRequestCredential(BrowserRequestCredentials.Include);

Next, let’s jump into a Blazor web app.

The Blazor identity UI

A stretch goal of our team that we were able to achieve was to implement the identity UI, which includes options to register, log in, and configure multi-factor authentication, in Blazor. The UI is built into the template when you select the “Individual accounts” option for authentication. Unlike the previous version of the identity UI, which was hidden unless you wanted to customize it, the template generates all of the source code so you can modify it as needed. The new version is built with Razor components and works with both server-side and WebAssembly Blazor apps.

Blazor login page

The new Blazor web model allows you to configure whether the UI is rendered server-side or from a client running in WebAssembly. When you choose the WebAssembly mode, the server will still handle all authentication and authorization requests. It will also generate the code for a custom implementation of AuthenticationStateProvider that tracks the authentication state. The provider uses the PersistentComponentState class to pre-render the authentication state and persist it to the page. The PersistentAuthenticationStateProvider in the client WebAssembly app uses the component to synchronize the authentication state between the server and browser. The state provider might also be named PersistingRevalidatingAuthenticationStateProvider when running with auto interactivity or IdentityRevalidatingAuthenticationStateProvider for server interactivity.

Although the examples in this blog post are focused on a simple username and password login scenario, ASP.NET Identity has support for email-based interactions like account confirmation and password recovery. It is also possible to configure multifactor authentication. The components for all of these features are included in the UI.

Add an external login

A common question we are asked is how to integrate external logins through social websites with ASP.NET Core Identity. Starting from the Blazor web app default project, you can add an external login with a few steps.

First, you’ll need to register your app with the social website. For example, to add a Twitter login, go to the Twitter developer portal and create a new app. You’ll need to provide some basic information to obtain your client credentials. After creating your app, navigate to the app settings and click “edit” on authentication. Specify “native app” for the application type for the flow to work correctly and turn on “request email from users.” You’ll need to provide a callback URL. For this example, we’ll use https://localhost:5001/signin-twitter which is the default callback URL for the Blazor web app template. You can change this to match your app’s URL (i.e. replace 5001 with your own port). Also note the API key and secret.

Next, add the appropriate authentication package to your app. There is a community-maintained list of OAuth 2.0 social authentication providers for ASP.NET Core with many options to choose from. You can mix multiple external logins as needed. For Twitter, I’ll add the AspNet.Security.OAuth.Twitter package.

From a command prompt in the root directory of the server project, run this command to store your API Key (client ID) and secret.

dotnet user-secrets set "Twitter:ApiKey" "<your-api-key>"
dotnet user-secrets set "TWitter:ApiSecret" "<your-api-secret>"

Finally, configure the login in Program.cs by replacing this code:

builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddIdentityCookies();

with this code:

builder.Services.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddTwitter(opt =>
        {
            opt.ClientId = builder.Configuration["Twitter:ApiKey"]!;
            opt.ClientSecret = builder.Configuration["Twitter:ApiSecret"]!;
        })
    .AddIdentityCookies();

Cookies are the preferred and most secure approach for implementing ASP.NET Core Identity. Tokens are supported if needed and require the IdentityConstants.BearerScheme to be configured. The tokens are proprietary and the token-based flow is intended for simple scenarios so it does not implement the OAuth 2.0 or OIDC standards.

What’s next? Believe it or not, you’re done. This time when you run the app, the login page will automatically detect the external login and provide a button to use it.

Blazor login page with Twitter

When you log in and authorize the app, you will be redirected back and authenticated.

Securing Blazor WebAssembly apps

A major motivation for adding the new identity APIs was to make it easier for developers to secure their browser-based apps including Single Page Apps (SPA) and Blazor WebAssembly. It doesn’t matter if you use the built-in identity provider, a custom login or a cloud-based service like Microsoft Entra, the end result is an identity that is either authenticated with claims and roles, or not authenticated. In Blazor, you can secure a razor component by adding the [Authorize] attribute to the component or to the page that hosts the component. You can also secure a route by adding the .RequireAuthorization() extension method to the route definition.

The full source code for this example is available in the Blazor samples repo.

The AuthorizeView tag provides a simple way to handle content the user has access to. The authentication state can be accessed via the context property. Consider the following:

<p>Welcome to my page!</p>
<AuthorizeView>
    <Authorizing>
        <div class="alert alert-info">We're checking your credentials...</div>
    </Authorizing>
    <Authorized>
        <div class="alert alert-success">You are authenticated @context.User.Identity?.Name</div>
    </Authorized>
    <NotAuthorized>
        <div class="alert alert-warning">You are not authenticated!</div>
    </NotAuthorized>
</AuthorizeView>

The greeting will be shown to everyone. In the case of Blazor WebAssembly, when the client might need to authenticate asynchronously over API calls, the Authorizing content will be shown while the authentication state is queried and resolved. Then, based on whether or not you’ve authenticated, you’ll either see your name or a message that you’re not authenticated. How exactly does the client know if you’re authenticated? That’s where the AuthenticationStateProvider comes in.

The App.razor page is wrapped in a CascadingAuthenticationState provider. This provider is responsible for tracking the authentication state and making it available to the rest of the app. The AuthenticationStateProvider is injected into the provider and used to track the state. The AuthenticationStateProvider is also injected into the AuthorizeView component. When the authentication state changes, the provider notifies the AuthorizeView component and the content is updated accordingly.

First, we want to make sure that API calls are persisting credentials accordingly. To do that, I created a handler named CookieHandler.

public class CookieHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
        return base.SendAsync(request, cancellationToken);
    }
}

In Program.cs I added the handler to the HttpClient and used the client factory to configure a special client for authentication purposes.

builder.Services.AddTransient<CookieHandler>();
builder.Services.AddHttpClient(
    "Auth",
    opt => opt.BaseAddress = new Uri(builder.Configuration["AuthUrl"]!))
    .AddHttpMessageHandler<CookieHandler>();

Note: the authentication components are opt-in and available via the Microsoft.AspNetCore.Components.WebAssembly.Authentication package. The client factory and extension methods come from Microsoft.Extensions.Http.

The AuthUrl is the URL of the ASP.NET Core server that exposes the identity APIs. Next, I created a CookieAuthenticationStateProvider that inherits from AuthenticationStateProvider and overrides the GetAuthenticationStateAsync method. The main logic looks like this:

var unauthenticated = new ClaimsPrincipal(new ClaimsIdentity());
var userResponse = await _httpClient.GetAsync("manage/info");
if (userResponse.IsSuccessStatusCode) 
{
    var userJson = await userResponse.Content.ReadAsStringAsync();
    var userInfo = JsonSerializer.Deserialize<UserInfo>(userJson, jsonSerializerOptions);
    if (userInfo != null)
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, userInfo.Email),
            new(ClaimTypes.Email, userInfo.Email)
        };
        var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
        user = new ClaimsPrincipal(id);
    }
}

return new AuthenticationState(user);

The user info endpoint is secure, so if the user is not authenticated the request will fail and the method will return an unauthenticated state. Otherwise, it builds the appropriate identity and claims and returns the authenticated state.

How does the app know when the state has changed? Here is what a login looks like from Blazor WebAssembly using the identity API:

async Task<AuthenticationState> LoginAndGetAuthenticationState()
{
    var result = await _httpClient.PostAsJsonAsync(
        "login?useCookies=true", new
        {
            email,
            password
        });

        return await GetAuthenticationStateAsync();
}

NotifyAuthenticationStateChanged(LoginAndGetAuthenticationState());

When the login is successful, the NotifyAuthenticationStateChanged method on the base AuthenticationStateProvider class is called to notify the provider that the state has changed. It is passed the result of the request for a new authentication state so that it can verify the cookie is present. The provider will then update the AuthorizeView component and the user will see the authenticated content.

Tokens

In the rare event your client doesn’t support cookies, the login API provides a parameter to request tokens. An custom token (one that is proprietary to the ASP.NET Core identity platform) is issued that can be used to authenticate subsequent requests. The token is passed in the Authorization header as a bearer token. A refresh token is also provided. This allows your application to request a new token when the old one expires without forcing the user to log in again. The tokens are not standard JSON Web Tokens (JWT). The decision behind this was intentional, as the built-in identity is meant primarily for simple scenarios. The token option is not intended to be a fully-featured identity service provider or token server, but instead an alternative to the cookie option for clients that can’t use cookies.

Not sure whether you need a token server or not? Read a document to help you choose the right ASP.NET Core identity solution. Looking for a more advanced identity solution? Read our list of identity management solutions for ASP.NET Core.

Docs and samples

The third deliverable is documentation and samples. We have already introduced new documentation and will be adding new articles and samples as we approach the release of .NET 8. Follow Issue #29452 – documentation and samples for identity in .NET 8 to track the progress. Please use the issue to communicate additional documentation or samples you are looking for. You can also link to the specific issues for various documents and provide your feedback there.

Conclusion

The new identity features in .NET 8 make it easier than ever to secure your applications. If your requirements are simple, you can now add authentication and authorization to your app with a few lines of code. The new APIs make it possible to secure your Web API endpoints with cookie-based authentication and authorization. There is also a token-based option for clients that can’t use cookies.

Learn more about the new identity features in the ASP.NET Core documentation.

Author

Jeremy Likness
Principal Program Manager - .NET Web Frameworks

Jeremy is a Principal Program Manager for .NET Web Frameworks at Microsoft. Jeremy wrote his first program in 1982, was recognized in the "who's who in Quake" list for programming the first implementation of "Midnight Capture the Flag" in Quake C and has been developing enterprise applications for 25 years with a primary focus on web-based delivery of line of business applications. Jeremy is the author of four technology books, a former 8-year Microsoft MVP for Developer Tools and Technologies, ...

More about author

42 comments

Discussion is closed. Login to edit/delete existing comments.

  • Pranto Das · Edited

    <code>

    When the login button is clicked on the Blazor Identity UI Login page, it produces errors if StreamRendering is enabled. I have searched for the cause of the issue, but I couldn't find any.

    Note: We have used Blazor Server

    <code>

    Read more
  • Pramod Lawate

    It’s a pleasure to come across this article. However, considering that we are in 2023, it would be greatly appreciated if a comprehensive YouTube video could be created for the same content. This would significantly enhance the user experience and accessibility of the information. Thank you.

  • Jim Braun

    Hi Jeremy,
    For entrepreneurs that rely on open source, free, community options to build something, It's been a tough few years with Dotnet and authentication since Duende decided to pull the plug on Open Source Identity Server. It seems that the Microsoft strategy has been to get everyone onboard with paying for Entra instead of reaching an agreement with Duende or committing to support the Identity Server 4 project. For the masses who are...

    Read more
  • Liu, Nianwei

    I noticed the “Microsoft Identity Platform” option is removed from the Visual Studio Blazor template, now you can only do “none” or “individual accounts”. In NET6 you can generate Blazor code based on Microsoft Identity Platform from project creation. What’s the rationale behind that move? Is that something that maybe adding back?

    • Jeremy LiknessMicrosoft employee Author

      The new Blazor Web model is completely different and there was no way to just migrate the code. We had to remove it until we could recreate it for the new app model. We are working on samples now that will eventually be worked back into templates.

      • Liu, Nianwei

        Thanks, looking forward to the updates

  • Preet Ranjan · Edited

    faced a very new issue while trying out new api end points with SQLite provider. This is while calling the register endpoint

    <code>

    Is there a way to add scaffolded item just like identity scaffold in MVC.

    Read more
    • Jeremy LiknessMicrosoft employee Author

      Can you please file an issue for the bug you encountered so the engineering team can take a look? We are working on providing scaffolding in future release of Visual Studio.

      Jeremy

  • Paul Astramowicz

    I’m developing a new .net 8 MAUI app which uses the new .NET SPA Identity Bearer token API ends. Is it possible to maybe add a RATE LIMITER on top of the /REGISTER endpoint somehow? I would like to prevent DDOS attacks on the /REGISTER endpoint API.

    This:
    https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/

    thanks,

    Paul Astramaowicz

  • Steven Archibald

    IIRC, some recent versions of Identity relied on Duende Identity Server. If you wanted to use Identity in your business, you had to purchase a license from Duende to use their Identity server that is packaged up as part of Identity.

    Is this still the case?

    • Ahmed Mohamed Abdel-Razek

      no, all of this updates to remove Duende dependencies
      it’s all microsoft libraries by default now

  • Ahmed Mohamed Abdel-Razek · Edited

    i straggle with decoding the new access token in blazor with c# or javascript even jwt dot io gives me signature error
    my setup

    also i don't know how to add my signing secret key

    <code>

    <code>

    <code>

    <code>

    <code>

    also is there a way to make the refresh token shorter like way shorter to fit in url?

    Read more
    • Jeremy LiknessMicrosoft employee Author

      The new tokens are not JWT tokens. They are custom and are intended to be securely passed in API requests and not as part of the URL.

      • Ahmed Mohamed Abdel-Razek

        1- i want to be able to decode and read my 'Access Token' in the front end like 'JWTs'
        i know it's "protected" but is there a way to "unprotect" it and make it readable from the front like 'JWTs'
        if not is there any plans to support that in the future?

        2- i don't want to use the refresh token as part of url to pass it around webpages i want to be able to...

        Read more
  • Niclas Rothman

    HI Jeremey,
    Im kind of struggling to find information or event better an example how to use Azure B2C with Blazor .NET 8.
    We are today on .NET 7 with Blazor Server but would like to benefit of the autorendering feature of .NET 8.
    I dont have the full picture how to handle authentication / authorization when using autorendering. I only see examples that are using local user accounts, no examples of external IDPs.
    Could...

    Read more
    • Jeremy LiknessMicrosoft employee Author

      Hi, Niclas.

      This is an important scenario and we are working on guidance via samples until we can get it back into the templates.

      You can see the work in progress in this pull request.

  • Richard Barraclough

    I don’t get it. What has changed? Hasn’t all this stuff been there for years?

    • Jeremy LiknessMicrosoft employee Author

      No, the identity APIs and ability to issue tokens for ASP. NET Core Identity are new to .NET 8.

      • Pony Speed

        Excellent work from your team again. But I do have the same impression of "I don’t get it" as Richard Barraclough . To be more precise, for the followings :

        Securing a simple web API backend
        Adding an external login like Google or Facebook

        They looks the same as .net 7 to me. Could you point to some documeation or blog that explain the difference in these two areas ?

        Thanks.

        Read more
      • Jeremy LiknessMicrosoft employee Author

        We didn’t change the way external login works.

        • The Blazor template with auth automatically implements external logins
        • The samples are to either show how where we didn’t have samples before, or to illustrate how it works with the new APIs

        Jeremy