Telerik blogs

Learn the basics of Blazor WebAssembly authentication, including the Identity authentication system.

Authentication in Blazor WebAssembly can be challenging if it’s your first time implementing it, especially due to the number of classes and components that the framework adds if you choose the Individual Accounts option while configuring the project. For this reason, throughout this post, I will explain the key concepts you need to understand to start your journey. Let’s begin!

Configuring a Blazor WebAssembly Project with Individual Accounts

An excellent way to learn about the authentication process in Blazor WebAssembly is by using the Blazor Web App template, selecting Authentication Type as Individual Accounts, Interactive Render Mode as WebAssembly, and Interactivity location as Global:

Creating a new Blazor WebAssembly project with Individual Accounts configuration

The above configuration indicates that the application should apply WebAssembly rendering to all components (in theory—you’ll see later that this doesn’t happen exactly like that). This template will help us understand how server-side authentication can be achieved while somehow persisting information on the client side.

After creating the project, I recommend running the application to see the initial project structure. You can see that, apart from the traditional pages in a Blazor project, the pages Auth Required, Register and Login have been created.

Creating a new user in the application

Let’s start by trying to create an account on the Register page. When entering the data and clicking the button, you’ll see an error occurs. This error happens because the first migration hasn’t been applied for the application to work. We can execute the recommended command in the Package Manager Console, or the simpler solution is to click the Blue button that says Apply Migrations:

Failed to create user because the initial migration has not been applied

After applying the migration, we just need to refresh the page to see that we can now register without this error appearing:

Message indicating that the user has registered successfully

Once we have the example project ready, let’s examine its operation more closely.

Examining the AddCascadingAuthenticationState Method

Let’s examine the Program.cs file in the server project, which will allow us to discover different important concepts in the world of authentication with ASP.NET Core Identity. The first line we should look at is the one that invokes the AddCascadingAuthenticationState method:

builder.Services.AddCascadingAuthenticationState();

The execution of this method is of utmost importance because internally, it will register the necessary services so that the user’s authentication state is available in the component hierarchy when the application is running. Some components like AuthorizeView couldn’t function if this method wasn’t executed.

Learning About UserManager and ApplicationUser

Continuing with our tour in Program.cs in the server project, the next line you can see is the registration in the DI of the IdentityUserAccessor class:

builder.Services.AddScoped<IdentityUserAccessor>();

Examining this class that is part of our project, you can see that it has the following structure:

internal sealed class IdentityUserAccessor(UserManager<ApplicationUser> userManager, IdentityRedirectManager redirectManager)
{
    public async Task<ApplicationUser> GetRequiredUserAsync(HttpContext context)
    {
        var user = await userManager.GetUserAsync(context.User);

        if (user is null)
        {
            redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context);
        }

        return user;
    }
}

At this point, the code might start to seem a bit confusing due to the classes being used. To clear up any doubts, I’ll explain some basic concepts.

First, you should know that the ASP.NET Core Identity architecture is made up of manager classes and stores. The official documentation indicates that managers are high-level classes that a developer can use to perform operations, such as creating an Identity User.

Going back to the source code, you can see that as the first parameter, it receives a parameter of type UserManager, which is a manager class that we described earlier. If we examine this class closely, we can find methods for creating users, updating them, deleting them, handling email confirmations and a huge list of methods. Here’s a portion of the available methods as part of this class:

A portion of the methods available in UserManager

Another important point is that the definition of the UserManager class receives a generic of type TUser:

public class UserManager<TUser> : IDisposable where TUser : class

In the template, you can see that the generic used is the ApplicationUser class. To better understand what an ApplicationUser refers to, it’s time to talk about stores. Again, according to the official documentation, stores are low-level classes that specify how users and roles are persisted. These stores follow the repository pattern, are strongly linked to persistence mechanisms and use an IdentityUser to work with the defined model.

A very important piece of information that will help you if you want to replace the persistence mechanism is that managers are decoupled from stores, which means that whenever you want, you can replace the persistence mechanism without affecting your application code.

Once we know this, if we examine the ApplicationUser class, we see that it’s an empty class that inherits from IdentityUser:

public class ApplicationUser : IdentityUser
{
}

IdentityUser is a base class that represents a user in ASP.NET Core Identity and provides basic properties necessary for user management such as UserName, PasswordHash, etc. You might wonder at this point why the ApplicationUser class is empty; the answer is so that you’re not tied to the base class and can extend ApplicationUser by adding your own properties.

