Secure ASP.NET Blazor WASM apps and APIs with Azure AD B2C
Tags: azure, webassembly, security, dotnet
Published: 2020-08-12 20:40:05

Today's blog post is a bit on the cutting edge of .NET Core and Identity. We will be creating a secure Blazor Client (WASM) web app that authenticates users against Azure AD B2C using local accounts and Google and then communicate securely with a .NET Core API to pull some data. There are a few moving components here:

- ASP.NET Core Blazor
- ASP.NET Core API
- Azure AD B2C

Unlike traditional ASP.NET Core web apps, Blazor WASM has some idiosyncrasies when it comes to authentication and token acquisition which we will highlight as we build the solution

This blog post comes at the back of our Tuesday Twitch stream were we attempted to build this live with our special guest Jon Gallant from the Azure SDKs team. You can watch the video on our [Twich channel](https://twitch.tv/425show) 

## Prerequisites
There are a couple of prerequisites if you want to follow along

- Latest [.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core/5.0) (5.0 or later)
- Your favorite .NET Core IDE
- The latest [Blazor templates](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Templates/). You can install the latest Blazor templates for the .NET CLI with the following command:

`dotnet new --install Microsoft.AspNetCore.Components.WebAssembly.Templates::3.2.1`

## Create the App Registration for the Client app (web app)
Log in to the Azure, go to your AD B2C portal and create a new App Registration. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wo4p2ji9yloqs1kjp7f6.png)

Next, we need to create a Sign in + Sign out Policy or flow in B2C. This is what creates the endpoint for our users to log in and also defines the behavior and data we collect and return during the sign in/signup process. The nice thing is that B2C does most of this automatically for us so we don't have to worry about the underlying details. At a later time, you can revisit your policies and customize them to better match your needs as well as the look and feel of your app. For now, we'll go with the defaults. At the root of your B2C tenant portal, select **User Flows** and then click on the **New user flow** button

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qokpxlem9g6zpx1l55wa.png)

Select the **Sign up and sign in** user flow and go with the **Recommended** version

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7e1ommyt4lvrynuz456b.png)

Select an appropriate **Name**(no spaces!!), add an external provider (optional), select the **Display Name** (minimum) attribute for the **Return Claim**, press **OK** and **Create**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7vi2djk3etnyfz7f45pz.png)

The **Display Name** in the return ID token will be used to populate the Name property in the `Context.User.Identity` object in our web app. The other properties are optional so you can choose what to pull in your token beyond the Display Name

Go back to the app registration we just created as we will need the details for creating our Blazor WASM app on the next step

## Create the Blazor Web App
We will use the .NET CLI to create the Blazor WASM app because it's easier when it comes to configuring authentication. Even better, the latest .NET Auth library (Microsoft.Identity.Web) has a built-in template for WASM + AAD/B2C. You can install it using the following command:

`dotnet new -i Microsoft.Identity.Web.ProjectTemplates::1.9.1
`

Open your favorite CMD and type the following - make sure you add your own application name:

`dotnet new blazorwasm2 -au IndividualB2C - -o {APP NAME}`

In my case this was:

`dotnet new blazorwasm -au IndividualB2C -o BlazorWeb`

Press Enter and watch the magic happen:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4e5rh5iafotaipsha3fu.png)

At this point, we need to go back to our App registration on the B2C portal to update the Redirect URI for our Blazor app. In the **Authentication** tab, click the **Add a platform** button, select Single Page App and configure the Redirect URIs with 

- `http://localhost:5000/authentication/login-callback`
- `https://localhost:5001/authentication/login-callback`

The above URIs are the ones that our Blazor app will be listening for the auth result. If you plan to use a different port with Kestrel, make sure to update the port numbers here as well. Make sure to press **Save** in the end.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/joop4gauq4n7ucb2qt7g.png)
We can now run our Blazor app and test that our signup and login works as expected. Since we are using local accounts, we'll have to sign up before being able to log in. Subsequent logins are much simpler. And if you have social media accounts configured in the your user flow, then these will appear as an option here as well

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4v09qq90i598gopj6mi0.png)

## Let's look at the code!
Because we used the template, we didn't have to write any code. However, this is not always the case and there are scenarios that we may want to add authentication to an existing application. So, let's look at the components necessary for authentication:

First, we need to add the [MSAL library](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-overview). This is a NuGet package specially designed for Blazor WASM apps, unlike the new [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web) library that work's with Blazor Server. Open your *.csproj and add the following reference:

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

