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)
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.
Enter the following details
Name: app-blazorwebappwasm-server
Supported account types: This organizational directory only (single tenant)Press Register
Expose an API screen.
Press Add a scope
Keep the Application ID URI as suggested.
Press Save and Continue
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 APIState: Enabled
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.
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.Press Register
API permissions screen.
Press Add a permission
Choose the APIs my organization uses tab
Select the server app reg create in the previous section
Tick the API.Access API
Press Add permissions
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.
Create new project
Choose Blazor Web App
Give it a Name, a Location and press Next
Configure the following
Interactive Mode: WebAssembly
Interactivity Location: Global
Include sample pages: Yes
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.
In the Server project, inside the Properties folder, open the LaunchSettings.json file.
Copy the https URL, including the port, from the https > applicationUrl
In my instance it was https://localhost:7205Go to the Client app reg in ME-ID
Open the Authentication screen
Update the Redirect URI you created previously to https://localhost:7205/authentication/login-callback
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.
- Add the MSAL JavaScript reference above the blazor.web.js script reference
<script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
- 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
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.
In the root of the project create a folder called wwwroot
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; }
}
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.
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.