Once we understand the above concepts, we can conclude that the purpose of registering IdentityUserAccessor is to obtain the current authenticated user through the GetRequiredUserAsync method.

Understanding the IdentityRedirectManager Class

Within the IdentityUserAccessor class that we’ve seen in the previous section, you may have noticed that a IdentityRedirectManager type is used as a parameter. Similarly, in Program.cs this same class is registered as follows:

builder.Services.AddScoped<IdentityRedirectManager>();

This is a very useful class that allows performing redirects within the application in a simple way, as in the following example:

RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation");

Understanding AuthenticationStateProvider, Plus PersistingServerAuthenticationStateProvider and PersistentAuthenticationStateProvider Classes

Continuing with the analysis of Program.cs, the next line we’ll analyze is the registration of PersistingServerAuthenticationStateProvider. At this point, it’s fundamental to understand another concept of ASP.NET Core Identity: what an AuthenticationStateProvider is. Basically, these are fundamental services that provide information about the user’s authentication state. Components like AuthorizeView rely on the information from this service to decide whether or not to show information to a user, because an AuthenticationStateProvider provides information about a user’s ClaimsPrincipal.

Examining the PersistingServerAuthenticationStateProvider class in more detail, we can notice an internal field called state of type PersistentComponentState, which will help us pass authentication data between server and client:

private readonly PersistentComponentState state;

We can see this better if we examine the OnPersistingAsync method:

private async Task OnPersistingAsync()
{
    if (authenticationStateTask is null)
    {
        throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}().");
    }

    var authenticationState = await authenticationStateTask;
    var principal = authenticationState.User;

    if (principal.Identity?.IsAuthenticated == true)
    {
        var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value;
        var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value;

        if (userId != null && email != null)
        {
            state.PersistAsJson(nameof(UserInfo), new UserInfo
            {
                UserId = userId,
                Email = email,
            });
        }
    }
}

In the above method, in summary, if a user authenticates correctly on the server side, the information is persisted as JSON in the PersistentComponentState. It’s worth noting that all this code, being part of your project, can be modified as needed, persisting other types of information if required.

Now, in the client-side project, you can see that we also have a Program.cs file. Within this file that contains fewer lines than the server project, there’s a line where the PersistentAuthenticationStateProvider type is registered:

builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();

Looking closely at PersistentAuthenticationStateProvider, we can see that on the client side, it tries to obtain the data from the PersistentComponentState that was previously persisted on the server side:

public PersistentAuthenticationStateProvider(PersistentComponentState state)
{
    if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
    {
        return;
    }

    Claim[] claims = [
        new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),
        new Claim(ClaimTypes.Name, userInfo.Email),
        new Claim(ClaimTypes.Email, userInfo.Email) ];

    authenticationStateTask = Task.FromResult(
        new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
            authenticationType: nameof(PersistentAuthenticationStateProvider)))));
}

This is how the client project manages to obtain information about the authentication that has been executed on the server side, allowing us to customize these classes as much as we need.

About the AddAuthorization and AddAuthentication Methods

Continuing our journey through Program.cs on the server side, we can find the execution of the AddAuthorization and AddAuthentication methods. The first method allows defining authorization policies for components:

builder.Services.AddAuthorization();

On the other hand, AddAuthentication is responsible for configuring authentication services, defining authentication schemes, and it’s possible to configure it with different providers like cookies and tokens.

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies();

In the template example, an authentication cookie is defined with schemes defined in the ApplicationScheme and ExternalScheme constants, meaning these schemes or names will appear for the cookies that will be saved in the browser once the client has authenticated correctly.

Specifying the Data Provider for Data Persistence

After executing the authentication and authorization methods, it’s time to define the database for persisting the information. Although by default a SQL Server database has been created, it’s possible to change this provider for another. For example, if this same template is created from Visual Studio Code, the default provider will be SQLite.

Let’s examine the template code a bit:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

The above code will look familiar if you’ve worked with Entity Framework before. We can navigate to the context called ApplicationDbContext, where you can see its following definition:

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<ApplicationUser>(options)
{
}

You can see that the application context inherits from IdentityDbContext, which is a specialized class that includes everything necessary to manage user identities, roles and claims. This is what causes tables like AspNetUsers, AspNetRoles, etc. to be available when you run the application. Similarly, you can see that as a generic, an ApplicationUser is used that represents a user and that we’ve reviewed previously. This is how the database that stores the application’s users is created.