Next, open the `Program.cs` file and add the following code:

```
builder.Services.AddMsalAuthentication(options =>
{
    builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
});
```
Replace the code in `App.razor` with the following:

```
<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>
```

In the `_Imports.razor`, add the appropriate import as per below:
`@using Microsoft.AspNetCore.Components.Authorization`

Inside `index.html`, add a new script before the end of the `<body>` tag

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

In the `appsettings.json` add a section for the Azure AD B2C settints

```
{
  "AzureAdB2C": {
    "Authority": "https://cmatdevb2c.b2clogin.com/cmatdevb2c.onmicrosoft.com/B2C_1_sisu_simple",
    "ClientId": "<your client id>",
    "ValidateAuthority": false
  }
}
```
In the `Shared` folder, we need to add a new page `RedirectToLogin.razor` with the following code

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

```
Next, we need to update the `MainLayout.razor` with a login prompt as well as a greeting for our authenticated users. For that, we will use a `LoginDisplay` component. So for now, add the `<LoginDisplay />` where it makes sense for your app. I put it at the top of the main content

```
@inherits LayoutComponentBase

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="top-row px-4 auth">
        <LoginDisplay />
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <div class="content px-4">
        @Body
    </div>
</div>
```

As mentioned in the previews step, we need to create a `LoginDisplay.razor` component in the `Shared` folder. The code for this component is:

```
@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");
    }
}
```
Finally, in the `Pages` folder, add a new `Authentication.razor` page with the following code:

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

@code{
    [Parameter] public string Action { get; set; }
}
```
Following the steps in the section, you can implement authentication in your Blazor WASM app. This is great but there is a good chance that your app will be using one or more APIs to pull some data in. So let's see what we need to change in the code to make this work. But first, we need an API that works with B2C as well :)

## Create the API App registration
To allow our API to validate tokens against B2C we need an app registration. In the B2C portal, create a new App Registration. You don't need a Redirect URI so provide a name and press Create. Next, open the **Expose and API** tab and Add a scope. Use `acess_as_user` for the scope name

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yht37kkcstdcarhzkara.png)

That's all we need from our API. With this change however, we also need to update the client app registration to allow it to request the appropriate scopes when users login. Open the client app registration and go to the **API Permissions** tab. Click the **Add a permission** button and select **My APIs**. Find the API you just created, click on it select the `access_as_user` permission. Click the **Add permissions** button

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2iqp8pwhu2syawy19ho4.png)

Our B2C is now to rock 'n roll some tokens! Back to our code.

## Create the .NET Core API
For the sake of simplicity, I'm going to create a simple API using the default template -> yes, the one that serves random weather data :)

`dotnet new webapi2 -o <yourAPIName> --auth IndividualB2C`

For the authentication/authorization bit, we're using the new Microsoft.Identity.Web library that simplifies significantly the whole process.

Open the `*.csproj` file and add the following package reference:

`<PackageReference Include="Microsoft.Identity.Web" Version="1.*" />`

For authentication to work, we need to tell Microsoft.Identity.Web which tenant and app registration to use. In the `appsettings.json` of our API, we need to add or update the following information

```
"AzureAdB2C": {
    "Instance": "https://cmatdevb2c.b2clogin.com/",
    "ClientId": "80978f9b-a2f9-44bf-8ae7-3c5099ff12b2",
    "Domain": "cmatdevb2c.onmicrosoft.com",
    "SignUpSignInPolicyId": "b2c_1_susi"
}
```

We can now wire up the authentication middleware in `Startup.cs`. Update the `ConfigureServices()` method with the following code:

```
private readonly string MyAllowSpecificOrigins = "localHostAccess";

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

    services.AddControllers();
    
    //DO NOT FORGET CORS
    services.AddCors(options =>
    {
        options.AddPolicy(name: MyAllowSpecificOrigins,
           builder =>
           {
               builder
                   .AllowAnyOrigin()
                   .AllowAnyMethod()
                   .AllowAnyHeader();
           });
    });
}
```
We also need to update the `Configure()` method in `Startup.cs` to wire up the authentication, authorization and CORS. Add the following 3 lines right after the `app.UseRouting();` line:

```
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthentication();
app.UseAuthorization();
```

