When you buy this book you support this site! - Thank You for your support!

Dec 22

Written by: Michael Washington
12/22/2019 9:39 AM  RssIcon

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.

 

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

image

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

Right-click on the Client project and select Manage NuGet Packages…

image

Install:

Microsoft.AspNetCore.Components.Authorization

 

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

See this video: Blazor in more depth - Steve Sanderson & Ryan Nowak for a detailed explanation of exactly how this works.

 

image

In the Client project, open the Startup.cs file and add the following using statement:

 

using Microsoft.AspNetCore.Components.Authorization;

 

Add the following to the ConfigureServices method:

 

    // Must call AddAuthorizationCore for Blazor Client auth 
    services.AddAuthorizationCore();
    // First register our CustomAuthenticationProvider
    services.AddScoped<CustomAuthenticationProvider>();
    // Use our CustomAuthenticationProvider as the 
    // AuthenticationStateProvider
    services.AddScoped<AuthenticationStateProvider>(
        provider => provider
        .GetRequiredService<CustomAuthenticationProvider>());

 

 

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:

 

<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

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.

 

Links

Blazor.net

ASP.NET Core Blazor authentication and authorization

Implement a custom AuthenticationStateProvider

Blazor in more depth - Steve Sanderson & Ryan Nowak

Adding Authentication (Chris Sainty)

Managing Authentication Token Expiry In WebAssembly-based Blazor

Blazor Microsoft Graph Calendar Example With Active Directory Authentication

 

 

Download

The project is available at http://Blazorhelpwebsite.com/Downloads.aspx

You must have Visual Studio 2019 (or higher) installed to run the code.

Tags: Blazor
Categories:

7 comment(s) so far...


Gravatar

Re: Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

Great tutorial! Is it possible this may be modified to accommodate Azure B2C?

By Adam on   1/7/2020 7:19 AM
Gravatar

Re: Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

@Adam - Yest it can. I don't have an example, sorry. However, you should be able to follow the steps from any .Net Core Azure B2C tutorial and it should work.

By Michael Washington on   1/7/2020 7:21 AM
Gravatar

Azure AD B2C

@Adam, I used this tutorial for an Azure AD B2C Blazor project with just a few changes.

Basically, in the server project, replace AzureAD with AzureADB2C everywere. Examples:
- Nuget package Microsoft.AspNetCore.Authentication.AzureADB2C.UI (instead of Microsoft.AspNetCore.Authentication.AzureAD.UI)
- using Microsoft.AspNetCore.Authentication.AzureADB2C.UI;
- services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureADB2C", options));
- services.Configure(AzureADB2CDefaults.OpenIdScheme,

In the client project in LoginDisplay.razor also replace AzureAD with AzureADB2C (href="AzureADB2C/Account/SignIn")

A little bit more complicated was the GetUser() method in the UserController, because depending on the configuration of your AD B2C tenant User.Identity.Name can be empty. In that case you must find an alternative to get the user name or use some other claim.

If you need to setup an AD B2C tenant from scratch, you can start with this tutorial: https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant

By Marcel Wolterbeek on   1/8/2020 4:56 AM
Gravatar

Re: Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

This is great thanks! Do you have an example that shows how to do B2C/AD Auth using JWT tokens?

By Andrew on   1/14/2020 5:19 AM
Gravatar

Re: Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

@Andrew - Sorry, I don't have a JWT example. However, others have covered that and did a good job. See Chris Sainty.

By Michael Washington on   1/14/2020 5:20 AM
Gravatar

Re: Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

Hi, I've been searching around and might be missing it but I'm wanting to make a blazor app which only uses client side similar to angular. However I'm having problems finding what the best way to make calls to azure ad authenticated apis. My blazor app is using azure ad to sign and in the app registration for it there is access to the api granted. When I sign in I see that I'm being granted access to both the api and the app however I dont believe my token is being sent to the api and I've used fiddler and am seeing the token is not applied. Any thoughts?

By Scott Blankenship on   1/16/2020 6:13 PM
Gravatar

Re: Client Side Blazor Authentication Using Azure AD and a Custom AuthenticationStateProvider

@Scott Blankenship - For an example of calling an API like Microsoft Graph see: http://blazorhelpwebsite.com/Blog/tabid/61/EntryId/4358/Blazor-Microsoft-Graph-Calendar-Example-With-Active-Directory-Authentication.aspx

By Michael Washington on   1/16/2020 6:14 PM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation