In this article, we are going to learn how to use Azure Active Directory to secure our Blazor WebAssembly hosted application. In the previous article, we’ve learned how to do the same thing with a Blazor WebAssembly standalone app, but this time, we have a server-side application too, which we need to register on Azure AD as well. So, we are going to split the Azure AD registration process into two steps. After that, we are going to create a new application and discover all the things we have to accomplish to secure Blazor WebAssembly hosted app with the Azure Active Directory.

To download the source code for this article, you can visit our Blazor WebAssembly Hosted with Azure AD repository.

Let’s get going.

Server App Registration in Azure Active Directory

Before we start, we will assume you have an Azure account, which you can use for Azure Active Directory registration.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

After we login to our Azure portal, we are going to choose the Azure Active Directory service:

Azure Active Directory source

Under the Manage section, we are going to click the App Registrations link, and then the New registration button:

Secure Blazor WebAssembly Hosted app - App registration in Azure Active Directory

Then, we have to provide the required information for our server app:

App registration to Secure Blazor WebAssembly Hosted app

So, as you can see, we have to provide a name for the server app and decide who can access this application. Also, since this is a server app, we don’t require a redirect URI, so we are going to leave it as-is. 

After clicking the Register button, we will see the Overview page with our app’s information:

Blazor server app registration details

We have to remember the Application (client) ID and the Directory (tenant) ID values because we are going to use them soon.

Also, under the Manage section, we can click the Branding link and save the Publisher domain value (it looks like: {CUSTOM_NAME}.onmicrosoft.com). 

Since our server app doesn’t require sign-in or user profile access, we can navigate to the API permissions menu, under the Manage section, and remove the User.Read permission:

Remove permission for Blazor WebAssembly Hosted Server App registration

Excellent.

Now, we have to expose our API.

To do that, we are going to click the Expose an API link, under the Manage section, and then the Add a scope button:

adding a scope for an api to secure blazor webassembly hosted app

In the provided screen with the Application ID URI field, we can just click the Save and continue button.

Then, we have to populate the required data and click the Add scope button:

Populating required fileds for the API scope to secure Blazor WebAssembly hosted app

Also, the State option must be Enabled, as it should be by default.

After we create a scope, we are going to see it in the list:

App ID Uri with Scope Name

We can copy and save this value since it contains both the App ID URI and the Scope name.

Client App Registration in Azure Active Directory

In our previous article, we’ve registered the client standalone application with Azure AD and we are going to follow the same steps here.

So, let’s navigate one more time to Azure Active Directory, click on the App registrations link, and click the New registration button:

Secure Blazor WebAssembly Hosted Client app - Azure registration

With this registration process, we have to select a single-page application option and provide a redirect URI. We will force our app, once we create one, to run on port 5001.

In the Overview screen, we only have to record the Application (client) ID value. The Directory (tenant) ID value is the same and we already have it.

Now, to confirm the client app registration, we can navigate to the Authentication page:

Azure AD information confirmation

Here, we can confirm our redirect URI is valid and also we can see that we are going to use Authorization Code Flow with PKCE, which is a recommended flow for a SPA. If you want to use the Implicit or the Hybrid flow, you can check any of the two checkboxes (Access tokens and ID tokens) provided a bit below on the Authentication page. Since we are using the Authorization Code flow, we are not going to check any of these options.

If you remember the last step of the server app registration, we have created an access scope for our API. Now, we can use it for the client app registration.

To do that, let’s navigate to the API permissions page and click the Add a permission button:

We can also see the User.Read permission, which we are going to keep for the client app.

After clicking the Add a permission button, we can navigate to the My APIs menu and select the server app:

Requesting API permissions

After clicking the application’s name, we can see our BlazorHostedAPI.Access permission. All we have to do is to check it and click the Add permissions button:Selecting API permission for the client app

Now, we can see both permissions (API and Microsoft Graph) but our new API permission doesn’t have an admin consent granted. So, we have to grant it by clicking the Grant button:

Granting admin consent for the permissions

That is it.

We can continue with the app creation.

Creating a New Blazor WebAssembly Hosted App

We can create our template app in two ways. By using the dotnet new command or by using a Visual Studio template project. We will show you how to use the dotnet new command but to show you the setup step by step process, we are going to use the Visual Studio template.

