Secure an ASP.NET Core Blazor WebAssembly standalone app with Azure Active Directory B2C

By Javier Calvarro Nelson and Luke Latham

To create a Blazor WebAssembly standalone app that uses Azure Active Directory (AAD) B2C for authentication:

Follow the guidance in the following topics to create a tenant and register a web app in the Azure Portal:

Create an AAD B2C tenant

Record the following information:

  • AAD B2C instance (for example, https://contoso.b2clogin.com/, which includes the trailing slash): The instance is the scheme and host of an Azure B2C app registration, which can be found by opening the Endpoints window from the App registrations page in the Azure portal.
  • AAD B2C Primary/Publisher/Tenant domain (for example, contoso.onmicrosoft.com): The domain is available as the Publisher domain in the Branding blade of the Azure portal for the registered app.

Follow the guidance in Tutorial: Register an application in Azure Active Directory B2C again to register an AAD app for the Client app and then do the following:

  1. In Azure Active Directory > App registrations, select New registration.
  2. Provide a Name for the app (for example, Blazor Standalone AAD B2C).
  3. For Supported account types, select the multi-tenant option: Accounts in any organizational directory or any identity provider. For authenticating users with Azure AD B2C.
  4. Leave the Redirect URI drop down set to Web and provide the following redirect URI: https://localhost:{PORT}/authentication/login-callback. The default port for an app running on Kestrel is 5001. If the app is run on a different Kestrel port, use the app's port. For IIS Express, the randomly generated port for the app can be found in the app's properties in the Debug panel. Since the app doesn't exist at this point and the IIS Express port isn't known, return to this step after the app is created and update the redirect URI. A remark appears later in this topic to remind IIS Express users to update the redirect URI.
  5. Confirm that Permissions > Grant admin consent to openid and offline_access permissions is enabled.
  6. Select Register.

Record the Application (client) ID (for example, 41451fa7-82d9-4673-8fa5-69eff5a761fd).

In Authentication > Platform configurations > Web:

  1. Confirm the Redirect URI of https://localhost:{PORT}/authentication/login-callback is present.
  2. For Implicit grant, select the check boxes for Access tokens and ID tokens.
  3. The remaining defaults for the app are acceptable for this experience.
  4. Select the Save button.

In Home > Azure AD B2C > User flows:

Create a sign-up and sign-in user flow

At a minimum, select the Application claims > Display Name user attribute to populate the context.User.Identity.Name in the LoginDisplay component (Shared/LoginDisplay.razor).

Record the sign-up and sign-in user flow name created for the app (for example, B2C_1_signupsignin).

In an empty folder, replace the placeholders in the following command with the information recorded earlier and execute the command in a command shell:

dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C INSTANCE}" --client-id "{CLIENT ID}" --domain "{TENANT DOMAIN}" -o {APP NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"
Placeholder Azure portal name Example
{AAD B2C INSTANCE} Instance https://contoso.b2clogin.com/
{APP NAME} BlazorSample
{CLIENT ID} Application (client) ID 41451fa7-82d9-4673-8fa5-69eff5a761fd
{SIGN UP OR SIGN IN POLICY} Sign-up/sign-in user flow B2C_1_signupsignin1
{TENANT DOMAIN} Primary/Publisher/Tenant domain contoso.onmicrosoft.com

The output location specified with the -o|--output option creates a project folder if it doesn't exist and becomes part of the app's name.

Note

In the Azure portal, the app's Authentication > Platform configurations > Web > Redirect URI is configured for port 5001 for apps that run on the Kestrel server with default settings.

If the app is run on a random IIS Express port, the port for the app can be found in the app's properties in the Debug panel.

If the port wasn't configured earlier with the app's known port, return to the app's registration in the Azure portal and update the redirect URI with the correct port.

After creating the app, you should be able to:

Authentication package

When an app is created to use an Individual B2C Account (IndividualB2C), the app automatically receives a package reference for the Microsoft Authentication Library (Microsoft.Authentication.WebAssembly.Msal). The package provides a set of primitives that help the app authenticate users and obtain tokens to call protected APIs.

If adding authentication to an app, manually add the package to the app's project file:

<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" 
  Version="3.2.0" />

The Microsoft.Authentication.WebAssembly.Msal package transitively adds the Microsoft.AspNetCore.Components.WebAssembly.Authentication package to the app.

Authentication service support

Support for authenticating users is registered in the service container with the AddMsalAuthentication extension method provided by the Microsoft.Authentication.WebAssembly.Msal package. This method sets up all of the services required for the app to interact with the Identity Provider (IP).

Program.cs:

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
});

The AddMsalAuthentication method accepts a callback to configure the parameters required to authenticate an app. The values required for configuring the app can be obtained from the AAD configuration when you register the app.

Configuration is supplied by the wwwroot/appsettings.json file:

{
  "AzureAdB2C": {
    "Authority": "{AAD B2C INSTANCE}{DOMAIN}/{SIGN UP OR SIGN IN POLICY}",
    "ClientId": "{CLIENT ID}",
    "ValidateAuthority": false
  }
}

Example:

{
  "AzureAdB2C": {
    "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_signupsignin1",
    "ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
    "ValidateAuthority": false
  }
}

Access token scopes

The Blazor WebAssembly template doesn't automatically configure the app to request an access token for a secure API. To provision an access token as part of the sign-in flow, add the scope to the default access token scopes of the MsalProviderOptions:

builder.Services.AddMsalAuthentication(options =>
{
    ...
    options.ProviderOptions.DefaultAccessTokenScopes.Add("{SCOPE URI}");
});

