AuthN-ing Blazor WASM with Azure AD B2C

If you are building a standalone Blazor WebAssembly app and hosting it on GitHub Pages, or any other static file host without a server side component, you will hit a wall the moment you need user sign in. There is no backend to hold a client secret, no server session to rely on, and no built in authentication feature like the one Azure Static Web Apps gives you out of the box. Azure AD B2C solves this cleanly because it is designed from the ground up for public client applications that cannot keep a secret.

This walkthrough covers setting up an Azure AD B2C tenant, wiring it into a standalone Blazor WASM project using the dotnet CLI, and getting a working sign up and sign in flow running locally. I have added a few production notes along the way that the original configuration steps do not call out, based on what actually trips people up when they take this from a demo to a real app.

Azure AD versus Azure AD B2C

Azure AD and Azure AD B2C sit on the same underlying identity platform and both speak OAuth2 and OpenID Connect, so a lot of the MSAL configuration looks familiar if you have used one before. The difference is in what each one is built for. Azure AD manages access to internal, organisation wide resources such as SharePoint, Teams, and Dynamics 365, with permissions scoped per resource and per account inside your tenant’s directory.

Azure AD B2C, on the other hand, exists to authenticate the users of a specific application, not employees of an organisation. It runs as an independent tenant with its own user store, so unless you deliberately federate it with your corporate directory, B2C users have no relationship with your internal Azure AD at all. This matters when you are picking between the two: if you are building a customer facing app where anyone with an email address should be able to sign up, B2C is the right tool. If you are building an internal line of business app, plain Azure AD is usually simpler and gives you group based authorization for free.

Setting up the Azure AD B2C tenant

Start by creating an Azure AD B2C resource from the Azure Portal. You can name the tenant whatever you like, the examples here use fitabilitydevkr. Once the resource finishes provisioning, you land on a summary page with an “Open B2C Tenant” link that switches you into the B2C admin context, which is a separate directory from your main Azure subscription’s tenant.

The provisioned Azure AD B2C resource in the Azure Portal.
The provisioned Azure AD B2C resource in the Azure Portal.

Inside the B2C tenant, open App registrations from the left navigation and click New registration. This is where you register the Blazor app itself as a client that B2C will issue tokens to.

Starting a new app registration inside the B2C tenant.
Starting a new app registration inside the B2C tenant.

Fill in the registration form with these values. Name the app something recognisable, for example my-fitability-app. Under supported account types, choose “Accounts in any identity provider or organisational directory”, since B2C manages its own identity providers rather than relying on work or school accounts. For redirect URI, select the Single-page Application platform and enter https://localhost/authentication/login-callback, which matches the callback route that the Blazor WASM authentication library expects out of the box. Tick the box to grant admin consent to the openid and offline_access permissions so users are not prompted for consent on every sign in.

App registration details for the Blazor client.
App registration details for the Blazor client.

After the app is created, go back into its Authentication blade and confirm three things are set correctly, because it is easy to fat finger one of these and end up debugging a silent redirect failure later. The redirect URI should be listed under Single-page Application, not Web. The Implicit grant and hybrid flows section should have both Access tokens and ID tokens checked, since SPA authentication relies on these token types being issued directly to the browser. And supported account types should still read “Accounts in any identity provider or organisational directory”.

Authentication settings confirming SPA redirect URI and token types.
Authentication settings confirming SPA redirect URI and token types.

Once you are back on the app’s overview page, copy the Application (client) ID. You will need this exact value twice: once when scaffolding the Blazor project with the dotnet CLI, and again in the app’s configuration file.

The application client ID on the registration overview page.
The application client ID on the registration overview page.

Creating the sign up and sign in user flow

A user flow is B2C’s term for a prebuilt, configurable authentication journey. Rather than writing your own registration and login pages, you pick a flow type and B2C hosts the pages for you. From the B2C tenant, open User flows and click New user flow.

Selecting a user flow type from the B2C tenant.
Selecting a user flow type from the B2C tenant.

Choose the “Sign up and sign in” flow type and select the Recommended version rather than the older deprecated one, since Microsoft is steadily retiring the legacy version and the recommended flow supports more identity provider combinations.

Choosing the Sign up and sign in user flow, recommended version.
Choosing the Sign up and sign in user flow, recommended version.

Name the flow SignUpSignIn and select Email signup as the identity provider. You can add social identity providers such as Google or Facebook here as well, but each one needs its own app registration on the provider’s side before it will show up as an option, so keep it to email for a first pass and add social login afterwards once the basic flow works.

Naming the user flow and selecting Email signup as the identity provider.
Naming the user flow and selecting Email signup as the identity provider.

In the user attributes and claims section, tick whichever fields you want collected at sign up and returned in the token. Display Name and Email Address are the two most apps need. Resist the temptation to collect more than you actually use in the UI, every extra attribute is one more field on the sign up form and one more thing users abandon the flow over.

Selecting user attributes and token claims for the flow.
Selecting user attributes and token claims for the flow.

Save the flow and the B2C side of the configuration is done. You now have a client app registration and a hosted sign up and sign in journey, both of which the Blazor project needs to reference.

Scaffolding the Blazor WebAssembly project

As of Visual Studio 17.3, the New Project wizard still does not expose an option to wire up Azure AD B2C when creating a Blazor WASM project, so you have to fall back to the dotnet CLI for this part.

