Articoli

Autenticazione con Azure AD B2C in Blazor

da

Introduzione

Il tema security è da sempre un argomento delicato e necessario per le nostre applicazioni.

In base ai requisiti che dobbiamo soddisfare può essere utile appoggiarci ad un servizio che possa gestire per noi i flussi di autenticazione e autorizzazione. Se il nostro Cloud provider di riferimento è Azure una possibile soluzione è Azure Active Directory B2C.

In questo articolo andremo a descrivere come configurare la nostra applicazione Blazor WebAssembly per fare in modo che supporti l’autenticazione tramite questo servizio.

Cosa è Azure AD B2C

Sulla documentazione ufficiale Azure Active Directory B2C viene definito in questo modo:

Azure Active Directory B2C provides business-to-customer identity as a service. Your customers use their preferred social, enterprise, or local account identities to get single sign-on access to your applications and APIs.

fonte: https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview

Semplificando, si tratta di un servizio offertoci da Azure che ci permette di gestire i nostri utenti e i flussi di registrazione, autenticazione ed autorizzazione alle nostre applicazioni, usufruendo di un piano gratuito fino ad un numero di 50.000 utenti attivi al mese ( https://azure.microsoft.com/en-us/pricing/details/active-directory/external-identities/#pricing).

La documentazione ufficiale è consultabile all’indirizzo https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview e offre una serie di tutorial utili a comprendere le basi di configurazione del servizio.

Creiamo l’applicazione Blazor

Ai fini dell’articolo creiamo una semplice applicazione Blazor WebAssembly hosted in ASP.NET Core e per il momento tralasciamo le opzioni che ci permettono di aggiungere l’autenticazione in modo da descrivere passo passo le operazioni necessarie.

Utilizzando la CLI di .NET ci basterà lanciare il seguente comando:

dotnet new blazorwasm --ho -n NomeDelMioProgetto

Mentre se preferiamo l’utilizzo di Visual Studio possiamo creare un nuovo progetto di tipo Blazor WebAssembly e configurarlo come mostrato in figura, lasciando per il momento a None la parte di autenticazione che vedremo come aggiungere manualmente nel corso di questo articolo:

Configurazione progetto Blazor WebAssembly ASP.NET Core hosted

Creato il progetto standard possiamo passare sul Portal di Azure per configurare il nostro servizio Azure AD B2C. 

Configuriamo Azure AD B2C

Partendo dal presupposto di avere già creato un Tenant di Azure Active Directory B2C (è possibile seguire il tutorial che troviamo nella documentazione ufficiale a questo indirizzo https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant) dirigiamoci nella pagina di Overview del nostro Tenant e iniziamo a creare le due Applicazioni di cui avremo bisogno, una per il nostro Backend ASP.NET Core e una per il nostro Client Blazor WebAssembly.

Cliccando sulla voce “App Registrations” che troviamo nel menu di navigazione a sinistra e poi sul link “New registration” che troviamo nella pagina delle App possiamo creare entrambe le applicazioni.

Per quanto riguarda l’applicazione relativa al Backend, diamole un nome e lasciamo il resto dei campi immutato cliccando infine sul bottone “Register” per concludere il salvataggio.

Dalla pagina di dettaglio dell’app appena creata avremo bisogno delle seguenti informazioni:

  • Application (client) ID
  • Publisher domain. Questa informazione è reperibile dalla voce di menu “Branding & Authorization” ed è riconoscibile dal formato <nome del tenant>.onmicrosoft.com.

Prima di passare all’applicazione client, definiamo uno scope per la nostra API, cliccando sulla voce “Expose an API” e, dalla pagina che ci si apre, il link “Add a scope”. Dal pannello che si aprirà, cliccando su “Save and continue” possiamo andare a compilare il form che ci appare e salvare le informazioni che abbiamo definito. A questo punto dalla pagina di elenco degli scope possiamo copiarci l’indirizzo dello scope appena creato, evidenziato nella immagine seguente:

Azure ADB2C API Scope

A questo punto ripetiamo il giro per quanto riguarda l’app che useremo per il nostro client.

In questo caso stiamo attenti nel form di creazione a compilare la sezione “Platform type” selezionando “Single Page Application” come valore della select e valorizzando il valore del redirect uri utilizzando il classico formato https://localhost:{porta}/authentication/login-callback, come nell’esempio in figura:

Azure AD B2C App Redirect URI

A questo punto dalla pagina di overview dell’app appena creata, teniamo traccia dell’Application ID come per l’app creata per il backend e clicchiamo sulla voce di menu “API Permissions” che troviamo a sinistra e nella pagina che ci si apre clicchiamo la voce “Add a permission”. Dal pannello che ci si apre selezioniamo la voce “My APIs” per selezionare l’applicazione backend e lo scope creato in precedenza. A questo punto cliccando su “Add permission” verremo riportati alla pagina di API Permissions dove potremo cliccare sulla voce “Grant admin consent” per abilitare l’aggiunta della API fatta in precedenza.

A questo punto va creato uno User flow che definisca, ad esempio, se sia possibile solo autenticarsi oppure se sia abilitata anche la registrazione di nuovi utenti alla piattaforma. Per brevità possiamo seguire il seguente tutorial:

https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-user-flow

Terminate le operazioni descritte ricordiamoci di tenere traccia del nome del flow appena creato.

Integriamo Azure AD B2C nella nostra applicazione

Possiamo finalmente tornare al codice della nostra applicazione.

Partiamo dalla parte di Backend, installando da NuGet il package Microsoft.Identity.Web e modifichiamo il Program.cs aggiungendo queste parti:

builder.Services.Configure<JwtBearerOptions>(
    JwtBearerDefaults.AuthenticationScheme,
    options =>
    {
        // Indica in quale Claim trovare il valore per la proprietà User.Identity.Name
        options.TokenValidationParameters.NameClaimType = "name";
    });

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"));

// ...

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

Nel file appsettings.json possiamo definire la sezione AzureAdB2C come segue:

"AzureAdB2C": {
    "Instance": "https://{TENANT}.b2clogin.com/",
    "ClientId": "{API_APP_CLIENT_ID}",
    "Domain": "{TENANT}.onmicrosoft.com",
    "SignUpSignInPolicyId": "{USER_FLOW_POLICY}"
}

Fatto questo ci basta decorare i controller che vogliamo proteggere con l’attributo [Authorize].

A questo punto, passiamo alla nostra applicazione client Blazor dove andiamo ad installare i package Microsoft.Authentication.WebAssembly.Msal per abilitare l’autenticazione con Azure AD B2C e Microsoft.Extensions.Http per abilitare la creazione dell’HttpClient utilizzando l’IHttpClientFactory.

Aggiunti questi package creiamo un file appsettings.json all’interno della cartella wwwroot e inseriamo questa sezione:

"AzureAdB2C": {
    "Authority": "https://{TENANT}.b2clogin.com/{TENANT}.onmicrosoft.com/{USER_FLOW}",
    "ClientId": "{APPLICATION_ID_CLIENT}",
    "ValidateAuthority": false
  }

E modifichiamo il Program.cs in questo modo:

builder.Services
    .AddHttpClient("MyServerApiClient", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

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

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

    options.ProviderOptions.LoginMode = "redirect"; //apre la pagina di login facendo redirect (di default viene aperta in popup)
});