Note

If the Azure portal provides the scope URI for the app and the app throws an unhandled exception when it receives a 401 Unauthorized response from the API, try using a scope URI that doesn't include the scheme and host. For example, the Azure portal may provide one of the following scope URI formats:

  • https://{TENANT}.onmicrosoft.com/{API CLIENT ID OR CUSTOM VALUE}/{SCOPE NAME}
  • api://{SERVER API CLIENT ID OR CUSTOM VALUE}/{SCOPE NAME}

Try supplying the scope URI without the scheme and host:

options.ProviderOptions.DefaultAccessTokenScopes.Add(
    "{SERVER API CLIENT ID OR CUSTOM VALUE}/{SCOPE NAME}");

For example:

options.ProviderOptions.DefaultAccessTokenScopes.Add(
    "41451fa7-82d9-4673-8fa5-69eff5a761fd/API.Access");

For more information, see the following sections of the Additional scenarios article:

Imports file

The Microsoft.AspNetCore.Components.Authorization namespace is made available throughout the app via the _Imports.razor file:

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using {APPLICATION ASSEMBLY}
@using {APPLICATION ASSEMBLY}.Shared

Index page

The Index page (wwwroot/index.html) page includes a script that defines the AuthenticationService in JavaScript. AuthenticationService handles the low-level details of the the OIDC protocol. The app internally calls methods defined in the script to perform the authentication operations.

<script src="_content/Microsoft.Authentication.WebAssembly.Msal/
    AuthenticationService.js"></script>

App component

The App component (App.razor) is similar to the App component found in Blazor Server apps:

  • The CascadingAuthenticationState component manages exposing the AuthenticationState to the rest of the app.
  • The AuthorizeRouteView component makes sure that the current user is authorized to access a given page or otherwise renders the RedirectToLogin component.
  • The RedirectToLogin component manages redirecting unauthorized users to the login page.
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" 
                DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        <RedirectToLogin />
                    }
                    else
                    {
                        <p>
                            You are not authorized to access 
                            this resource.
                        </p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin component

The RedirectToLogin component (Shared/RedirectToLogin.razor):

  • Manages redirecting unauthorized users to the login page.
  • Preserves the current URL that the user is attempting to access so that they can be returned to that page if authentication is successful.
@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl=" +
            Uri.EscapeDataString(Navigation.Uri));
    }
}

LoginDisplay component

The LoginDisplay component (Shared/LoginDisplay.razor) is rendered in the MainLayout component (Shared/MainLayout.razor) and manages the following behaviors:

  • For authenticated users:
    • Displays the current username.
    • Offers a button to log out of the app.
  • For anonymous users, offers the option to log in.
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
    <Authorized>
        Hello, @context.User.Identity.Name!
        <button class="nav-link btn btn-link" @onclick="BeginLogout">
            Log out
        </button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code {
    private async Task BeginLogout(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

Authentication component

The page produced by the Authentication component (Pages/Authentication.razor) defines the routes required for handling different authentication stages.

The RemoteAuthenticatorView component:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
    [Parameter]
    public string Action { get; set; }
}

Custom user flows

The Microsoft Authentication Library (Microsoft.Authentication.WebAssembly.Msal, NuGet package) doesn't support AAD B2C user flows by default. Create custom user flows in developer code.

For more information on how to build a challenge for a custom user flow, see User flows in Azure Active Directory B2C.

Troubleshoot

Cookies and site data

Cookies and site data can persist across app updates and interfere with testing and troubleshooting. Clear the following when making app code changes, user account changes with the provider, or provider app configuration changes:

  • User sign-in cookies
  • App cookies
  • Cached and stored site data

One approach to prevent lingering cookies and site data from interfering with testing and troubleshooting is to:

  • Configure a browser
    • Use a browser for testing that you can configure to delete all cookie and site data each time the browser is closed.
    • Make sure that the browser is closed manually or by the IDE between any change to the app, test user, or provider configuration.
  • Use a custom command to open a browser in incognito or private mode in Visual Studio:
    • Open Browse With dialog box from Visual Studio's Run button.
    • Select the Add button.
    • Provide the path to your browser in the Program field. The following executable paths are typical installation locations for Windows 10. If your browser is installed in a different location or you aren't using Windows 10, provide the path to the browser's executable.
      • Microsoft Edge: C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
      • Google Chrome: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
      • Mozilla Firefox: C:\Program Files\Mozilla Firefox\firefox.exe
    • In the Arguments field, provide the command-line option that the browser uses to open in incognito or private mode. Some browsers require the URL of the app.
      • Microsoft Edge: -inprivate
      • Google Chrome: --incognito --new-window https://localhost:5001
      • Mozilla Firefox: -private -url https://localhost:5001
    • Provide a name in the Friendly name field. For example, Firefox Auth Testing.
    • Select the OK button.
    • To avoid having to select the browser profile for each iteration of testing with an app, set the profile as the default with the Set as Default button.
    • Make sure that the browser is closed by the IDE between any change to the app, test user, or provider configuration.

Run the Server app

When testing and troubleshooting a hosted Blazor app, make sure that you're running the app from the Server project. For example in Visual Studio, confirm that the Server project is highlighted in Solution Explorer before you start the app with any of the following approaches:

  • Select the Run button.
  • Use Debug > Start Debugging from the menu.
  • Press F5.

Inspect the content of a JSON Web Token (JWT)

To decode a JSON Web Token (JWT), use Microsoft's jwt.ms tool. Values in the UI never leave your browser.

Additional resources