So, if we want to use the command, we have to provide our previously saved information:

dotnet new blazorwasm -au SingleOrg --api-client-id "{SERVER API APP CLIENT ID}" --app-id-uri "{SERVER API APP ID URI}" --client-id "{CLIENT APP CLIENT ID}" --default-scope "{DEFAULT SCOPE}" --domain "{TENANT DOMAIN}" -ho -o {APP NAME} --tenant-id "{TENANT ID}"

In our case:

  • SERVER API APP CLIENT ID is the Application ID of the server app => 862c81f0-91e8-476e-8646-b7c297967ec9
  • SERVER API APP ID URI is the Application ID URI of the server app => 862c81f0-91e8-476e-8646-b7c297967ec9 (you have to use it without the api:// part)
  • CLIENT APP CLIENT ID is the Application ID of the client app => f3fea9a1-5ccd-4ff0-a446-2be905a9f67a
  • DEFAULT SCOPE is the scope we’ve created in API permissions => BlazorHostedAPI.Access
  • TENANT DOMAIN is the primary domain => MarinkoBlogCodeMaze.onmicrosoft.com
  • APP NAME is the name of our application => BlazorWasmHostedAAD
  • TENANT ID is the Directory (tenant) ID value => acd9a136-2bfa-485e-934b-4fb1c6fea421
dotnet new blazorwasm -au SingleOrg --api-client-id "862c81f0-91e8-476e-8646-b7c297967ec9" --app-id-uri "862c81f0-91e8-476e-8646-b7c297967ec9" --client-id "f3fea9a1-5ccd-4ff0-a446-2be905a9f67a" --default-scope "BlazorHostedAPI.Access" --domain "MarinkoBlogCodeMaze.onmicrosoft.com" -ho -o BlazorWasmHostedAAD --tenant-id "acd9a136-2bfa-485e-934b-4fb1c6fea421"

After we press the Enter key, this will create a new application for us with all the required configurations.

You have to pay attention that even though we removed the api:// part from the APP ID URI value, it will be present inside the Program.cs file of the client project:

options.ProviderOptions.DefaultAccessTokenScopes.Add("api://862c81f0-91e8-476e-8646-b7c297967ec9/BlazorHostedAPI.Access");

The command adds it by default.

If we want to use Visual Studio for the project creation, we have to create a new Blazor WebAssembly Hosted app with the Microsoft identity platform authentication type:

Secure Blazor WebAssembly Hosted Application with Azure AD from Visual Studio Template

This is going to create a template for us with the placeholders for the required information.

Firstly, we are going to modify the server’s launchsettings.json file to force the app to always start on port 5001:

{
    "profiles": {
        "BlazorWebAssemblyHostedAAD.Server": {
            "commandName": "Project",
            "dotnetRunMessages": "true",
            "launchBrowser": true,
            "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
            "applicationUrl": "https://localhost:5001;http://localhost:5000",
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            }
        }
    }
}

Then, let’s modify the server’s appsettings.json file:

{
    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "MarinkoBlogCodeMaze.onmicrosoft.com",
        "TenantId": "acd9a136-2bfa-485e-934b-4fb1c6fea421",
        "ClientId": "862c81f0-91e8-476e-8646-b7c297967ec9",
        "CallbackPath": "/signin-oidc"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*"
}

Here, we just provide the required information for the server application id, tenant id, and domain. The Instance and the CallbackPath properties are the same.

Additionally, we have to modify the client’s appsetting.json file:

{
    "AzureAd": {
        "Authority": "https://login.microsoftonline.com/acd9a136-2bfa-485e-934b-4fb1c6fea421",
        "ClientId": "f3fea9a1-5ccd-4ff0-a446-2be905a9f67a",
        "ValidateAuthority": true
    }
}

Here, we add the combination of the instance and the tenant id values for the Authority property. Also, for the ClientId property, we use the client’s application id value.

Now, the last thing we have to do is to open the Program.cs file of the client project, and modify the default access token scope:

builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    options.ProviderOptions.DefaultAccessTokenScopes.Add("api://862c81f0-91e8-476e-8646-b7c297967ec9/BlazorHostedAPI.Access");
});

With the AddMsalAuthentication method, we add the authentication to our Blazor app. It also accepts the callback where we provide our configuration from the appsettings.json file and our default access token scopes.

