Share user authentication state with interactive WASM components

January 24, 2024 · 7 minute read · Tags: aspnet | blazor

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.

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.

    Next up

    Finally! Improved Blazor Server reconnection UX
    .NET 9 changes how your Blazor Server app behaves when server connection is lost
    .NET 9 improves JavaScript module importing for Blazor
    .NET 9 ensures your users always get the latest version of your JS modules
    How to use .NET 9 to ensure users always get the latest version of your stylesheets
    .NET 9 changes how static files are served, and it solves a long-standing problem