Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

In this article, using client (WebAssembly) Blazor, we will demonstrate how to use Authentication Using Azure AD and a Custom AuthenticationStateProvider

image info

You can easily implement authentication for your Client Side Blazor applications using Azure Active Directory.

We do this by Implementing a custom AuthenticationStateProvider.

Walk-Thru

image info

When we navigate to the Fetch data page, without being logged in, it will indicate that you Must be logged in.

To log in, click the Log In link.

image info

You can log in with an Azure Active Directory account that has authorization to log into the Azure registered application.

image info

Once the user is logged in, navigating to the Fetch data page displays the data.

The logged in user’s name is also displayed at the top of the application.

Step 1 : Install Blazor Client Side (Blazor WebAssembly)

See the following directions to install Blazor WebAssembly (Client Side Blazor):

https://docs.microsoft.com/en-us/aspnet/core/blazor/get-started?view=aspnetcore-3.1&tabs=visual-studio

Create The Project

Open Visual Studio 2019 (or higher).

image info

Create a new project.

image info

Select Blazor App.

image info

Name the project BlazorClientAD

image info

Select the ASP.NET 3.1 (or higher) Blazor WebAssembly template and select ASP.NET Core hosted.

image info

The Solution will be created.

Open the launchSettings.json file.

image info

Note the sslPort.

You will need it in the next section.

Set-Up The Application In Azure

image info

You will need a free Azure Account for the following steps.

Log into https://portal.azure.com/ and select the Azure Active Directory node.

image info

Next, select App registrations.

image info

Select New registration.

image info

Create a Registration.

For Redirect URI, select Web, and use the following url (replacing {your sslPort} with the number you copied earlier):

https://localhost:{your sslPort}/signin-oidc

image info

On the Overview node for the application, copy the Application (client) ID.

image info

Click on the Authentication node and enable ID Tokens.

Configure The Application

image info

Return to Visual Studio, and add a appsettings.json file to the Server project using the following code (replacing {Your Application (client ID)} with the ID you copied in the previous step):


{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/common",
    "ClientId": "{Your Application (client) ID}",
    "CallbackPath": "/signin-oidc"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

image info

Right-click on the Server project and select Manage NuGet Packages

image info

Install:

Microsoft.AspNetCore.Authentication.AzureAD.UI

image info

Open the Startup.cs file in the Server project.

Add the following using statements:


using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Authentication.AzureAD.UI;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using System.Threading.Tasks;

Add the following to the top of the class:


        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

Add the following to the end of the ConfigureServices section:


services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
    .AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme,
options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        // Instead of using the default validation (validating against a 
        // single issuer value, as we do in
        // line of business apps), we inject our own multitenant 
        // validation logic
        ValidateIssuer = false,
        // If the app is meant to be accessed by entire organizations, 
        // add your issuer validation logic here.
        // IssuerValidator = 
        // (issuer, securityToken, validationParameters) => {
        //    if (myIssuerValidationLogic(issuer)) return issuer;
        //}
    };
    options.Events = new OpenIdConnectEvents
    {
        OnTicketReceived = context =>
        {
            // This is called on successful authentication
            // This is an opportunity to write to the database 
            // or alter internal roles ect.
            return Task.CompletedTask;
        },
        OnAuthenticationFailed = context =>
        {
            context.Response.Redirect("/Error");
            context.HandleResponse(); // Suppress the exception
            return Task.CompletedTask;
        }
    };
});

Add the following to the Configure section (under the UseRouting() line):


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

image info

In the Client project, open the _Imports.razor file and add the following lines:


@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

Server Side User Security

image info

All security must be enforced in the Server project.

Add a file called BlazorUser.cs using the following code:


using System;
using System.Collections.Generic;
using System.Text;
namespace BlazorClientAD.Shared
{
    public class BlazorUser
    {
        public string UserName { get; set; }
    }
}

image info

Add a file to the Controllers folder in the Server project called UserController.cs using the following code:


using BlazorClientAD.Shared;
using Microsoft.AspNetCore.Mvc;
namespace BlazorClientAD.Server.Controllers
{
    [ApiController]
    public class UserController : Controller
    {
        [HttpGet("api/user/GetUser")]
        public BlazorUser GetUser()
        {
            BlazorUser objBlazorUser = new BlazorUser();
            if (this.User.Identity.IsAuthenticated)
            {
                objBlazorUser.UserName = this.User.Identity.Name;
            }
            else
            {
                objBlazorUser.UserName = ""; // Not logged in
            }
            return objBlazorUser;
        }
    }
}

This code provides a method for the Blazor client side custom authentication provider (that we will create later), to determine if the user is logged in.

image info

In the Controllers folder in the Server project, open the WeatherForecastController.cs file and add the following using statement:


using Microsoft.AspNetCore.Authorization;

Add the following above the public IEnumerable Get() method:

        [Authorize]

This will prevent non-authenticated users from calling this method.

This is where we are implementing the required server side security that cannot be bypassed by a user manipulating client side code in the Blazor WebAssembly project.

Implement A Custom AuthenticationStateProvider

image info

In the Client project, create a Util folder and a file called CustomAuthenticationProvider.cs with the following code:


using BlazorClientAD.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
namespace BlazorClientAD
{
    public class CustomAuthenticationProvider : AuthenticationStateProvider
    {
        private readonly HttpClient _httpClient;
        public CustomAuthenticationProvider(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }
        public override async Task<AuthenticationState>
            GetAuthenticationStateAsync()
        {
            ClaimsPrincipal user;
            // Call the GetUser method to get the status
            // This only sets things like the AuthorizeView
            // and the AuthenticationState CascadingParameter
            var result =
                await _httpClient.GetJsonAsync<BlazorUser>("api/user/GetUser");
            // Was a UserName returned?
            if (result.UserName != "")
            {
                // Create a ClaimsPrincipal for the user
                var identity = new ClaimsIdentity(new[]
                {
                   new Claim(ClaimTypes.Name, result.UserName),
                }, "AzureAdAuth");
                user = new ClaimsPrincipal(identity);
            }
            else
            {
                user = new ClaimsPrincipal(); // Not logged in
            }
            return await Task.FromResult(new AuthenticationState(user));
        }
    }
}

The purpose of this code is to call the GetUser method, created earlier, to determine if the user is logged in.

If the user is logged in, it creates a ClaimsPrincipal with a user, otherwise it creates a ClaimsPrincipal without a user. The Blazor security knows that a ClaimsPrincipal with a user indicates that a user is logged in.

In the next step, this class will be registered as the AuthenticationStateProvider, and the Blazor security will allow us to show and hide elements using the built-in authentication tags and interfaces.

image info

In the Client project, open the Program.cs file, and replace all the code with the following:


using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Components.Authorization;
namespace BlazorClientAD.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // Use our CustomAuthenticationProvider as the 
            // AuthenticationStateProvider
            builder.Services.AddScoped<AuthenticationStateProvider,
                CustomAuthenticationProvider>();
            // Add Authentication support
            builder.Services.AddOptions();
            builder.Services.AddAuthorizationCore();
            builder.RootComponents.Add<App>("app");
            await builder.Build().RunAsync();
        }
    }
}

image info

In the Client project, open the App.razor file and add replace all the code with the following:


<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>

This will implement authentication in the routing.

Create the Login / Logout Buttons

image info

In the Client project, add a LoginDisplay.razor file to the Shared directory using the following code:

<AuthorizeView>
    <Authorized>
        <h6>Hello, @context.User.Identity.Name! </h6>
        <a href="AzureAD/Account/SignOut">[Log out]</a>
    </Authorized>
    <NotAuthorized>
        <a href="AzureAD/Account/SignIn">[Log in]</a>
    </NotAuthorized>
</AuthorizeView>

Notice this directs the user to endpoints that begin with AzureAD/Account.

These endpoints will be serviced by code from the Microsoft.AspNetCore.Authentication.AzureAD.UI NuGet package that was installed earlier.

image info

In the Client project, open the MainLayout.razor file and add replace all the code with the following:

@inherits LayoutComponentBase
<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="top-row px-4">
        <LoginDisplay />
    </div>
    <div class="content px-4">
        @Body
    </div>
</div>
 

This adds the LoginDisplay control to the top of the page in the application.

image info

In the Client project, open the Index.razor file in the Pages directory, and add the following to the top of the file:


@page "/Account/SignOut"
 

When the user logs out of the application, the user will be sent to the Account/SignOut url. This code allows the Index.razor page to also service that destination.

Consume The Blazor Client Side Security

image info

Finally, in the Client project, open the FetchData.razor file, in the Pages directory, and add replace all the code with the following:


@page "/fetchdata"
@using BlazorClientAD.Shared
@inject HttpClient Http
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<AuthorizeView>
    <Authorized>
        @if (forecasts == null)
        {
            <p><em>Loading...</em></p>
        }
        else
        {
            <table class="table">
                <thead>
                    <tr>
                        <th>Date</th>
                        <th>Temp. (C)</th>
                        <th>Temp. (F)</th>
                        <th>Summary</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var forecast in forecasts)
                    {
                        <tr>
                            <td>@forecast.Date.ToShortDateString()</td>
                            <td>@forecast.TemperatureC</td>
                            <td>@forecast.TemperatureF</td>
                            <td>@forecast.Summary</td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    </Authorized>
    <NotAuthorized>
        <p>Must be logged in</p>
    </NotAuthorized>
</AuthorizeView>
@code {
    [CascadingParameter]
    private Task<AuthenticationState>
        authenticationStateTask
    { get; set; }
    private WeatherForecast[] forecasts;
    protected override async Task OnInitializedAsync()
    {
        var authState = await authenticationStateTask;
        var user = authState.User;
        if (user.Identity != null)
        {
            if (user.Identity.IsAuthenticated)
            {
                forecasts =
                    await Http
                    .GetJsonAsync<WeatherForecast[]>("WeatherForecast");
            }
        }
    }
}
 

The AuthorizeView tag and AuthenticationState Cascading Parameter will be triggered by the custom authentication provider created earlier.

Note: This is helpful for the application to determine what should be shown, but, because Blazor Client Side security can be bypassed, security must always be enforced in the server side code.