Reviewing the AddIdentityCore, AddEntityFrameworkStores, AddSignInManager and AddDefaultTokenProviders Methods

Within the Program.cs file on the server side, you can see that the following methods are executed:

builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

The function of the above methods is as follows:

  • AddIdentityCore: Adds the basic Identity services, configuring ApplicationUser as the class for users. Additionally, it indicates that accounts must be confirmed to be able to log in.
  • AddEntityFrameworkStores: Configures Identity for Entity Framework to be the mechanism for storing information, specifying ApplicationDbContext as the context for the database. This is what allows the identity service to interact with the database to store user information, roles, etc.
  • AddSignInManager: Adds the SignInManager service to the DI container, which contains methods for logging in and out, generating tokens, verifying passwords, etc.
  • AddDefaultTokenProviders: Enables token generation for performing operations such as password reset, Email confirmation, two-factor authentication, etc.

Additional Information about Authentication in Blazor WebAssembly

At the end of the Program.cs file, you can notice that the following line is executed:

app.MapAdditionalIdentityEndpoints();

If we examine this class, we see that it contains a single method that defines endpoints required by the Identity Razor components. Remember that although it’s a Blazor WebAssembly application, you always need to perform user authentication operations on the server side. This is one of the mechanisms that allows hosting authentication endpoints on the server side. Here’s a portion of this class:

internal static class IdentityComponentsEndpointRouteBuilderExtensions
{
    // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project.
    public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints)
    {
        ArgumentNullException.ThrowIfNull(endpoints);

        var accountGroup = endpoints.MapGroup("/Account");

        accountGroup.MapPost("/PerformExternalLogin", (
            HttpContext context,
            [FromServices] SignInManager<ApplicationUser> signInManager,
            [FromForm] string provider,
            [FromForm] string returnUrl) =>
        {
            IEnumerable<KeyValuePair<string, StringValues>> query = [
                new("ReturnUrl", returnUrl),
                new("Action", ExternalLogin.LoginCallbackAction)];

            var redirectUrl = UriHelper.BuildRelative(
                context.Request.PathBase,
                "/Account/ExternalLogin",
                QueryString.Create(query));

            var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
            return TypedResults.Challenge(properties, [provider]);
        });
        ...
    }
}

Perhaps another question you might have at this point is, if the application was created specifying a global WebAssembly rendering mode, shouldn’t all components, including authentication ones, be rendered this way? To answer this question, we must go to the App.razor file in the server project. Here, you can see the following line:

private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account")
    ? null
    : InteractiveWebAssembly;

In the above code, what’s being done is basically specifying that all components within the /Account route should not behave as if they were WebAssembly components, which serves to protect authentication components from client-side attacks.

Using Authentication Components

Once we know the authentication fundamentals, you should know that thanks to all of the above, it’s possible to show or hide information to users based on their authentication state. You can clearly see this if you go to the client project and open the Auth.razor page. This file looks like this:

@page "/auth"

@using Microsoft.AspNetCore.Authorization

@attribute [Authorize]

<PageTitle>Auth</PageTitle>

<h1>You are authenticated</h1>

<AuthorizeView>
    Hello @context.User.Identity?.Name!
</AuthorizeView>

In the above code, the first attribute [Authorize] serves to block the rendering of the entire page to any unauthenticated user. On the other hand, the AuthorizeView component allows nesting the Authorized and NotAuthorized components to show different content blocks depending on the authentication state. Finally, AuthorizeView allows extracting information about authenticated users, as in the following example:

@page "/auth"

@using Microsoft.AspNetCore.Authorization

<PageTitle>Auth</PageTitle>

<h1>You are authenticated</h1>

<AuthorizeView>
    <Authorized>
        Hello @context.User.Identity?.Name!
    </Authorized>
    <NotAuthorized>
        You are not authenticated!
    </NotAuthorized>
</AuthorizeView>

The result of the above page is the following:

Content being rendered for unauthenticated users

Conclusion

Throughout this article, you have learned the basics of Blazor WebAssembly authentication, including important concepts regarding the Identity authentication system. It’s definitely a good time to explore the example project by putting the knowledge gained into practice and learning much more along the way.


About the Author

Héctor Pérez

Héctor Pérez is a Microsoft MVP with more than 10 years of experience in software development. He is an independent consultant, working with business and government clients to achieve their goals. Additionally, he is an author of books and an instructor at El Camino Dev and Devs School.

 

Related Posts

Comments

Comments are disabled in preview mode.