Visual Studio's Blazor WebAssembly template, which has no B2C authentication option in the wizard.
Visual Studio’s Blazor WebAssembly template, which has no B2C authentication option in the wizard.
dotnet new blazorwasm \
  --output "MyBlazorWasmApp" \
  --framework net6.0 \
  --hosted false \
  --auth IndividualB2C \
  --aad-b2c-instance "https://fitabilitydevkr.b2clogin.com/" \
  --domain "fitabilitydevkr.onmicrosoft.com" \
  --client-id "<CLIENT_ID>" \
  --susi-policy-id "B2C_1_SignUpSignIn"

A few of these flags are worth understanding rather than copy pasting blind. –hosted false scaffolds a standalone WASM app with no ASP.NET Core backend, which is exactly what you need for GitHub Pages or any static file host; setting it to true instead scaffolds a server hosted variant with an API project. –auth IndividualB2C tells the template to wire up the Microsoft.Authentication.WebAssembly.Msal package and generate the login and logout UI. –susi-policy-id takes the name of the user flow you just created, prefixed with B2C_1_ which is the naming convention B2C uses internally for every policy. Running this command generates a working project with authentication scaffolding already in place, you do not need to add the MSAL NuGet package or the login component yourself.

Wiring up MSAL authentication in Program.cs

The template already adds most of what is needed, but it is worth looking at exactly what AddMsalAuthentication does because you will need to touch this method again the moment you add an API scope or a second policy.

builder.Services.AddScoped(sp => new HttpClient
{
    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
 
builder.Services.AddMsalAuthentication(options =>
{
    options.ProviderOptions.DefaultAccessTokenScopes.Add("openid");
    options.ProviderOptions.DefaultAccessTokenScopes.Add("offline_access");
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
});
 
await builder.Build().RunAsync();

AddMsalAuthentication registers the MSAL.js backed authentication service and the AuthenticationStateProvider that Blazor’s AuthorizeView and [Authorize] attribute depend on. The two default scopes matter for different reasons: openid is what actually gets you an ID token back, and offline_access is what gets you a refresh token so MSAL can silently renew the session in an iframe instead of forcing a full redirect every time the access token expires. The Bind call pulls the Authority, ClientId, and ValidateAuthority values straight out of configuration, which is why appsettings.json needs to match the shape MSAL expects exactly.

Configuring appsettings.json

Update appsettings.json under wwwroot with the B2C authority, using the same tenant name and policy name from earlier.

{
  "AzureAdB2C": {
    "Authority": "https://fitabilitydevkr.b2clogin.com/fitabilitydevkr.onmicrosoft.com/B2C_1_SignUpSignIn",
    "ClientId": "<CLIENT_ID>",
    "ValidateAuthority": false
  }
}

ValidateAuthority has to be false here, and this catches people out if they are used to plain Azure AD configuration where it is normally left true or omitted. B2C authority URLs include the policy name as part of the path, which does not match the metadata document format that MSAL’s authority validation expects for regular Azure AD tenants, so validation needs to be switched off explicitly or every token request fails before it even reaches B2C.

Running and testing the flow

Build and run the project locally. You should see the default Blazor landing page with a Log in link in the top right corner.

The Blazor WASM app running locally with the Log in link visible.
The Blazor WASM app running locally with the Log in link visible.

Clicking Log in opens the B2C hosted sign in page in a popup, styled with B2C’s default template. From here a new user can click Sign up now to register with an email and password, or an existing user can sign in directly.

The B2C hosted sign in and sign up page opened from the Blazor app.
The B2C hosted sign in and sign up page opened from the Blazor app.

After a successful sign in, MSAL closes the popup, stores the tokens in the browser, and the app’s AuthenticationStateProvider picks up the new identity. The username should now show up wherever you have wired up the AuthorizeView component or read the identity from a component parameter.

The Blazor app after a successful sign in, showing the authenticated username.
The Blazor app after a successful sign in, showing the authenticated username.

Where this approach falls short

This gets a working demo up quickly, but there are a few gaps you should plan for before shipping it. The default MSAL token cache in a standalone WASM app lives in browser storage, which means a user who clears site data or switches browsers has to sign in again, there is no server side session to fall back on. If your app needs to call a protected API, you will need to add that API’s scope to DefaultAccessTokenScopes and configure the API to validate B2C issued tokens, which is a separate piece of configuration on the API side that this walkthrough does not cover.

The built in sign up and sign in flow used here is a Microsoft managed template with limited customisation, mostly logo, colours, and which fields to collect. If you need custom validation logic, multi step registration, or integration with an external user store during sign up, you need B2C custom policies built on the Identity Experience Framework, which is a considerably steeper learning curve involving XML policy files rather than a portal wizard. Reach for custom policies only when the built in flow genuinely cannot do what you need, most B2B and consumer apps never outgrow the standard user flows.

It is also worth comparing this against the alternative of hosting on Azure Static Web Apps instead of GitHub Pages, since ASWA has authentication built into the platform itself with zero MSAL configuration required. If you are not tied to GitHub Pages for a specific reason, ASWA’s built in auth is genuinely less code to maintain. B2C earns its keep when you need a hosting platform that does not offer built in identity, or when you specifically need the sign up flow and custom branding that ASWA’s simpler provider based login does not give you.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading