Blazor Web App - WASM Hosted - Azure AD Authentication

Blazor Web App - WASM Hosted - Azure AD Authentication

Setup Azure AD Authentication in the latest .NET 8 Blazor Web App template using WASM (WASM ASP.NET Hosted)

ยท

8 min read

๐Ÿ““
The title mentions Azure AD as that is what most people might search for, but in the rest of the article I will use the new name Microsoft Entra ID (ME-ID)

Article Goal

In this article I will discuss how to get ME-ID Authentication working with a Blazor Web App using Global WASM Interactivity. This will not tackle Server or Auto interactivity modes.

I decided to write this because the new Blazor Web App template does not include an option for pre-setup of ME-ID Authentication, like the old Blazor templates did, and especially for what essentially will be a Blazor WASM ASP.NET Hosted app.

When I was trying to do this myself, I was struggling and couldn't find much out there in a way of help. Hopefully this basic guide will help get you started.

Setting up Application Registrations

For this to work, we require two application registrations (app reg) to be created and configured within your ME-ID tenant. There will be one for the WASM Client to use and one for the Server to use.

Server Application Registration

The server app reg is very simple. It just needs to be scoped to your organization to allow any auth tokens you server API receives from a client app to be validated using this app registration. This means any API resources you have marked as secured will be usable by the client. Only auth tokens created from the client app registration will be accepted, this is achieved by exposing an API from the server app reg, to which the client app registration will be scoped to.

Register an application screen.

  1. Enter the following details
    Name: app-blazorwebappwasm-server
    Supported account types: This organizational directory only (single tenant)

  2. Press Register

Expose an API screen.

  1. Press Add a scope

    Keep the Application ID URI as suggested.

  2. Press Save and Continue

  3. Enter the following details
    Scope name: API.Access
    Who can consent: Admins only
    Admin consent display name: Access API
    Admin consent description: Allows access to the API

    State: Enabled

  4. Press Add scope

That's the Server app reg complete.

Client Application Registration

The Client app reg will be configured to allow the client to authenticate to your ME-ID directory. This will result in a token being sent to your client which you can then use to control user authorization within the application, and also to use when making API calls to your server. Secure resources on the server will require a valid token to be sent from the client.

Register an application screen.

  1. Enter the following details
    Name: app-blazorwebappwasm-client
    Supported account types: This organizational directory only (single tenant)

    Redirect URI: Single-page application (SPA) - localhost/authentication/login-callback

    ๐Ÿ““
    We will come back and update this redirect value once we have created our Blazor WebApp.
  2. Press Register

API permissions screen.

  1. Press Add a permission

  2. Choose the APIs my organization uses tab

  3. Select the server app reg create in the previous section

  4. Tick the API.Access API

  5. Press Add permissions

  6. Press Grant admin consent for Default Directory

    ๐Ÿ““
    If you are not a Tenant Admin then you won't be able to press this button and will need to ask someone in your organization who is. There is another way to achieve the same thing which is documented here in this MS docs article. Look at the blue Important call out near the bottom of this section.

Create the Blazor Web App

We are going to create a Blazor Web App which will use Global WebAssembly Interactivity. I will be using Visual Studio 2022 Preview, but the same will work with Visual Studio 2022.

  1. Create new project

  2. Choose Blazor Web App

  3. Give it a Name, a Location and press Next

  4. Configure the following

    Interactive Mode: WebAssembly

    Interactivity Location: Global

    Include sample pages: Yes

  5. Press Create

Update Client App Registration Redirect URI

Before we go any further and forget, now that the Web App is created, we can update the Redirect URI in the client app reg so it knows how to return the authenticate tokens to after login.

  1. In the Server project, inside the Properties folder, open the LaunchSettings.json file.

  2. Copy the https URL, including the port, from the https > applicationUrl
    In my instance it was https://localhost:7205

  3. Go to the Client app reg in ME-ID

  4. Open the Authentication screen

  5. Update the Redirect URI you created previously to https://localhost:7205/authentication/login-callback

  6. Press Save

Add NuGet Packages

Server project

  • Microsoft.Identity.Web

Client project

  • Microsoft.Authentication.WebAssembly.Msal

  • Microsoft.Extensions.Http

Update App.razor

