Azure Active Directory multitenant integration with Blazor WASM – Part 2

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.

  1. Create a new ASP.NET hosted Blazor WASM application in Visual Studio
  2. Important: For Authentication type select Microsoft identity platform
  3. 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)
  4. 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.

  1. Edit wwwroot/appsettings.json
  2. 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"
  }
}
  1. 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).

  1. Edit Program.cs
  2. 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.

  1. Edit wwwroot/appsettings.json
  2. 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"
}
  1. Replace {Application (client) ID} with the GUID you noted earlier

Update the server to validate user tokens, and use Authorization

  1. Edit Program.cs
  2. 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.

  1. Open the server Program.cs file
  2. 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.

  1. Read the configuration to find the AzureAD::Instance value, which is a URL
  2. HTTP GET that URL with /organizations/v2.0/.well-known/openid-configuration HTTP/1.1 appended
  3. The result is a JSON document, one of the keys in the document is jwks_uri
  4. 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
  5. 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.
  6. The keys from the trusted authority will now be cached to improve performance.

One thought on “Azure Active Directory multitenant integration with Blazor WASM – Part 2

  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *