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.