In the Server project, look for App.Razor inside the Components folder and update it as follows.

  1. Add the MSAL JavaScript reference above the blazor.web.js script reference
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
  1. Update the Routes component reference to change the rendermode attribute.
<Routes @rendermode="@( new InteractiveWebAssemblyRenderMode(false))" />

We do this to disable pre-rendering. When pre-rendering is on, you will get an error along the lines of

โ—
InvalidOperationException: Cannot provide a value for property 'AuthenticationStateProvider' on type 'Microsoft.AspNetCore.Components.Authorization.CascadingAuthenticationState'. There is no registered service of type 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'.

Configure Services and App

Server Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

// Auth Services which will read from AzureAd section in appsettings.json
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd");

// Add these after `app.UseAntiforgery();`
app.UseAuthentication();
app.UseAuthorization();

Client Program.cs

using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

// Add the HTTP Client which will allow you to make API calls to the server.
builder.Services.AddHttpClient("ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ServerAPI"));

// Setup Auth Configuration, again reads from AzureAd section in appsettings.json
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("<SCOPE FROM SERVER APP REGISRATION API>");
});

Update the <SCOPE FROM SERVER APP REGISTRATION API> string with the scope you exposed on the Server app registration.

For example api://f84023e7-087c-423c-9257-b7af1a3dc892/API.Access

Appsetting.json setup

Server project

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com",
    "Domain": "<your domain>.onmicrosoft.com", // Found on the summary page of ME-ID
    "TenantId": "<your tenant id>", // Found on the summary page of ME-ID
    "ClientId": "<server app reg clientid>", // ClientId of the Server app registration
    "Scopes": "API.Access", // This should match the name of the API you exposed on your Server app registrtraion
    "CallbackPath": "/signin-oidc"
  },
  "AllowedHosts": "*"
}

Client Project

You will notice that the Client project doesn't have an appsettings.json file, so you will need to create it.

  1. In the root of the project create a folder called wwwroot

  2. Inside wwwroot create appsettings.json file

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/<your tenant id>", // Found on the summary page of ME-ID
    "ClientId": "<client app reg clientid>", // ClientId of the Client app registration
    "ValidateAuthority": true
  }
}

Update Routes.razor

If you were to run the application now, it would work as normal, and wouldn't ask the user to login to their ME-ID tenant in order to use it. We need to continue to amend out of the box components, as well as add a couple of helper components to allow authentication to trigger and work as expected.

In the Client project, amend the Routes.razor file to look like this.

@using Blog.BlazorWebAppWasmAzureAdAuth.Client.Layout
@using Blog.BlazorWebAppWasmAzureAdAuth.Client.Auth
@using Microsoft.AspNetCore.Components.Authorization

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        <RedirectToLogin />
                    }
                    else
                    {
                        <p role="alert">You are not authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Create RedirectToLogin.razor component

Create a folder called Auth, inside then create the RedirectToLogin.razor component.

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateToLogin("authentication/login");
    }
}

Create Authentication.razor component

Inside the Auth folder, create the Authentication.razor component.

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

@code{
    [Parameter] public string? Action { get; set; }
}
๐Ÿ““
If you were to run the web app now, it will load as normal and you can navigate between pages just fine. But it hasn't asked you to authenticate! This only happens when you attempt to access a Page or Component which requires authorization.

Add Authorization

In the Client project, under the Pages folder, open the Home.razor file and add the following to the top.

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

This marks this whole page as requiring that the user is an authenticated user. And will trigger the web app to ask you to authenticate.

Test

Now when you launch the web app, because the first page you are routed to is the Home page, and it has been marked as requiring authorization, you will be immediately redirected to the Microsoft authentication page to login.

The login prompt will ask you, as a user, to allow the web app to access your basic data since that was all setup as delegated on the app registration.

โ—
As of writing, there is a bug within the current version of .NET 8 which results in the browser saying 'This page isn't working at the moment'. The workaround is documented below.

Authentication bug workaround

To resolve the This page isn't working at the moment message you need to add some middleware.

In the Server project, create the AuthorizationMiddlewareResultHandler.cs middleware class.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;

namespace Blog.BlazorWebAppWasmAzureAdAuth.Middleware;

public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
    {
        return next(context);
    }
}

Now register it as a service in Program.cs

builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();

Now when you run the web app, you should be redirected to the Microsoft Authentication page as expected.

ย