Posted on

Blazor WebAssembly with JWT Authentication on Web Api Server

Dopo aver configurato l’autenticazione (vedi), il passo logico successivo è far sì che la tua app Blazor parli con il tuo Web API server in modo sicuro.

Riassunto della “Stretta di Mano”

  1. Auth0: Rilascia il “passaporto” (Token) a Blazor.

  2. Blazor: Prende il passaporto e lo mette in una busta (Header HTTP) e lo manda al Server.

  3. Server (JWT): Dice “Controllo il passaporto: è firmato da Auth0 ed è valido. Prego, puoi passare!”

 

Configurazione su Auth0.com

Single Page Applications

Le Regular Web Applications (come Blazor Server) e le Single Page Applications (come Blazor WASM) usano due modi completamente diversi di gestire la sicurezza:

  • Blazor Server (Regular Web App): Il “segreto” risiede sul server. Può gestire Client Secret e scambi di token protetti “dietro le quinte”.

  • Blazor WASM (SPA): Il codice gira nel browser dell’utente. Non può custodire segreti (perché chiunque farebbe F12 e li vedrebbe). Usa il flusso PKCE (senza secret).

Nel nostro caso è fondamentale che la nostra applicazione su Auth0 sia una Single Page Application altrimenti Auth0 non rilascia l’access-token. In definitiva :

  • ID Token: È la tua carta d’identità. Serve a Blazor per sapere chi sei (nome, email). L’audience (aud) è giustamente il ClientId. Non può essere usato per chiamare le API.

  • Access Token: È il tuo “pass” per il server. Deve avere come audience (aud) l’URL della tua API. Se questo manca, il server API vede la tua carta d’identità ma dice: “Non mi interessa chi sei, mi serve il pass per questa risorsa”.

Assicurati che la tua Auth0 app sia una single page appllication.

Scope nell’API di Auth0

Assicurati che lo scope che chiedi nel codice sia registrato nell’API:

  1. Dashboard Auth0 -> APIs -> [Tua API] -> Scheda Permissions.

  2. Verifica che esista esattamente la riga leggi:miecose, altrimenti inseriscila

 

WEB API Server

Il vostro server che espone api non protette (fino ad ora) è probabilmente un un classico progetto ASP.NET Core Web API (basato su Controller). È la struttura più solida e comune per gestire API aziendali.

Per prima cosa, dobbiamo installare il componente che gestisce i token JWT. Apri il terminale in Visual Studio (o il Gestore Pacchetti NuGet) e installa:

Microsoft.AspNetCore.Authentication.JwtBearer

Se il tuo progetto è in .NET 8 e i pacchetti esistenti sono alla versione 8.0.7, installare un pacchetto alla versione 10.0.0 (che appartiene a una versione di .NET non ancora rilasciata o in preview) creerebbe quasi certamente dei conflitti di dipendenze e bug in fase di compilazione.

Mantenere tutto sulla v8.x.x garantisce che tutti i componenti parlino la stessa lingua.

Configurazione in Program.cs

Apri il file Program.cs. Dobbiamo aggiungere il servizio di autenticazione e dire alle API di usare Auth0. Cerca il punto in cui vengono registrati i servizi (prima di var app = builder.Build();) e incolla questo:

using Microsoft.AspNetCore.Authentication.JwtBearer;

#region Auth0
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = $"https://{builder.Configuration["Auth0:Domain"]}/";
options.Audience = builder.Configuration["Auth0:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true
};
});
#endregion

 

Poi, scendi verso il basso dove viene configurata la “pipeline” (dopo var app = builder.Build();) e aggiungi queste due righe esattamente in questo ordine (devono stare tra UseRouting e MapControllers):

app.UseRouting(); // Aggiungilo se non c'è, aiuta a gestire meglio i percorsi

app.UseAuthentication(); // Deve venire PRIMA di UseAuthorization

app.UseAuthorization();

app.MapControllers();

CORS

Dato che il tuo Web Server (API) e la tua App Blazor girano probabilmente su porte o domini diversi, il server deve esplicitamente permettere a Blazor di parlargli.

Nel Program.cs del Server API, assicurati di avere il CORS configurato:

// Nel Server API
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("https://localhost:XXXX") // L'URL della tua Blazor WASM
.AllowAnyHeader()
.AllowAnyMethod();
});
});

// ... dopo builder.Build()
app.UseCors();

Oppure puoi usare una policy “aperta” (se la sicurezza del tuo scenario lo permette):

//TUTTI
builder.Services.AddCors(options =>
{
options.AddPolicy("OpenPolicy", policy =>
{
policy.AllowAnyOrigin() // Accetta chiamate da chiunque (anche dalla Blazor WASM)
.AllowAnyHeader()
.AllowAnyMethod();
});
});

Nota sulla sicurezza: Con l’autenticazione Auth0 che abbiamo appena messo, anche se il CORS è “aperto”, nessuno potrà leggere i dati protetti senza un token valido. Quindi sei comunque al sicuro!

Configura le credenziali in appsettings.json

Apri il file appsettings.json nel progetto API e aggiungi i dati che trovi nella dashboard di Auth0 (sotto la voce API):

{
  "Auth0":
       {
          "Domain": "[IL-TUO-TENANT].auth0.com",
          "Authority": "https://[IL-TUO-TENANT].auth0.com",
          "Audience": "https://api.iltuoservizio.com"
      }
}

Controller di prova

Crea un controller per provare le chiamate che richiedono autorizzazione (es. AuthController.cs)

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
// Questa API è protetta!
[HttpGet("test-privato")]
[Authorize]
public IActionResult GetPrivato()
{
return Ok(new { Messaggio = "Se vedi questo, l'autenticazione funziona!" });
}

// Questa API è pubblica (come le tue attuali)
[HttpGet("test-pubblico")]
public IActionResult GetPubblico()
{
return Ok(new { Messaggio = "Questa è un'API libera." });
}
}

Come verificare se funziona?

Se provi a chiamare l’API direttamente da Swagger dopo queste modifiche, dovresti ricevere un errore 401 Unauthorized. (Le altre continueranno a rispondere 200 OK come sempre.) È il segno che la protezione è attiva!

Modifiche al client Blazor WebAssembly

Dobbiamo istruire Blazor a usare un “MessageHandler” speciale. Questo componente intercetta le chiamate in uscita e aggiunge automaticamente il token Authorization: Bearer <TOKEN>.

Configurazione in Program.cs

Rispetto alla versione precedente dobbiamo forzare il flusso PKCE nella configurazione Auth0, ed aggiungere il client http per le chiamate al nostro server :

using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using TestBlazorWasmArticolo;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// =========================================================
// >>> REGISTRAZIONI DI BASE (Devono venire prima di AddOidcAuthentication) <<<
// =========================================================
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<SignOutSessionStateManager>();
// =========================================================

// =========================================================
// >>> Parte cruciale per l'autenticazione Auth0/OIDC <<<
// =========================================================
builder.Services.AddOidcAuthentication(options =>
{
// Impostazione dell'Audience (CRUCIALE per chiamare le API)
// L'Audience deve essere passato come parametro aggiuntivo OIDC
options.ProviderOptions.AdditionalProviderParameters.Add(
"audience",
builder.Configuration["Auth0:Audience"]!
);

// Modifica o aggiungi questo per assegnare esplicitamente l'Authority
options.ProviderOptions.Authority = builder.Configuration["Auth0:Authority"]!;

// Assegna il ClientId
options.ProviderOptions.ClientId = builder.Configuration["Auth0:ClientId"]!;

// 2. Forza il flusso PKCE
options.ProviderOptions.ResponseType = "code";

// Aggiungi tutti gli scope richiesti, inclusi quelli personalizzati:
options.ProviderOptions.DefaultScopes.Add("openid");
options.ProviderOptions.DefaultScopes.Add("profile");
options.ProviderOptions.DefaultScopes.Add("email");
options.ProviderOptions.DefaultScopes.Add("address");
options.ProviderOptions.DefaultScopes.Add("phone");
options.ProviderOptions.DefaultScopes.Add("leggi:miecose"); // <--- Scope API

// FORZA LA RICHIESTA DI CONSENSO
//options.ProviderOptions.AdditionalProviderParameters.Add("prompt", "consent");
});


// =========================================================

#region API
string urlservice = "https://" + builder.Configuration["TestWebservice:Address"] + "/";
builder.Services.AddScoped<AuthorizationMessageHandler>();

