This is the second part of a two part blog. The first describes the process of setting up an Azure Active Directory multitenant on Azure, this part describes how to integrate with a Blazor WASM application.
Create the solution
We want to use most of the template that Visual Studio generates for us, but not all of it. This is because the default template creates two AAD apps, one for the client and one for the server. We are only going to use a single AAD registered app to manage access.
- Create a new ASP.NET hosted Blazor WASM application in Visual Studio
- Important: For
Authentication type
selectMicrosoft identity platform
- The
Required components
wizard will appear and try to connect to your AAD registered app. This will also add items to AAD that we don’t want (another app registration) - Click
Cancel
Match our development sign-in callback URL to the AAD registered application’s callback URL
As mentioned in part 1, Azure Active Directory won’t redirect to a URL with the user token after authentication unless it has been registered in advance; this is to prevent a malicious server receiving users’ tokens. As we previously registered our development callback URL as https://localhost:6510/authentication/login-callback
, this means we must ensure our application runs on https://localhost:6510
Edit Properties/launchSettings.json
in both the client and server app, and set the applicationUrl
to https://localhost:6510
Set client application configuration for AAD
The Blazor WASM application needs to know which application it is going to request access for, what signing authority it should trust (AAD), and which scopes it is interested in.
- Edit
wwwroot/appsettings.json
- Replace the contents with the following
{
"AzureAd": {
"ClientId": "{Application (client) ID}",
"Authority": "https://login.microsoftonline.com/organizations"
},
"ServerApi": {
"Scopes": "api://{Application (client) ID}/access_as_user"
}
}
- Replace
{Application (client) ID}
with the GUID you noted earlier
Set up client authentication to read from our configuration
We now need to change our client application to read its authentication settings from the configuration file. We do this so we can have a single code base and multiple deployment environments (dev, test, prod).
- Edit
Program.cs
- Change the
AddMsalAuthentication
section so scopes are read from the config file
builder.Services.AddMsalAuthentication(options => { builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); string? scopes = builder.Configuration!.GetSection("ServerApi")["Scopes"] ?? throw new InvalidOperationException("ServerApi::Scopes is missing from appsettings.json"); options.ProviderOptions.DefaultAccessTokenScopes.Add(scopes); // Uncomment the next line if you have problems with a pop-up sign-in window // options.ProviderOptions.LoginMode = "redirect"; });
Set server application configuration for AAD
We now need to let the server know which authority it should trust for checking tokens are valid. In this example we will be telling it to trust https://login.microsoftonline.com/organizations
– the organizations
part means we will trust any organisation’s AAD. If we wanted to trust only a specific directory we would put its object ID (guid) there instead.
- Edit
wwwroot/appsettings.json
- Replace the contents with the following
"AzureAd": { "Instance": "https://login.microsoftonline.com/", "TenantId": "organizations", "ClientId": "{Application (client) ID}", "Scopes": "api://{Application (client) ID}/access_as_user" }
- Replace
{Application (client) ID}
with the GUID you noted earlier
Update the server to validate user tokens, and use Authorization
- Edit
Program.cs
- Replace the following
// Replace
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
// with
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
builder.Services.AddAuthorization();
Note: Until I work out how to pass scopes in the token, edit WeatherForecastController
and comment out the [RequiredScope]
attribute.
Optional: Make the server API and static pages only
As our server will only be serving static Blazor WASM content and API requests, we can remove support for Views and Razor Pages.
- Open the server
Program.cs
file - Makethe following changes
// Replace
builder.Services.AddControllersWithViews();
// with
builder.Services.AddControllers();
// Remove
builder.Services.AddRazorPages();
// Remove
app.UseRazorPages();
How it works
When our server receives a request for an endpoint that that has an [Authorize]
attribute, the server will need a user token, and need to know that the token was signed by the AAD authority and not just any old unknown third party. Do do this, the .Net framework will contact the Instance
we specified in our config file for verification. These are the steps the .Net framework takes behind the scenes.
- Read the configuration to find the AzureAD::Instance value, which is a URL
- HTTP GET that URL with
/organizations/v2.0/.well-known/openid-configuration HTTP/1.1
appended - The result is a JSON document, one of the keys in the document is
jwks_uri
- The value of this tells the server where to find a list of keys that the AzureAD::Identity might use to sign the token. For example
/organizations/discovery/v2.0/keys HTTP/1.1
- The server will then fetch that document for the keys, and ensure the token was signed with one of those keys, thus ensuring this is a valid token.
- The keys from the trusted authority will now be cached to improve performance.
what an elegant solution, I’ll def use this for my first try at multitenant AAD, thankyou
Carl Franklin from dotnetrocks.com has also some interesting AAD+Blazor stories.