Simpler auth for Blazor Web Apps with Auth0?

January 16, 2024 · 8 minute read · Tags: aspnet | blazor

Are you using ‘user stories’ to capture requirements for your web app?

If so, I bet you’ve seen (or written) at least one story like this:

As a user, I want to log in, so I can use the app

Here’s the thing.

Nobody wants to log in.

Users don’t go to bed dreaming of a future where they can spend ages trying to remember their username and password, all for the reward of being greeted by a pithy welcome message when they finally log in to the app.

“Welcome back Jon!”

They want to use your app because it empowers them to achieve something.

But, to do that, they need to log in.

Which means we, as Developers, are required to deliver a robust, secure mechanism to make that possible.

In recent years auth has gotten complicated.

Spin up a new Blazor .NET 8 app with auth enabled and you’ll see a large number of additional components.

These are designed to give you the basic auth UI for your app, including user registration, login, forgotten password etc.

This is a good place to start, if you want to use MS Identity to store user details in you own database.

But there is another option - to outsource the UI, and complexity of Auth, to another service.

This is where third parties like Auth0 come in.

I’ve used Auth0 for a number of projects in recent years, but always with client-side frameworks (Blazor WASM and/or JS frameworks).

As part of a recent migration of https://practicaldotnet.io to .NET 8, I needed to figure out how to get Auth0 working with a Blazor app which is primarily running on the server (using .NET 8’s new server-side rendering mode).

Here’s what I learned, and how I got it working.

Use Auth0.AspNetCore.Authentication

Early on I realised this integration should be similar to how you would integrate Auth0 with any other .NET web application, running on the server.

My previous experience of Auth0 was with Blazor running as a full ‘Single Page App’ in the browser, using Web Assembly, storing user tokens in JWTs.

But for this project I’d be rendering everything on the server, which means using cookies instead of JWTs stored in local storage (as local storage isn’t available when rendering components on the server).

Research led me to Auth0’s ASP.NET NuGet package:

dotnet add package Auth0.AspNetCore.Authentication

Once you have that installed there’s a small amount of config needed to get it set up in Program.cs.

using Auth0.AspNetCore.Authentication;

...

builder.Services
    .AddAuth0WebAppAuthentication(options =>
    {
        options.Domain = builder.Configuration["Auth0:Domain"]!;
        options.ClientId = builder.Configuration["Auth0:AppClientId"]!;
    });

This is just about the minimum you need to integrate your ASP.NET app with Auth0 and takes care of a number of details under the hood, including configuring the application cookie we’ll be using when users are logged in.

The next step is to provide an endpoint where users can log in.

Implement Log In

For that I spun up a simple minimal API endpoint.

using Microsoft.AspNetCore.Authentication;

...

app.MapGet("/Account/Login", async (HttpContext httpContext, string returnUrl = "/") =>
{
    var authenticationProperties = new LoginAuthenticationPropertiesBuilder()
        .WithRedirectUri(returnUrl)
        .Build();

    authenticationProperties.IsPersistent = true;

    await httpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);
});

httpContext.ChallengeAsync triggers the authentication process. It issues a challenge to the Auth0 Authentication Scheme.

If the current context’s request is unauthenticated the user will be redirected to the Auth0 login page.

The second parameter sets the properties for that challenge, including the RedirectUri. This is where the user will be redirected to after successfully logging in.

For this to actually work you’ll need to add the return URL to the Allowed Callback URLs list for the site (in Auth0’s control panel).

I also decided to set IsPersistent to true.

This ensures the cookie (that will eventually be issued when the user logs in) persists between browser sessions.

With this the users should now be able to log in when their browser makes a GET request to:

<your-site>/Account/Login

Implement Log Out

Next I added another minimal API endpoint for logging out.

using Microsoft.AspNetCore.Authentication.Cookies;

...

app.MapPost("/Account/LogOut", async (HttpContext httpContext) =>
{
    await httpContext.SignOutAsync(Auth0Constants.AuthenticationScheme);
    await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
});

This ensures the user’s cookies are destroyed, effectively logging them out of the app.

