Share user authentication state with interactive WASM components
One of the big changes with .NET 8 is that you can mix and match different render modes for components within your Blazor app.
A fairly typical scenario is that you want to run your app on the server, using static server-side rendering, but then have islands of interactivity which use one of the interactive render modes (WASM, Server, or Auto).
But what if your app uses authentication? How can you ensure the user’s current auth state is available to all your components, regardless of which render mode they’re using?
In the last article we saw one way to handle authentication, using Auth0.
With that approach users are logged in to the server app at which point a cookie is issued to indicate that they are logged in.
Now let’s say we decide to take one component and run it interactively, using Blazor WASM.
You can get the source code for this example here.
First let’s make sure the client project is wired up for authentication/authorization:
Auth0BlazorDemo.Client/Program.cs
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
await builder.Build().RunAsync();
The client project needs a reference to Microsoft.AspNetCore.Components.WebAssembly.Authentication
which you can add via Nuget.
Now we can create our interactive component.
Auth0BlazorDemo.Client/Components/Pages/Weather.razor
@page "/weather"
@using Auth0BlazorDemo.Shared
@rendermode @(InteractiveWebAssembly)
<PageTitle>Weather</PageTitle>
<AuthorizeView>
<Authorized>
<!-- Weather goes here -->
</Authorized>
<NotAuthorized>
You are not authorised!
</NotAuthorized>
</AuthorizeView>
This component will spin up using WASM, and should show different UI depending on whether the user is logged in or not.
But if we try and run our app and view this component we get an error in the console.
One or more errors occurred. (No service for type ‘Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider’ has been registered.)
The problem is the component (and the client app hosting it) have no way, currently, to determine the authenticated state of a user.
The fix is to register an instance of AuthenticationStateProvider
to fetch that state.
But where’s the user’s auth state going to come from?
We need a way to take that state from the server and get it over to the client app when someone attempts to view our interactive component.
For this we can use PersistentComponentState.
PersistentComponentState
is a mechanism whereby we can persist state when running on the server, then access that from a component running via a different render mode.
The first step is to capture user information and persist it every time the user’s auth status changes on the server.
For that we can implement a custom AuthenticationStateProvider
in the server project.
Auth0BlazorDemo/PersistingAuthenticationStateProvider.cs
using System.Diagnostics;
using Auth0BlazorDemo.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace Auth0BlazorDemo.Identity;
public class PersistingAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable
{
private Task<AuthenticationState>? _authenticationStateTask;
private readonly PersistentComponentState _state;
private readonly PersistingComponentStateSubscription _subscription;
private readonly IdentityOptions _options;
public PersistingAuthenticationStateProvider(
PersistentComponentState persistentComponentState,
IOptions<IdentityOptions> optionsAccessor)
{
_options = optionsAccessor.Value;
_state = persistentComponentState;
AuthenticationStateChanged += OnAuthenticationStateChanged;
_subscription = _state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
}
private async Task OnPersistingAsync()
{
// persist the auth state
}
private void OnAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
{
_authenticationStateTask = authenticationStateTask;
}
public void Dispose()
{
_authenticationStateTask?.Dispose();
AuthenticationStateChanged -= OnAuthenticationStateChanged;
_subscription.Dispose();
}
}
We’ll get on to the actual code to persist the state in a moment, but first this wires up the mechanism to respond to auth changes.
OnAuthenticationStateChanged
will fire every time the user’s auth state changes.
There we capture the current AuthenticationState
task and store it.
We’ll be able to use that to retrieve the latest information about the user’s auth state.
In the constructor we take PersistentComponentState
and use it to register a subscription that will be fired every time Blazor decides to persist component state.
That subscription points to the OnPersistingAsync
method.
Here we’ll decide what to persist (which will then be available to components running via WASM).
By implementing IDisposable
we can ensure we properly dispose of the _authenticationStateTask
unregister the OnAuthenticationStateChanged
handler, and also dispose of the subscription we created for reacting when Blazor persists state.
To wire this up we need to register it in Program.cs.
builder.Services.AddScoped<AuthenticationStateProvider, PersistingAuthenticationStateProvider>();
Persisting auth state
So how to actually persist auth state?
For that we can use the _authenticationStateTask
and retrieve the user’s details, then use PersistentComponentState
to persist that information.
...
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 name = principal.FindFirst("name")?.Value;
if (userId != null && name != null)
{
_state.PersistAsJson(nameof(UserInfo), new UserInfo
{
UserId = userId,
Name = name,
});
}
}
}
...
Auth0 exposes the user’s name via the name
claim.
Here we take the user Id, and user name values, assign them to a new instance of a class called UserInfo
, then persist that via component state.
I’ve created the UserInfo
class in the client project for this app. That way we can reference it from both the server and client apps.
Auth0BlazorDemo.Client/UserInfo.cs
public class UserInfo
{
public required string UserId { get; set; }
public required string Name { get; set; }
}
With that the user’s basic information will be persisted in PersistentComponentState
and will be available to our components running via WASM.
Now to wire up the other side of the equation, and access that state.
Retrieve the user state
Now we need a corresponding AuthenticationState
provider over in the client project to retrieve the persisted state.
Auth0BlazorDemo.Client/PersistentAuthenticationstateProvider.cs
public class PersistentAuthenticationStateProvider : AuthenticationStateProvider
{
private static readonly Task<AuthenticationState> DefaultUnauthenticatedTask =
Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
private readonly Task<AuthenticationState> _authenticationStateTask = DefaultUnauthenticatedTask;
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.Name) ];
_authenticationStateTask = Task.FromResult(
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
authenticationType: nameof(PersistentAuthenticationStateProvider)))));
}
public override Task<AuthenticationState> GetAuthenticationStateAsync() => _authenticationStateTask;
}
AuthenticationStateProvider
requires us to declare the GetAuthenticationStateAsync
method.
Here we wire that up to initially return a default AuthenticationState
for when the user isn’t authenticated.
This returns an instance of AuthenticationState
with an ’empty’ ClaimsPrincipal, which essentially indicates that the user is unauthenticated.
In the constructor we take in PersistentComponentState
via dependency injection and use it to try and retrieve the persisted user information.
If we find state for UserInfo
we use it to return a new instance of AuthenticationState
complete with claims for the user’s Name
and NameIdentifier
.
That’s enough to indicate to Blazor that the user is authenticated.
Before we forget, we need to wire up this new AuthenticationStateProvider
in Program.cs.
Auth0BlazorDemo.Client/Program.cs
// add these
using Auth0BlazorDemo.Client.Services;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
// and this
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
await builder.Build().RunAsync();
The user is authenticated
Wit this our interactive WASM component now works, and correctly shows UI based on whether the user is authenticated or not.
To see how this actually works, if we inspect the HTML for our page (which hosts the interactive component) we can see the encoded component state in the markup (Blazor-Server-Component-State).
This is how Blazor gets state over the boundary between server and client, by encoding the state and rendering it in the markup returned from the server. The client app (Blazor WASM) then fetches and decodes this state.
What it doesn’t do
This approach ensures we can use Blazor’s built-in auth components in interactive WASM components.
But we’re not transmitting user tokens or credentials in component state.
For that we can depend on the cookie that’s created when users authenticate with our server app.
That cookie will be included when our interactive WASM component makes calls to the server (for example, API calls to fetch data) and should be used to check user access/permissions.
In Summary
Persistent Component State is a handy mechanism for taking state from the server and passing it to components running via Interactive WebAssembly.
Here we’ve seen how you can use it to take user information from the server and pass it to the client.
This enables us to use Blazor’s built-in auth components like AuthorizeView
in all components, irrespective of where they’re running (on the server via SSR, or interactively on the server or client via WASM).
Check out the source code for this example here.