// Registra l'HttpClient usando la factory
builder.Services.AddHttpClient("ApiGateway", client =>
{
client.BaseAddress = new Uri(urlservice);
})
.AddHttpMessageHandler(sp =>
{
// Configuriamo l'handler al volo
return sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { urlservice },
scopes: new[] { "leggi:miecose" }
);
});
#endregion

//Client httpclient per chimatae locali (weather)
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

ResponseType = “code”: Questo dice a Blazor e Auth0 di usare il flusso PKCE, che è lo standard moderno per le Single Page Application e risolve molti problemi di “blocco” durante il ritorno dal redirect.

Creazione della Pagina Protetta

Ora creiamo una nuova pagina Razor, ad esempio FetchDati.razor. Questa pagina dovrà essere accessibile solo agli utenti loggati.

@page "/fetch-dati"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Microsoft.AspNetCore.Components.Authorization
@attribute [Authorize]
@inject IHttpClientFactory ClientFactory
@inject IAccessTokenProvider TokenProvider
@inject NavigationManager Navigation

<PageTitle>Dati Protetti</PageTitle>

<AuthorizeView>
<Authorized>
<h3>Dati dal Server</h3>
@if (messaggioDalServer == null)
{
<p>Caricamento dati in corso...</p>
}
else
{
<div class="alert alert-success">@messaggioDalServer</div>
}
<h3>Test Api Gateway</h3>
<button class="btn btn-success" @onclick="CaricaDatiConToken">Carica Dati con Token</button>
<p>@messaggioDalServer</p>
</Authorized>
<Authorizing>
<p>Controllo autorizzazione...</p>
</Authorizing>
</AuthorizeView>

@code {
private string? messaggioDalServer = "ciao";

protected override async Task OnInitializedAsync()
{
try
{
var client = ClientFactory.CreateClient("ApiGateway");
// Proviamo prima una chiamata semplicissima per vedere se il problema è il Token
var response = await client.GetAsync("api/Auth/test-privato");

if (response.IsSuccessStatusCode)
{
messaggioDalServer = await response.Content.ReadAsStringAsync();
}
else
{
messaggioDalServer = $"Errore Server: {response.StatusCode}";
}
}
catch (AccessTokenNotAvailableException ex)
{
// Se il token fallisce, reindirizza
ex.Redirect();
}
catch (Exception ex)
{
// Se c'è un errore di rete o altro, lo scriviamo ma non blocchiamo l'app
messaggioDalServer = $"Errore tecnico: {ex.Message}";
Console.WriteLine(ex.ToString());
}
}

private async Task CaricaDatiConToken()
{
// 1. Chiediamo il token manualmente
var tokenResult = await TokenProvider.RequestAccessToken();

if (tokenResult.TryGetToken(out var token))
{
try
{
var client = ClientFactory.CreateClient("ApiGateway");

// 2. Aggiungiamo il token manualmente all'header
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Value);

var response = await client.GetAsync("api/auth/test-privato");
messaggioDalServer = await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
messaggioDalServer = $"Errore chiamata: {ex.Message}";
}
}
else
{
// Questo ti dirà PERCHÉ fallisce (es. "consent_required", "invalid_scope")
if (tokenResult.Status == AccessTokenResultStatus.RequiresRedirect)
{
messaggioDalServer = "Errore: È necessario un redirect per il consenso.";
Console.WriteLine("tokenResult.InteractiveRequestUrl: " + tokenResult.InteractiveRequestUrl);
// Navigation.NavigateTo(tokenResult.InteractiveRequestUrl); // Scommenta se vuoi forzare il consenso
}
else
{
messaggioDalServer = $"Errore GetToken: {tokenResult.Status}";
}
}

}

public class RispostaApi { public string Messaggio { get; set; } = ""; }
}

Cosa fare adesso?

Il “motore” è montato e sincronizzato. Ora non resta che accenderlo:

  1. Avvia il Web Server API (assicurati che sia su HTTPS).

  2. Avvia la Blazor WASM App.

  3. Naviga sulla pagina fetcdati.razor.

Posted on

Blazor WebAssembly with Auth0 Authentication

Focus su Blazor WebAssembly (WASM)

Blazor WebAssembly è l’opzione più adatta per creare applicazioni client per il consumo di API.

Punti Chiave

  1. C# nel Browser: L’intero runtime .NET e la tua applicazione sono scaricati dal browser come file WebAssembly. Il codice C# viene eseguito direttamente nel browser in un sandbox.

  2. Architettura SPA: Blazor WASM crea applicazioni Single Page Application (SPA), il che significa che l’applicazione viene caricata una volta e le successive interazioni utente aggiornano dinamicamente solo le parti necessarie della pagina, offrendo un’esperienza utente fluida.

  3. Componenti Razor: Le interfacce utente sono costruite utilizzando i componenti Razor (.razor), che combinano markup HTML con logica C# (usando la direttiva @code).

Scrivere un’app in Blazor significa usare una tecnologia Microsoft (C#) che però poggia su una base totalmente aperta e universale (WebAssembly). Questo garantisce che la tua app funzionerà su qualsiasi dispositivo moderno, senza dipendere da plugin proprietari

Focus su Auth0

  • Auth0 è un Identity Provider che usa il protocollo OIDC (OpenID Connect).

  • Blazor WASM è un client OIDC nativo e Microsoft fornisce le librerie Microsoft.AspNetCore.Components.WebAssembly.Authentication per gestirlo.

  • L’applicazione Blazor reindirizza l’utente alla pagina di login Auth0 (Universal Login), riceve l’Access Token e lo memorizza.

Progetto Blazor WebAssembly

Creazione del Progetto in Visual Studio

  • Apri Visual Studio e crea un Nuovo Progetto.

  • Cerca e seleziona il template “Blazor WebAssembly App” (assicurati che non sia “Blazor Server App”).

  • Nella finestra di configurazione, per quanto riguarda  l’Autenticazione, scegli “Nessuna”

✅ Riepilogo delle Tue Scelte (Consigliate)

Opzione Stato (Check) Note
Applicazione Web Progressiva (PWA) Deselezionato Non necessario ora, aumenta la complessità.
Non usare istruzioni di primo livello Deselezionato Usa il codice moderno e conciso di .NET.
Configura per HTTPS Selezionato Essenziale. La tua app deve usare HTTPS per l’autenticazione con Auth0 (e per chiamare le API in produzione).
Include sample pages Selezionato Utile per avere un’architettura di base già pronta (layout, menu) e per testare velocemente l’autenticazione.

Configurazione su Auth0.com

Applicazione

Nel portale Auth0 crea una nuova applicazione di tipo “Single Page Application“.

Nella applicazione , nella tab settings :

  • prendi nota di : Domain e ClientID.
  • Seleziona “Cross-Origin Authentication”

API

Nella sezione API , Clicca su + Create API. Devi compilare questi campi:

  • Name (Nome): Un nome descrittivo (es. “Mio Servizio Dati API”).

  • Identifier (Identificativo): Questo è l’Audience!

Il Campo Identifier (Audience)

L’Identificativo (Audience) deve essere un URI (Uniform Resource Identifier) e di solito ha questo formato:

Esempio Audience: https://api.iltuoservizio.com

Importante: Non deve per forza essere un URL funzionante, ma deve essere un URI univoco.

Configurazione di Auth0 e delle Dipendenze

Pacchetti

Per autenticare un’applicazione Blazor WebAssembly con Auth0, devi utilizzare il supporto integrato di Microsoft per l’autenticazione OIDC (OpenID Connect), che è il protocollo utilizzato da Auth0. Con Nuget

  • installa il pacchetto Microsoft.Authentication.WebAssembly.Msal
  • installa il pacchetto Microsoft.AspNetCore.Components.WebAssembly.Authentication. Attenzione: installare la versione 8.0.1
  • installa il pacchetto Microsoft.Extensions.Http

Configurazione Applicazione

Nelle applicazioni .NET (inclusi i client Blazor WASM), il file appsettings.json è lo standard per archiviare le configurazioni, come: le Credenziali di Autenticazione: (Il Domain e il Client ID di Auth0). Blazor WebAssembly è progettato per leggere le impostazioni da questo file. Crea il file appsettings.json nella directory wwwroot per mantenere le configurazioni separate dal codice (il che è una best practice).

{
  "Auth0":
       {
          "Domain": "[IL-TUO-TENANT].auth0.com",
          "Authority": "https://[IL-TUO-TENANT].auth0.com",
          "ClientId": "[IL-TUO-CLIENT-ID-AUTH0.COM]",
          "Audience": "https://api.iltuoservizio.com"
      },
  "TestWebservice:
     {
       "Address": "localhost:7003"
     }
}

File Applicazione

Program.cs
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

// =========================================================
// >>> REGISTRAZIONI DI BASE (Devono venire prima di AddOidcAuthentication) <<<
// =========================================================
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<SignOutSessionStateManager>();
// =========================================================

// =========================================================
// >>> Parte cruciale per l'autenticazione Auth0/OIDC <<<
// =========================================================
builder.Services.AddOidcAuthentication(options =>
{
// Impostazione dell'Audience (CRUCIALE per chiamare le API)
// L'Audience deve essere passato come parametro aggiuntivo OIDC
options.ProviderOptions.AdditionalProviderParameters.Add(
"audience",
builder.Configuration["Auth0:Audience"]!
);

// Modifica o aggiungi questo per assegnare esplicitamente l'Authority
options.ProviderOptions.Authority = builder.Configuration["Auth0:Authority"]!;

// Assegna il ClientId
options.ProviderOptions.ClientId = builder.Configuration["Auth0:ClientId"]!;

// 3. Configurazione degli Scope
// Sostituiamo gli Scope predefiniti con la tua lista completa.
options.ProviderOptions.DefaultScopes.Clear(); // Rimuovi gli scope predefiniti

// Aggiungi tutti gli scope richiesti, inclusi quelli personalizzati:
options.ProviderOptions.DefaultScopes.Add("openid");
options.ProviderOptions.DefaultScopes.Add("profile");
options.ProviderOptions.DefaultScopes.Add("email");
options.ProviderOptions.DefaultScopes.Add("address");
options.ProviderOptions.DefaultScopes.Add("phone");
options.ProviderOptions.DefaultScopes.Add("read:appointments"); // <--- Scope API
});

// =========================================================

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

await builder.Build().RunAsync();
MainLayout.razor

Per mostrare lo stato di login, devi usare il componente Blazor integrato CascadingAuthenticationState nel file di layout principale.

Nel file Layout/MainLayout.razor, assicurati che il contenuto sia racchiuso in questo

@using Microsoft.AspNetCore.Components.Authorization
@inherits LayoutComponentBase

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

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

<article class="content px-4">

@Body

</article>
</main>
</CascadingAuthenticationState>
</div>
LoginDisplay.razor

Crea la cartella Shared ed in essa il componente LoginDisplay.razor.

@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager

<AuthorizeView>
<Authorized>
Ciao, @context.User.Identity?.Name!
<button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
</Authorized>
<NotAuthorized>
<a href="authentication/login">Log in</a>
</NotAuthorized>
</AuthorizeView>

@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthenticationState { get; set; }

private async Task BeginSignOut()
{
// Reindirizza l'utente al flusso di Logout di Auth0 (tramite il gestore Blazor)
await SignOutManager.SetSignOutState();
Navigation.NavigateTo("authentication/logout");
}
}

Importante. Per usare questo componente è necessario aggiungere a _Imports.razor il collegamento alla cartella Shared.

Script di Autenticazione (index.html)

Devi semplicemente aggiungere il tag <script> mancante nel tuo index.html nella sezione <body>, prima del tag che carica Blazor (_framework/blazor.webassembly.js).

Aggiungi questa riga:

<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
Authentication.razor

Crea il componente Pages/Authentication.razor

@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

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

// NOTA BENE: Non è necessario dichiarare RouteData
// in questo componente specifico se usi solo l'Action
}

Configurazione Auth0.com (seconda parte)

Lanciando l’app vedrete che la scritta Login in alto compare ma compare con u lieve ritardo. Se spingo login , scrive “Checking login state…” , poi, dopo alcuni secondi, vieni reindirizzato su una pagina si Auth0.com con errore “Callback URL mismatch.The provided redirect_uri is not in the list of allowed callback URLs.”

Trovate la vostra applicazione :

  • alla voce “Allowed Callback Urls” aggiungete   : https://localhost:7004/authentication/login-callback
  • alla voce “Allowed Logout Urls” aggiungete : https://localhost:7004/authentication/logout-callback

Riprovate. Dovreste avere implementato Login e Logout