Testing the Application

Let’s start our application, and as soon as it starts, we can see the Login button in the header of the app.

After we click on it, a new popup window will appear where we have to add our credentials. We can use our existing Azure credentials.

As soon as we log in successfully, we are going to see our username inside the “Hello” message:

Successful Azure Active Directory Authentication with Blazor WASM Standalone app

But as soon as we try to visit the FetchData page, we are going to get a 403 (Forbidden) error.

To fix this, we have to navigate to the WeatherForecastController class and modify the scopeRequiredByApi variable:

static readonly string[] scopeRequiredByApi = new string[] { "BlazorHostedAPI.Access" };

The template places the default placeholder value here, but we have to replace that value with the name of our scope.

With this in place, we can log in again and navigate to the FetchData page, and we will be able to see the data.

Projects Overview

Let’s start with the server-side project.

We’ve already seen the appsettings.json file but we have to inspect the Startup.cs file to see where do we use all the information from the appsettings file:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));

    services.AddControllersWithViews();
    services.AddRazorPages();
}

With the AddAuthentication method, we register the authentication services in our app. Also, we use the AddMicrosoftIdentityWebApi method, where we pass our configuration from the appsettings file, to protect our API with Microsoft Identity Platform version 2.0.

Of course, to be able to use this code, we have to install the required libraries:

Secure Blazor WebAssembly Hosted server app with required packages

Also, if we inspect the Configure method:

app.UseAuthentication();
app.UseAuthorization();

We can see two methods responsible for adding the authentication and authorization to the application’s pipeline.

The last important thing for the server part of our application is the WeatherForecastController.cs file:

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

In this action, we use the VerifyUserHasAnyAcceptedScope method to verify that the right claim has been sent from the client app and that it has a value that contains the scope expected by the API.

The Client App Overview

So, we’ve seen the appsettings.json file where we add the required configuration info and the Program.cs file where we read that configuration using the AddMsalAuthentication method. But to make this work, our template installs the required package for us:

Secure Blazor WebAssembly Hosted client app with required package

If we inspect the index.html file, we are going to see the reference to this package’s js file:

<!DOCTYPE html>
<html>

<head>
    ...
</head>

<body>
    <div id="app">Loading...</div>

    ...
    <script src="_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
</body>

</html>

This script is important for handling authentication actions that we perform in our app.

It is also important to notice the @using Microsoft.AspNetCore.Components.Authorization using directive inside the Imports.razor file. This way the authorization is enabled throughout the entire application.

Next, we can open the App.razor file:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <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>

There are different components here like CascadingAuthenticationState, AuthorizeRouteView, NotAuthorized. You can read our AuthenticationStateProvider article to learn more about how these components work and why do we wrap our code inside the CascadingAuthenticationState component. Long story short, with the CascadingAuthenticationState component, we expose the authentication state to the rest of the app. Additionally, we use the AuthorizeRouteView component to determine whether the user is authorized or not. If they are not, we use the RedirectToLogin component to redirect the user to the login page.

RedirectToLogin and Authentication Components

If we inspect the RedirectToLogin component:

@inject NavigationManager Navigation
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@code {
    protected override void OnInitialized()
    {
        Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
    }
}

We can see it uses the NavigationManager service to navigate the user to the Authentication component with the login action as a parameter and additional query string for returnUrl.

So, if we open the Pages folder, we are going to find the Authentication component inside:

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action" />

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

It only calls the RemoteAuthenticatorView component, which includes a default set of UI pieces for each authentication state. If we want, we can modify all these default UI components.

Log in and Log out Links

If we open the LoginDisplay.razor file, we are going to see the code responsible for showing the Log in and Log out links:

@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");
    }
}

If the user is authenticated, we are going to see the “Hello” message with the user’s username, and the Log out button. Otherwise, we are going to see the Log in button.

The FetchData Component

This component is secured against unauthorized users by using the Authorize attribute:

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@attribute [Authorize]

And that’s all it takes to protect this page from unauthenticated users.

Conclusion

So, that’s all the actions we need to secure our Blazor WebAssembly hosted app by using Azure Active Directory (AAD). With simple app registrations and using template projects, we can create well-secured applications. Additionally, we can use these steps to secure an existing Blazor WebAssembly hosted app without using the template project.

Until the next article,

All the best.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!