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!
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
:
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.
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
:
After applying the migration, we just need to refresh the page to see that we can now register without this error appearing:
Once we have the example project ready, let’s examine its operation more closely.
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.
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:
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.
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");
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.
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.
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.
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.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.
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:
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.
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.