> Note: if you don't define a CORS policy, your solution is going to be dead in the water. You need to explicitly say what Origin URLs and which Headers to allow in your app or otherwise you'll be met with the dreaded CORS error even if everything else is wired correctly.

The final bit in our API is to configure the controller(s) with the appropriate Authorize attribute to ensure that only authenticated requests can access the various actions. In the `WeatherForecastController.cs` class, decorate the `Get()` method with the `[RequiredScope(new [] { "access_as_user" })]` attribute. Here, we want to ensure that any incoming calls contain an access token with the correct scope!

## Configure the Blazor app to retrieve data securely from the API

In Blazor WASM apps, there is a way to configure the code so that outgoing HTTP requests are wired with the appropriate authorization headers automatically. There is a [whole section in our docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-5.0) that explains how and why things work in the way they do, so feel free to read about it. If, however, all you care about is the code changes necessary to make this happen, skip the doc and read on...

First we need to add a NuGet package. Open your `*.csproj` and add the following package reference:

`<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" />`

Because the API that we're using is not residing in the same Base URL as our Blazor app (simulating a real scenario), we need to do some changes In our Blazor WASM app. First we need to add a new class `CustomAuthorizationMessageHandler.cs`, as per the docs, to rewire the automatic HTTP Message handler with our custom settings

```
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
    private static string scope = @"https://cmatdevb2c.onmicrosoft.com/80978f9b-a2f9-44bf-8ae7-3c5099ff12b2/access_as_user";

    public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "http://localhost:8080" },
            scopes: new[] { scope });
    }
}
```
You can get the value for the `scope` variable from the API Permissions in your Client App Registration:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/b5js2q8k6ez0v3b8qr08.png)

The `authorizedUrls` value needs to match your API URL. So make sure to set the right value here. My API is configured to run on localhost port 8080.

With our custom MessageHandler in place, we can now update the `Program.cs` to make use of it. The update code should look like the following:

```
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorWeb
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var api_scope = @"https://cmatdevb2c.onmicrosoft.com/80978f9b-a2f9-44bf-8ae7-3c5099ff12b2/access_as_user";

            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.AddScoped<CustomAuthorizationMessageHandler>();

            builder.Services.AddHttpClient("WeatherAPI",
                    client => client.BaseAddress = new Uri("http://localhost:8080"))
                .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();

            builder.Services.AddScoped(sp => 
                sp.GetRequiredService<IHttpClientFactory>()
    .           CreateClient("WeatherAPI"));

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

            await builder.Build().RunAsync();
        }
    }
}
```
Make sure to update the `api_scope` value with your own. Finally, we can update the `FetchData.razor` code to grab the data from our API instead of the local sample file. At the very top of the razor page, add these 2 lines:

```
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject IHttpClientFactory ClientFactory
```

Then, we can update the `OnInitializedAsync()` method with the following code:

```
protected override async Task OnInitializedAsync()
    {
        try
        {
            var client = ClientFactory.CreateClient("WeatherAPI");

            forecasts = await client.GetFromJsonAsync<WeatherForecast[]>(@"http://localhost:8080/WeatherForecast");
        }
        catch (AccessTokenNotAvailableException exception)
        {
            exception.Redirect();
        }
        
    }
```
You'll notice that use the injected `ClientFactory` to get access to our own `HttpClient` that makes use of the `CustomAuthorizationMessageHandler`. With this we don't have to worry about add Auth headers etc. It's all done for us via the middleware. Sweet, right?

#I need teh codez
Just need to code? You can grab the working sample from [GitHub](https://github.com/cmatskas/BlazorWASMB2C/tree/NET5)

#What about an Azure AD version?
We got you covered. If you want to see this exact same code working with AAD only, then check out the [repo](https://github.com/cmatskas/BlazorWASMB2C/tree/BlazorAAD)

## Summary
ASP.NET Blazor works great with Azure AD or Azure AD B2C to provide authentication and allow secure access to custom APIs (MS Graph or your own). In this blog post we examined the steps necessary to configure end-to-end authentication and authorization in our Blazor WASM (self-hosted) and .NET Core API apps with Azure AD B2C.

## Get in touch!
Do you have a cool app (any app, any platform) that uses either AAD or AAD B2C? Do you want to show us 

We stream live twice a week at [twitch.tv/425show](aka.ms/425show)! Join us:

8-10am PT Tuesdays
8-10am PT Fridays

Be sure to send your questions to us here, on twitter or email: 425Show@microsoft.com!