In questo modo la generazione dell’istanza della classe HttpClient passa attraverso l’IHttpClientFactory e, ad ogni richiesta che ha come indirizzo quello di base dell’applicazione, viene aggiunto l’header di autorizazzione. Inoltre abbiamo aggiunto la configurazione dell’autenticazione con Azure popolandola con le informazioni prese dal file appsettings.json creato in precedenza ed impostando lo scope con l’indirizzo dello scope creato in precedenza nel Portal di Azure.

Nel file index.html includiamo lo script che abilita in javascript il servizio AuthenticationService aggiungendo in un tag script il seguente riferimento:

_content/Microsoft.Authentication.WebAssembly.Msal/AuthenticationService.js

A questo punto possiamo per comodità aggiungere i namespace Microsoft.AspNetCore.Components.Authorization e Microsoft.AspNetCore.Authorization al nostro _Imports.razor e modificare il componente App.razor, racchiudendo il Router all’interno del componente CascadingAuthenticationState ed utilizzando AuthorizeRouteView al posto di RouteView come mostrato di seguito:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        <RedirectToLogin />  @*aggiungeremo in seguito questo componente*@
                    }
                    else
                    {
                        <p role="alert">You are not authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
        <NotFound>
            <PageTitle>Not found</PageTitle>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Fatto questo possiamo decorare le pagine che vogliamo accessibili solo da utenti autenticati con l’attributo [Authorize] ed eventualmente mostrare i contenuti per cui è necessaria autenticazione usando il componente AuthorizeView:

@page "/fetchdata"
@using BlazorAdB2CDemo.Shared
@inject HttpClient Http

@attribute [Authorize]

<AuthorizeView>
    <Authorized>
        @*Contenuto sotto autenticazione*@
    </Authorized>
    <NotAuthorized>
        @*Contenuto senza autenticazione*@
    </NotAuthorized>
</AuthorizeView>

Per concludere dobbiamo creare ancora tre componenti. Nel dettaglio dobbiamo aggiungere una pagina Authentication.razor, un componente LoginDisplay.razor e il componente RedirectToLogin visto nell’App.razor in precedenza.

Partiamo dalla prima, aggiungendo un Razor component nella cartella Pages che si occupa di fare redirect ad una action che gli verrà passata come parameter:

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

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

Il componente RedirectToLogin è più un componente di utility che aggiungiamo nella cartella Shared semplicemente si occuperà di fare redirect a questa pagina passandogli come action “login” utilizzando il NavigationManager:

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

ultimo componente da aggiungere è LoginDisplay, che posizioniamo sempre nella cartella Shared e che si occuperà semplicemente di mostrare il link alla pagina di login in caso l’utente non sia autenticato oppure il nome utente di quest’ultimo e il bottone di Logout in caso contrario:

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

Fatto questo possiamo finalmente avviare la nostra applicazione e verificare che sia possibile autenticarsi con il nostro servizio Azure Active Directory B2C.

Tips sul Deploy

Potrebbe capitare che, una volta deployata la nostra applicazione, ci scontriamo con il seguente errore:

“There was an error trying to log you in.
Error: Cannot read properties of undefined (reading ‘toLowerCase’)”

Cercando su GitHub è possibile imbattersi in questa issue https://github.com/dotnet/aspnetcore/issues/38082 che dimostra che è un problema conosciuto dovuto al trimming del file javascript che definisce l’AuthenticationService.

Per risolverlo è possibile (come indicato proprio nella issue) utilizzare due approcci:

  1. Aggiungere il tag <PublishedTrimmed>false</PublishTrimmed> alla sezione PropertyGroup del csproj dell’applicazione client. Questo tag fa si che tutto l’assembly del progetto Blazor non venga trimmato in fase di pubblicazione.
  2. Aggiungere, sempre al csproj del nostro progetto Blazor un ItemGroup che contiene il seguente contenuto che fa si che venga escluso dal trimming solo l’assembly Microsoft.Authentication.WebAssembly.Msal:
<ItemGroup>
    <TrimmerRootAssembly Include="Microsoft.Authentication.WebAssembly.Msal" />
</ItemGroup>

Conclusioni

In questo articolo abbiamo provato a vedere come è possibile aggiungere l’autenticazione con Azure Active Directory B2C alla nostra applicazione Blazor WebAssembly hosted. I passaggi sono tanti ma in realtà la documentazione di Microsoft descrive bene come approcciarsi e il pricing che questo servizio ci offre, con i primi 50.000 utenti attivi al mese gratuiti, potrebbe risultare vantaggioso a seconda dei nostri requisiti.

Il codice utilizzato come snippet in questo articolo è disponibile su GitHub al seguente repository: https://github.com/albx/BlazorAdB2CDemo

Alla prossima!