UI For Logging In/Out

The last step was to implement the UI for users to log in, or log out (if they’re already logged in).

Rather than have a direct Login link I wanted to show a link to the restricted members area (in the main nav bar for the application).

The desired flow is that a user who isn’t logged in will click that link, and be redirected to login.

Once logged in they’ll be returned to the members page.

Lock down the members page

First I locked the members page (razor component) down, making it accessible to logged in users only.

Members.razor

@page "/Members"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

@attribute [Authorize] does the job here.

Then I implemented a link to the members page, in the top nav bar for the site.

<a href="/members">My Courses</a>

If a user clicks that link while not logged in they’re redirected to the default Login URI for the ASP.NET application - /Account/Login

There, the minimal API endpoint we created earlier will handle the request and redirect them to Auth0.

I only wanted logged in users to see a log out button, so for that I used AuthorizeView.

First step was to add the @using Microsoft.AspNetCore.Components.Authorization package:

dotnet add package Microsoft.AspNetCore.Components.Authorization

This gives us the AuthorizeView component.

For logging out, the easiest option is to implement a form which makes a POST request to /Account/Logout.

<AuthorizeView>
    <Authorized>
       <form action="Account/Logout" method="post">
            <AntiforgeryToken/>  
            <button type="submit">
                Logout
            </button>
       </form>
    </Authorized>
</AuthorizeView>

Add CascadingAuthenticationState to your app

Almost there, but run this now and you’ll see an error:

InvalidOperationException: Authorization requires a cascading parameter of type Task. Consider using CascadingAuthenticationState to supply this.

CascadingAuthenticationState is a built-in Blazor component which provides information about the authentication state of the current user.

AuthorizeView uses CascadingAuthenticationState internally, to retrieve the user’s auth state so we need to ensure it’s available for our conditional logic to work.

Prior to .NET 8 you would typically wrap your entire app in the CascadingAuthenticationState component, like this:

<CascadingAuthenticationState>
    <Router ...>
        <Found ...>
            ...
        </Found>
    </Router>
</CascadingAuthenticationState>

As of .NET 8 you can achieve the same result by adding the following line to Program.cs.

builder.Services.AddCascadingAuthenticationState();

It’s alive!

With this users can now log in to the app via Auth0.

When a user logs in a cookie is created stored in browser storage.

Dev tools showing several .AspNetCore.Cookies, scoped to the site practicaldotnet.io

As the user interacts with the site the Blazor app (running on the server) will check that cookie, to ensure they’re authenticated.

Gotchas

I did run into one confusing error.

At some point during testing I saw this error message:

An unhandled exception occurred while processing the request.

OpenIdConnectProtocolInvalidNonceException: IDX21323: RequireNonce is 'True'. 
    OpenIdConnectProtocolValidationContext.Nonce was null, 
    OpenIdConnectProtocol.ValidatedIdToken.Payload.Nonce was not null. 
    The nonce cannot be validated. If you don't need to check the nonce, 
    set OpenIdConnectProtocolValidator.RequireNonce to 'false'. 
    Note if a 'nonce' is found it will be evaluated.

After several hours scratching my head I realised I was running the app locally using HTTP instead of HTTPS.

I switched to HTTPS for local development, and the problem went away.

In Summary

Auth0 (and other third party hosted auth solutions) offer a convenient path to implementing authentication and authorization in your Blazor web apps.

Building on standard ASP.NET mechanisms and plumbing Auth0 provides a thin layer of abstraction, making it quick and easy to get Auth working in your app.

Next up

In this case I didn’t need to connect to an API, and all the components are running via server-side rendering.

But in many Blazor web apps you’ll likely find yourself making API calls and you may want some components to run using Blazor Web Assembly.

In the next article(s) we’ll see how to get both of those requirements up and running, building on what we’ve seen here.

References

I picked up a lot of the details to get this working from the following Github thread.

Big thanks to Tomás López Rodríguez for sharing this working example of using Auth0 with Blazor in .NET 8 which I leaned on heavily to get this up and running.

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