1/7/2020 Admin

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

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

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

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

 image

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

Create a new project.

 image

Select Blazor App.

 image

Name the project BlazorClientAD

 image

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

 image

The Solution will be created.

Open the launchSettings.json file.

 image

Note the sslPort.

You will need it in the next section.

Set-Up The Application In Azure

 image

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

Next, select App registrations.

 image

Select New registration.

 image

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

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

 image

Click on the Authentication node and enable ID Tokens.

Configure The Application

 image

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

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

 image

Install:

Microsoft.AspNetCore.Authentication.AzureAD.UI

 image

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

An error has occurred. This application may no longer respond until reloaded. Reload 🗙