Con l’avanzare delle tecnologie web, così come i metodi e i protocolli progettati per garantirne la sicurezza. I protocolli OAuth 2.0 e OpenID Connect hanno evoluto significativamente in risposta alle minacce di sicurezza emergenti e alla crescente complessità delle applicazioni web. I metodi tradizionali di autenticazione, un tempo efficaci, stanno diventando obsoleti per le moderne Single Page Applications (SPAs), che affrontano nuove sfide di sicurezza. In questo contesto, il pattern architetturale Backend-For-Frontend (BFF) è emergente come soluzione raccomandata per organizzare le interazioni tra le SPAs e i loro sistemi backend, offrendo un approcio più sicuro e gestibile all’autenticazione e alla gestione delle sessioni. In questo articolo viene esplorato a fondo il pattern BFF, illustrando la sua applicazione pratica attraverso una soluzione minimale implementata con .NET e React. Al termine, otterrete una chiara comprensione di come sfruttare il pattern BFF per migliorare la sicurezza e la funzionalità delle vostre applicazioni web.
Contesto Storico
La storia di OAuth 2.0 e OpenID Connect riflette l’evoluzione continua delle tecnologie Internet. Scopriremo di più su questi protocolli e sulla loro influenza sulle applicazioni web moderne.
Introdotto nel 2012, il protocollo OAuth 2.0 è diventato uno standard ampiamente adottato per l’autorizzazione. Permette alle applicazioni terze di ottenere un accesso limitato ai risorse utente senza esporre le credenziali utente al client. OAuth 2.0 supporta diversi flussi, ognuno progettato per adattarsi flessibilmente a diversi casi d’uso.
Sulla base della piattaforma OAuth 2.0, il protocollo OpenID Connect (OIDC) è stato introdotto nel 2014, aggiungendo funzionalità di autenticazione essenziali. Fornisce alle applicazioni client una metodologia standard per verificare l’identità dell’utente e ottenere informazioni di base su di lui attraverso un punto d’accesso standardizzato o acquistando un token ID in formato JWT (JSON Web Token).
Evoluzione del Modello di Threat
Con le capacità e la popolarità crescenti dei SPAs, il modello di threat per i SPAs è anche evoluto. Le vulnerabilità come l’Injection Cross-Site Scripting (XSS) e la Forced Cross-Site Request Forgery (CSRF) sono diventate più diffuse. Poiché i SPAs spesso interagiscono con il server attraverso API, la salvaguardia sicura e l’utilizzo corretto di token d’accesso e refresh token è diventato fondamentale per la sicurezza.
Respingendo le richieste del tempo, i protocolli OAuth e OpenID Connect continuano ad evolversi per adattarsi ai nuovi挑战 che emergono con le nuove tecnologie e il crescente numero di threat. Allo stesso tempo, l’evoluzione costante delle threat e l’improvedimento delle pratiche di sicurezza implicano che le approci obsoleti non soddisfano più i requisiti di sicurezza moderni. Come risultato, il protocollo OpenID Connect attualmente offre una vasta gamma di capacità, ma molte di queste sono già considerate obsolete e spesso non sicure. Questa diversità crea difficoltà per i developer di SPA nell’scelta della via più appropriata e sicura con cui interagire con il server OAuth 2.0 e OpenID Connect.
In particolare, il Flusso Implicito ora può essere considerato obsoleto, e per ogni tipo di client, sia che si tratti di un SPA, una applicazione mobile o una applicazione desktop, ora si raccomanda fortemente l’uso del Flusso di Autorizzazione insieme alla Chiave di prova per lo Scambio di Codice (PKCE).
Sicurezza degli SPAs moderni
Perché gli SPAs moderni sono ancora considerati vulnerabili, anche quando viene utilizzato il Flusso di Autorizzazione con PKCE? Esistono diverse risposte a questa domanda.
Vulnerabilità del codice JavaScript
JavaScript è un potente linguaggio di programmazione che riveste un ruolo chiave negli Single Page Applications (SPA) moderni. Tuttavia, la sua ampia capacità e la sua diffusione possono rappresentare una minaccia potenziale. Gli SPAs moderni costruiti su librerie e framework come React, Vue o Angular usano un gran numero di librerie e dipendenze. Le possiamo vedere nella cartella node_modules
e il numero di queste dipendenze può essere di centinaia o persino migliaia. Ognuna di queste librerie può contenere vulnerabilità di varie gravità, e i developeri di SPA non hanno la possibilità di controllare a fondo il codice di tutte le dipendenze utilizzate. Spesso i developer non persino tracciano l’elenco completo delle dipendenze, poiché sono dipendenti tra loro in modo transitivo. Anche se sviluppano il proprio codice secondo i più alti standard di qualità e sicurezza, non si può essere certi assolutamente dell’assenza di vulnerabilità nell’applicazione finale.
Il codice JavaScript maligno, che può essere iniettato in un’applicazione nei vari modi, attraverso attacchi come l’Injection di Script tra Siti (XSS) o attraverso la compromissione di librerie terze, ottiene gli stessi privilegi e il livello d’accesso ai dati del codice applicativo legittimo. Questo consente al codice maligno di rubare dati dalla pagina corrente, interagire con l’interfaccia dell’applicazione, inviare richieste al backend, rubare dati dalla memoria locale (localStorage, IndexedDB) e persino iniziare sessioni di autenticazione in autonomia, ottenendo i suoi stessi token di accesso utilizzando lo stesso Flusso di Autorizzazione e PKCE.
Vulnerabilità Spectre
La vulnerabilità Spectre sfrutta le caratteristiche dell’architettura dei processori moderni per accedere a dati che dovrebbero essere isolati. Tale vulnerabilità è particolarmente pericolosa per gli SPA.
Prima di tutto, gli SPA utilizzano intensamente JavaScript per gestire lo stato dell’applicazione e per interagire col server. Questo aumenta la superficie di attacco per il codice JavaScript maligno che può esploitarne le vulnerabilità Spectre. In secondo luogo, a differenza delle applicazioni a pagine multipli tradizionali (MPAs), gli SPA raramente ricaricano, significa che la pagina e il suo codice caricato rimangono attivi per un lungo tempo. Questo dà agli attaccanti molto più tempo per eseguire attacchi utilizzando il codice JavaScript maligno.
Le vulnerabilità Spectre consentono agli attaccanti di rubare i token di accesso memorizzati nella memoria di un’applicazione JavaScript, consentendo l’accesso ai risorse protette impersonando l’applicazione legittima. L’esecuzione speculativa può anche essere utilizzata per rubare i dati della sessione utente, permettendo agli attaccanti di proseguire gli attacchi anche dopo la chiusura dell’SPA.
Non si può escludere la scoperta in futuro di altre vulnerabilità simili a Spectre.
Cosa fare?
Facciamo un riepilogo di una conclusione intermedia importante. I moderni SPA, dipendenti da un gran numero di librerie JavaScript di terze parti e che operano nell’ambiente del browser sui dispositivi utente, operano in un ambiente software e hardware che i sviluppatori non possono controllare completamente. Per questo motivo, dovremmo considerare tali applicazioni intrinsecamente vulnerabili.
Per rispondere alle minacce elencate, molti esperti tendono a evitare completamente l’archiviazione dei token nel browser e a progettare l’applicazione in modo da ottenere e processare l’accesso e i token di aggiornamento solo sul lato server dell’applicazione, e non li passano mai al lato browser. Nel contesto di un SPA con un backend, questo può essere raggiunto utilizzando il pattern Backend-For-Frontend (BFF).
Il schema di interazione tra il server di autorizzazione (OP), il client (RP) che implementa il pattern BFF e un API di terze parti (Server risorsa) è così descritto:
L’utilizzo del pattern BFF per proteggere gli SPA offre diversi vantaggi. Gli accesso e i token di aggiornamento sono memorizzati sul lato server e non vengono mai passati al browser, impedendo così il loro furto a causa di vulnerabilità. La gestione delle sessioni e dei token viene gestita sul server, permettendo un migliore controllo della sicurezza e una verifica di autenticazione più affidabile. L’applicazione client interagisce con il server attraverso il BFF, che semplifica la logica dell’applicazione e riduce il rischio di esecuzione di codice maligno.
Implementare il pattern Backend-For-Frontend sulla piattaforma .NET
Prima di procedere all’implementazione pratica del BFF sulla piattaforma .NET, consideriamo i suoi componenti necessari e pianifichiamo le nostre azioni. Assumiamo di avere già configurato un server OpenID Connect e che abbiamo bisogno di sviluppare un SPA che lavora con un backend, implementare l’autenticazione utilizzando OpenID Connect, e organizzare l’interazione tra le parti server e client utilizzando il pattern BFF.
Secondo il documento OAuth 2.0 for Browser-Based Applications, il pattern architetturale BFF presuppone che il backend agisca come client OpenID Connect, utilizzi il Flusso di Autorizzazione con PKCE per l’autenticazione, ottenga e conservi i token d’accesso e di aggiornamento sul proprio lato, e non li passi mai al lato SPA del browser. Il pattern BFF presuppone anche la presenza di un API sul lato backend costituito da quattro endpoint principali:
- Check Session: serve per verificare una sessione di autenticazione utente attiva. Di solito viene chiamato dallo SPA utilizzando un’API asincrona (fetch) e, se riuscito, restituisce informazioni sull’utente attivo. Così, lo SPA, caricato da una terza fonte (ad es. CDN), può verificare lo stato di autenticazione e decidere se proseguire le sue operazioni con l’utente oppure procedere all’autenticazione utilizzando il server OpenID Connect.
- Login: avvia il processo di autenticazione sul server OpenID Connect. Di solito, se l’SPA non riesce ad ottenere i dati utente autenticati alla fase 1 tramite Check Session, reindirizza il browser alla URL, che a sua volta forma una richiesta completa al server OpenID Connect e reindirizza il browser verso di esso.
- Accedi: riceve l’Authorization Code inviato dal server dopo la fase 2 in caso di autenticazione riuscita. Effettua una richiesta diretta al server OpenID Connect per scambiare l’Authorization Code + il codice verificatore PKCE per i token di Accesso e di Refresh. Avvia una sessione autenticata sul lato client emettendo un cookie di autenticazione all’utente.
- Esci: serve per terminate la sessione di autenticazione. Di solito, l’SPA reindirizza il browser alla URL, che a sua volta forma una richiesta all’endpoint End Session sul server OpenID Connect per terminare la sessione, nonché cancella la sessione sul lato client e il cookie di autenticazione.
Ora esaminate gli strumenti forniti dalla piattaforma .NET di base e guardate ciò che possiamo usare per implementare il pattern BFF. La piattaforma .NET offre il pacchetto NuGet Microsoft.AspNetCore.Authentication.OpenIdConnect
, che è una implementazione pronta di un client OpenID Connect supportato da Microsoft. Questo pacchetto supporta sia il Flusso di Authorization Code che PKCE, e aggiunge un endpoint con il percorso relativo /signin-oidc, che già implementa la funzionalità dell’endpoint di Accesso (vedi punto 3 sopra). Così, dobbiamo implementare solo i restanti tre endpoint.
Per un esempio pratico di integrazione, prenderemo un server OpenID Connect di prova basato sulla libreria Abblix OIDC Server. Tuttavia, tutto quello che viene menzionato di seguito si applica anche a qualsiasi altro server, inclusi i server pubblicamente disponibili di Facebook, Google, Apple e tutti gli altri che rispettano la specifica del protocollo OpenID Connect.
Per implementare il SPA sul lato frontend, userremo la libreria React, e sul lato backend, userremo .NET WebAPI. Questa è una delle tecnologie più comuni alla data della scrittura di questo articolo.
Il schema generale dei componenti e della loro interazione è così descritto:
Per lavorare con gli esempi da questo articolo, sarà anche necessario installare il .NET SDK e Node.js. Tutti gli esempi in questo articolo sono stati sviluppati e testati utilizzando .NET 8, Node.js 22 e React 18, che erano gli standard all’epoca della scrittura.
Creazione di un SPA Client su React con un backend su .NET
Per creare rapidamente un’applicazione client, è conveniente utilizzare una template pronto. Fino alla versione .NET 7, l’SDK offriva un template integrato per un’applicazione .NET WebAPI e un React SPA. Purtroppo, questo template è stato rimosso nella versione .NET 8. Ecco perché il team di Abblix ha creato il suo template personale, che include un backend .NET WebApi, un frontend SPA basato sulla libreria React e TypeScript, costruito con Vite. Questo template è disponibile pubblicamente come parte del pacchetto Abblix.Templates
e puoi installarlo eseguendo il seguente comando:
dotnet new install Abblix.Templates
Ora possiamo usare il template denominato abblix-react
. Usiamolo per creare una nuova applicazione chiamata BffSample
:
dotnet new abblix-react -n BffSample
Questo comando crea un’applicazione che consiste in un backend .NET WebApi e un client SPA React. I file relativi all’SPA sono situati nella cartella BffSample\ClientApp
.
Dopo aver creato il progetto, il sistema ti chiederà di eseguire un comando per installare le dipendenze:
cmd /c "cd ClientApp && npm install"
Questaazione è necessaria per installare tutte le dipendenze richieste per la parte client dell’applicazione. È consigliato accettare e eseguire questo comando digitando Y
(sì) per un avvio del progetto riuscito.
Cambiamo immediatamente il numero di porta su cui l’applicazione BffSample
viene eseguita localmente in 5003. Questa operazione non è obbligatoria, ma semplificherà la configurazione del server OpenID Connect. Per fare questo, apri il file BffSample\Properties\launchSettings.json
, cerca il profilo denominato https
e cambia il valore della proprietà applicationUrl
a https://localhost:5003
.
Successivamente, aggiungere il pacchetto NuGet che implementa il client OpenID Connect spostandosi nella cartella BffSample
e eseguendo il seguente comando:
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
Configurare due schemi di autenticazione denominate Cookies
e OpenIdConnect
nell’applicazione, leggendo le loro impostazioni dalla configurazione dell’applicazione. Per fare ciò, apportare modifiche al file BffSample\Program.cs
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ******************* INIZIO *******************
var configuration = builder.Configuration;
builder.Services
.AddAuthorization()
.AddAuthentication(options => configuration.Bind("Authentication", options))
.AddCookie()
.AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************** FINE ********************
var app = builder.Build();
E aggiungere le impostazioni necessarie per connettersi al server OpenID Connect nel file BffSample\appsettings.json
:
{
// ******************* INIZIO *******************
"Authentication": {
"DefaultScheme": "Cookies",
"DefaultChallengeScheme": "OpenIdConnect"
},
"OpenIdConnect": {
"SignInScheme": "Cookies",
"SignOutScheme": "Cookies",
"SaveTokens": true,
"Scope": ["openid", "profile", "email"],
"MapInboundClaims": false,
"ResponseType": "code",
"ResponseMode": "query",
"UsePkce": true,
"GetClaimsFromUserInfoEndpoint": true
},
// ******************** FINE ********************
"Logging": {
"LogLevel": {
"Default": "Information",
E nel file BffSample\appsettings.Development.json
:
{
// ******************* INIZIO *******************
"OpenIdConnect": {
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
"ClientSecret": "secret"
},
// ******************** FINE ********************
"Logging": {
"LogLevel": {
"Default": "Information",
Ora ripassiamo velocemente ogni impostazione e il suo scopo:
Autenticazione
sezione: La proprietàDefaultScheme
imposta l’autenticazione di default utilizzando lo schemaCookies
, eDefaultChallengeScheme
delega l’esecuzione dell’autenticazione allo schemaOpenIdConnect
quando l’utente non può essere autenticato dallo schema predefinito. Pertanto, quando l’utente è sconosciuto all’applicazione, il server OpenID Connect verrà chiamato per l’autenticazione, e successivamente l’utente autenticato riceverà un cookie di autenticazione, e tutte le ulteriori chiamate al server saranno autenticate con esso, senza contattare il server OpenID Connect.Sezione OpenIdConnect
:SignInScheme
eSignOutScheme
impostano ilCookies
scheme, che sarà utilizzato per salvare le informazioni dell’utente dopo l’accesso.- La proprietà
Authority
contiene l’URL base del server OpenID Connect.ClientId
eClientSecret
specificano l’identificativo e la chiave segreta della applicazione client, registrati sul server OpenID Connect. SaveTokens
indica la necessità di salvare i token ricevuti come risultato dell’autenticazione dal server OpenID Connect.Scope
contiene una lista degli scope ai quali la applicazioneBffClient
richiede l’accesso. In questo caso, gli standard scopeopenid
(identificativo utente),profile
(profilo utente) eemail
(email) sono richiesti.MapInboundClaims
è responsabile della trasformazione delle claim in entrata dal server OpenID Connect in claim usate nella applicazione. Un valore difalse
significa che le claim saranno salvate nella sessione dell’utente autenticato nella forma in cui sono ricevute dal server OpenID Connect.ResponseType
con il valorecode
indica che il client userà il Flusso di Autorizzazione Codice.ResponseMode
specifica la trasmissione del Codice di Autorizzazione nella stringa di query, che è il metodo predefinito per il Flusso di Autorizzazione Codice.- La proprietà
UsePkce
indica la necessità di usare PKCE durante l’autenticazione per prevenire l’intercettazione del Codice di Autorizzazione. - La proprietà
GetClaimsFromUserInfoEndpoint
indica che i dati del profilo utente dovrebbero essere ottenuti dall’endpoint UserInfo.
Poiché la nostra applicazione non presuppone un’interazione con l’utente senza autenticazione, garantiremo che il React SPA sia caricato solo dopo un’autenticazione riuscita. naturalmente, se l’SPA è caricato da una sorgente esterna, come un Host Web Statico, per esempio da server Content Delivery Network (CDN) o da un server di sviluppo locale avviato con il comando npm start
(ad esempio, quando si sta eseguendo l’esempio in debug), non sarà possibile controllare lo stato di autenticazione prima del caricamento dell’SPA. Ma, quando il nostro backend .NET è responsabile del caricamento dell’SPA, questo è possibile.
Per questo, aggiungere il middleware responsabile dell’autenticazione e dell’autorizzazione nel file BffSample\Program.cs
:
app.UseRouting();
// ******************* INIZIO *******************
app.UseAuthentication();
app.UseAuthorization();
// ******************* FINE *******************
Alla fine del file BffSample\Program.cs
, in corrispondenza del passaggio al caricamento dell’SPA, aggiungere il requisito per l’autorizzazione, .RequireAuthorization()
:
app.MapFallbackToFile("index.html").RequireAuthorization();
Impostazione del server OpenID Connect.
Come menzionato prima, per l’esempio di integrazione pratica, userò un server di OpenID Connect di test basato sulla libreria Abblix OIDC Server. La template di base per un’applicazione basata su ASP.NET Core MVC con la libreria Abblix OIDC Server
è anche disponibile nel pacchetto Abblix.Templates
che abbiamo installato prima. Usiamo questo template per creare una nuova applicazione denominata OpenIDProviderApp
:
dotnet new abblix-oidc-server -n OpenIDProviderApp
Per configurare il server, dobbiamo registrare l’applicazione BffClient
come client sul server OpenID Connect e aggiungere un utente di test. Per fare ciò, aggiungere i seguenti blocchi al file OpenIDProviderApp\Program.cs
:
var userInfoStorage = new TestUserStorage(
// ******************* INIZIO *******************
new UserInfo(
Subject: "1234567890",
Name: "John Doe",
Email: "[email protected]",
Password: "Jd!2024$3cur3")
// ******************** FINE ********************
);
builder.Services.AddSingleton(userInfoStorage);
// ...
// Registra e configura Abblix OIDC Server
builder.Services.AddOidcServices(options =>
{
// Configura le opzioni del server OIDC qui:
// ******************* INIZIO *******************
options.Clients = new[] {
new ClientInfo("bff_sample") {
ClientSecrets = new[] {
new ClientSecret {
Sha512Hash = SHA512.HashData(Encoding.ASCII.GetBytes("secret")),
}
},
TokenEndpointAuthMethod = ClientAuthenticationMethods.ClientSecretPost,
AllowedGrantTypes = new[] { GrantTypes.AuthorizationCode },
ClientType = ClientType.Confidential,
OfflineAccessAllowed = true,
PkceRequired = true,
RedirectUris = new[] { new Uri("https://localhost:5003/signin-oidc", UriKind.Absolute) },
PostLogoutRedirectUris = new[] { new Uri("https://localhost:5003/signout-callback-oidc", UriKind.Absolute) },
}
};
// ******************** FINE ********************
// La seguente URL porta all'azione di Login del controller AuthController
options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);
// La seguente riga genera una chiave nuova per la firma del token. Sostituirla se si desidera utilizzare le proprie chiavi.
options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});
Esaminaremo questo codice in dettaglio. Registriamo un cliente con l’identificativo bff_sample
e la chiave segreta secret
(memorizzandola come hash SHA512), indicando che l’acquisizione del token userà l’autenticazione del cliente con la chiave segreta inviata in un messaggio POST (ClientAuthenticationMethods.ClientSecretPost
). AllowedGrantTypes
specifica che il cliente è autorizzato solo ad usare il Flusso di Autorizzazione Code. ClientType
definisce il cliente come confidentale, cioè può memorizzare sicuramente la sua chiave segreta. OfflineAccessAllowed
consente al cliente di usare i token di aggiornamento. PkceRequired
obbliga l’uso di PKCE durante l’autenticazione. RedirectUris
e PostLogoutRedirectUris
contengono liste di URL consentiti per la reindirizzazione dopo l’autenticazione e la terminazione della sessione, rispettivamente.
Per qualsiasi altro server OpenID Connect, le impostazioni saranno simili, con differenze solo nella loro configurazione.
Implementazione dell’API BFF di base
Precisamente, abbiamo menzionato che l’utilizzo del package Microsoft.AspNetCore.Authentication.OpenIdConnect
aggiunge automaticamente l’implementazione dell’endpoint di Accesso al nostro applicazione di esempio. Ora è il momento di implementare la parte rimanente dell’API BFF. Usaremo un controller ASP.NET MVC per questi endpoint aggiuntivi. Iniziamo aggiungendo una cartella Controllers
e un file BffController.cs
nel progetto BffSample
con il seguente codice all’interno:
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BffSample.Controllers;
[ApiController]
[Route("[controller]")]
public class BffController : Controller
{
public const string CorsPolicyName = "Bff";
[HttpGet("check_session")]
[EnableCors(CorsPolicyName)]
public ActionResult> CheckSession()
{
// Restituisce 401 Non autorizzato per forzare la reindirizzazione dell'SPA all'endpoint di accesso
if (User.Identity?.IsAuthenticated != true)
return Unauthorized();
return User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
}
[HttpGet("login")]
public ActionResult> Login()
{
// Logica per iniziare il flusso dell'autorizzazione codice
return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
}
[HttpPost("logout")]
public IActionResult Logout()
{
// Logica per gestire il logout dell'utente
return SignOut();
}
}
Ci dispiace rompere questo codice di classe in dettaglio:
- Il suffisso
[Route("[controller]")]
imposta la base per tutte le azioni nel controller. In questo caso, la route corrisponderà al nome del controller, ovvero tutte le path per i nostri metodi API cominceranno con/bff/
. - La costante
CorsPolicyName = "Bff"
definisce il nome della politica CORS (Cross-Origin Resource Sharing) da usare negli attributi del metodo. Ne farà riferimento più avanti. - I tre metodi
CheckSession
,Login
eLogout
implementano le funzionalità necessarie del BFF descritte prima. Gestiscono le richieste GET su/bff/check_session
,/bff/login
e le richieste POST su/bff/logout
rispettivamente. - Il metodo
CheckSession
verifica lo stato di autenticazione dell’utente. Se l’utente non è autenticato, restituisce il codice401 Non autorizzato
, che dovrebbe forzare l’SPA a reindirizzare all’endpoint di autenticazione. Se l’autenticazione è riuscita, il metodo restituisce un insieme di dichiarazioni e i loro valori. Questo metodo include anche una bind della politica CORS con il nomeCorsPolicyName
poiché la chiamata a questo metodo può essere cross-domain e contenere cookie usati per l’autenticazione utente. - Il metodo
Login
viene chiamato dall’SPA se la precedente chiamataCheckSession
ha restituito401 Non autorizzato
. Assicura che l’utente non sia ancora autenticato e inizia il processo configuratoChallenge
, che comporterà la reindirizzazione al server OpenID Connect, l’autenticazione utente tramite Flusso di Autorizzazione e PKCE, e l’emissione di una cookie di autenticazione. Dopo questo, il controllo torna alla root della nostra applicazione"~/"
, che farà saltare all’SPA a ciclo e avvierà un utente autenticato. - Il metodo
Logout
è anche chiamato dall’SPA ma termina la sessione di autenticazione corrente. Rimuove le cookie di autenticazione emessi dalla parte server diBffSample
e chiama anche l’endpoint Sessione su lato server OpenID Connect.
Configurazione CORS per BFF
Come menzionato sopra, il metodo CheckSession
è rivolto a chiamate asincrone dall’SPA (di solito utilizzando l’API Fetch). La corretta funzionatura di questo metodo dipende dalla possibilità di inviare cookie di autenticazione dal browser. Se l’SPA è caricato da un Host Web Statico separato, come un CDN o un server di sviluppo che funziona su una porta separata, questa chiamata diventa cross-domain. Questo rende necessaria la configurazione di una politica CORS, senza la quale l’SPA non sarà in grado di richiamare questo metodo.
Già indicammo nel codice del controller nel file Controllers\BffController.cs
che la politica CORS denominata CorsPolicyName = "Bff"
deve essere utilizzata. Ora è il momento di configurare i parametri di questa politica per risolvere il nostro compito. Torniamo al file BffSample/Program.cs
e aggiungiamo i seguenti blocchi di codice:
// ******************* INIZIO *******************
using BffSample.Controllers;
// ******************** FINE ********************
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ...
builder.Services
.AddAuthorization()
.AddAuthentication(options => configuration.Bind("Authentication", options))
.AddCookie()
.AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* INIZIO *******************
builder.Services.AddCors(
options => options.AddPolicy(
BffController.CorsPolicyName,
policyBuilder =>
{
var allowedOrigins = configuration.GetSection("CorsSettings:AllowedOrigins").Get();
if (allowedOrigins is { Length: > 0 })
policyBuilder.WithOrigins(allowedOrigins);
policyBuilder
.WithMethods(HttpMethods.Get)
.AllowCredentials();
}));
// ******************** FINE ********************
var app = builder.Build();
Questo codice consente alle politiche CORS di essere chiamate dalle SPAs caricate da fonti specificate nella configurazione come un array di stringhe CorsSettings:AllowedOrigins
, utilizzando il metodo GET
e consente l’invio di cookie in questa chiamata. Inoltre, assicurarsi che la chiamata a app.UseCors(...)
sia posizionata proprio prima di app.UseAuthentication()
:
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* INIZIO *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** FINE ********************
app.UseAuthentication();
app.UseAuthorization();
Per garantire che la politica CORS funzioni correttamente, aggiungere il设置 corrispondente al file di configurazione BffSample\appsettings.Development.json
:
{
// ******************* INIZIO *******************
"CorsSettings": {
"AllowedOrigins": [ "https://localhost:3000" ]
},
// ******************** FINE ********************
"OpenIdConnect": {
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
Nell nostro esempio, l’indirizzo https://localhost:3000
è l’indirizzo dove viene avviato il server di sviluppo con l’SPA React utilizzando il comando npm run dev
. Questo indirizzo puoi trovarlo nel tuo caso aprendo il file BffSample.csproj
e trovando il valore del parametro SpaProxyServerUrl
. In un’applicazione reale, la politica CORS potrebbe includere l’indirizzo del tuo CDN (Content Delivery Network) o un servizio simile. E’ importante ricordare che se il tuo SPA è caricato da un indirizzo e una porta diversi da quelli che forniscono l’API BFF, devi aggiungere questo indirizzo alla configurazione della politica CORS.
Implementare l’autenticazione tramite BFF in un’applicazione React
Abbiamo implementato l’API BFF sul lato server. Ora è il momento di concentrarci sul React SPA e aggiungere la funzionalità corrispondente per chiamare questa API. Cominciamo navigando nella cartella BffSample\ClientApp\src\
, creando una cartella components
, e aggiungendo un file Bff.tsx
con il seguente contenuto:
import React, { createContext, useContext, useEffect, useState, ReactNode, FC } from 'react';
// Definisci la forma del contesto BFF
interface BffContextProps {
user: any;
fetchBff: (endpoint: string, options?: RequestInit) => Promise;
checkSession: () => Promise;
login: () => void;
logout: () => Promise;
}
// Crea un contesto per BFF per condividere lo stato e le funzioni attraverso l'applicazione
const BffContext = createContext({
user: null,
fetchBff: async () => new Response(),
checkSession: async () => {},
login: () => {},
logout: async () => {}
});
interface BffProviderProps {
baseUrl: string;
children: ReactNode;
}
export const BffProvider: FC = ({ baseUrl, children }) => {
const [user, setUser] = useState(null);
// Normalizza l'URL di base togliendo il trattino iniziale per evitare URL incoerenti
if (baseUrl.endsWith('/')) {
baseUrl = baseUrl.slice(0, -1);
}
const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
try {
// La funzione fetch include le credenziali per gestire i cookie, necessari per l'autenticazione
return await fetch(`${baseUrl}/${endpoint}`, {
credentials: 'include',
...options
});
} catch (error) {
console.error(`Error during ${endpoint} call:`, error);
throw error;
}
};
// La funzione login reindirizza alla pagina di login quando l'utente deve autenticarsi
const login = (): void => {
window.location.replace(`${baseUrl}/login`);
};
// La funzione checkSession è responsabile della verifica della sessione utente all'render iniziale
const checkSession = async (): Promise => {
const response = await fetchBff('check_session');
if (response.ok) {
// Se la sessione è valida, aggiorna lo stato utente con i dati ricevuti dei claim
setUser(await response.json());
} else if (response.status === 401) {
// Se l'utente non è autenticato, reindirizza alla pagina di login
login();
} else {
console.error('Unexpected response from checking session:', response);
}
};
// Funzione per l'uscita dell'utente
const logout = async (): Promise => {
const response = await fetchBff('logout', { method: 'POST' });
if (response.ok) {
// Reindirizza alla pagina iniziale dopo l'uscita riuscita
window.location.replace('/');
} else {
console.error('Logout failed:', response);
}
};
// useEffect è utilizzato per eseguire la funzione checkSession appena il componente viene montato
// Questo assicura che la sessione venga verificata immediatamente quando l'app si carica
useEffect(() => { checkSession(); }, []);
return (
// Fornisci al contesto BFF valori e funzioni relative da utilizzare attraverso l'applicazione
{children}
);
};
// Hook personalizzato per usare facilmente il contesto BFF in altri componenti
export const useBff = (): BffContextProps => useContext(BffContext);
// Esporta HOC per fornire accesso al contesto BFF
export const withBff = (Component: React.ComponentType) => (props: any) =>
{context => }
;
Questo file esporta:
- Il componente
BffProvider
, che crea un contesto per BFF e fornisce funzioni e stato relative all’autenticazione e alla gestione delle sessioni per l’intera applicazione. - La custom hook
useBff()
, che restituisce un oggetto contenente lo stato attuale dell’utente e funzioni per lavorare con il BFF:checkSession
,login
, elogout
. Ѐ progettato per l’uso in componenti React funzionali. - Il Higher-Order Component (HOC)
withBff
per l’uso in componenti React basati su classi.
Successivamente, creare un componente UserClaims
, che mostrerà i claim dell’utente corrente dopo l’autenticazione riuscita. Crea un file UserClaims.tsx
nella directory BffSample\ClientApp\src\components
con il seguente contenuto:
import React from 'react';
import { useBff } from './Bff';
export const UserClaims: React.FC = () => {
const { user } = useBff();
if (!user)
return <div>Checking user session...</div>;
return (
<>
<h2>User Claims</h2>
{Object.entries(user).map(([claim, value]) => (
<div key={claim}>
<strong>{claim}</strong>: {String(value)}
</div>
))}
</>
);
};
Questo codice controlla l’esistenza di un utente autenticato usando il hook useBff()
e mostra i claim dell’utente come una lista se l’utente è autenticato. Se i dati utente non sono ancora disponibili, mostra il testo Checking user session...
.
Adesso, passiamo al file BffSample\ClientApp\src\App.tsx
. Sostituisci il suo contenuto con il codice necessario. Importa BffProvider
da components/Bff.tsx
e UserClaims
da components/UserClaims.tsx
, e inserisci il codice della componente principale:
import { BffProvider, useBff } from './components/Bff';
import { UserClaims } from './components/UserClaims';
const LogoutButton: React.FC = () => {
const { logout } = useBff();
return (
<button className="logout-button" onClick={logout}>
Logout
</button>
);
};
const App: React.FC = () => (
<BffProvider baseUrl="https://localhost:5003/bff">
<div className="card">
<UserClaims/>
</div>
<div className="card">
<LogoutButton />
</div>
</BffProvider>
);
export default App;
Qui, il parametro baseUrl
specifica l’URL di base dell’API BFF https://localhost:5003/bff
. Questa semplificazione è intenzionale e fatta perché è semplice. In un’applicazione reale, dovresti fornire questa impostazione dinamicamente invece di codificarla manualmente. Ci sono vari modi per ottenere questo risultato, ma discuterne è fuori dal scope di questo articolo.
Il pulsante Logout
permette all’utente di disconnettersi. chiama la funzione logout
disponibile tramite il hook useBff
e reindirizza il browser dell’utente all’endpoint /bff/logout
, che termina la sessione utente sul lato server.
A questo punto, ora puoi avviare l’applicazione BffSample
insieme all’OpenIDProviderApp
e testare le sue funzionalità. Puoi usare il comando dotnet run -lp https
in ciascun progetto o nel tuo IDE preferito per avviarli. Entrambe le applicazioni devono essere in esecuzione contemporaneamente.
Dopo questo, apri il tuo browser e naviga a https://localhost:5003
. Se tutto è impostato correttamente, lo SPA caricherà e chiamerà /bff/check_session
. L’endpoint /check_session
restituirà una risposta 401, che richiederà allo SPA di reindirizzare il browser a /bff/login
, che quindi avvierà l’autenticazione sul server tramite lo Flusso di Autorizzazione OpenID Connect utilizzando PKCE. Puoi osservare questa sequenza di richieste apriendo la Console di Sviluppo del tuo browser e passando alla scheda Network. Dopo aver fornito correttamente le credenziali utente ([email protected]
, Jd!2024$3cur3
), il controllo ritorna allo SPA e vedrai le dichiarazioni dell’utente autenticato nel browser:
sub: 1234567890
sid: V14fb1VQbAFG6JXTYQp3D3Vpa8klMLcK34RpfOvRyxQ
auth_time: 1717852776
name: John Doe
email: [email protected]
Inoltre, cliccando sul pulsante Logout
il browser verrà reindirizzato a /bff/logout
, che disconterà l’utente e tornarai alla pagina di accesso con un prompt per immettere il tuo nome utente e la password.
Se incontrate degli errori, potete confrontare il vostro codice con il nostro repository GitHub Abblix/Oidc.Server.GettingStarted, che contiene questo esempio e altri pronti per l’esecuzione.
Risoluzione problemi di fiducia negli certificati HTTPS
Quando si testano localmente applicazioni web configurate per funzionare attraverso HTTPS, potreste ricevere avvisi del browser che il certificato SSL non è fidato. Questo problema si verifica perché i certificati di sviluppo usati da ASP.NET Core non sono emessi da una Certification Authority (CA) riconosciuta, ma sono firmati automaticamente o non sono presenti nel sistema affatto. Questi avvisi possono essere eliminati esecutando una volta il seguente comando:
dotnet dev-certs https --trust
Questo comando genera un certificato firmato automaticamente per localhost
e lo installa nel vostro sistema in modo da fidarsi di questo certificato. Il certificato sarà usato da ASP.NET Core per eseguire le applicazioni web localmente. Dopo aver eseguito questo comando, riavvia il vostro browser perché le modifiche abbiano effetto.
Nota speciale per gli utenti di Chrome: Anche dopo aver installato il certificato di sviluppo come fidato, alcune versioni di Chrome potrebbero ancora limitare l’accesso ai siti di localhost
per motivi di sicurezza. Se incontrate un errore che indica che la vostra connessione non è sicura e l’accesso a localhost
è bloccato da Chrome, puoi aggirare questo problema così:
- Clicca ovunque sulla pagina di errore e digita
thisisunsafe
obadidea
, a seconda della versione di Chrome in uso. Queste sequenze di tasti funzionano come comandi di bypass in Chrome, permettendovi di procedere al sito dilocalhost
.
È importante utilizzare questi metodi di bypass solo in ambienti di sviluppo, in quanto possono comportare rischi reali per la sicurezza.
Chiamata di API terze parti tramite BFF
Abbiamo già implementato con successo l’autenticazione nella nostra applicazione BffSample
. Ora procediamo alla chiamata di un API esterno che richiede un token di accesso.
Immaginate di avere un servizio separato che fornisce i dati necessari, come previsioni del tempo, e che questo accesso sia consentito solo tramite un token di accesso. Il ruolo del server di BffSample
sarà quello di agire come un proxy inverso, cioè accettare e autenticare la richiesta di dati dall’SPA, aggiungere il token di accesso a essa, inoltrare questa richiesta al servizio del tempo e poi restituire la risposta dal servizio all’SPA.
Creazione del servizio ApiSample
Prima di dimostrare la chiamata remota dell’API tramite il BFF, occorre creare un’applicazione che si occuperà di questo API nel nostro esempio.
Per creare l’applicazione, userà una template fornita da .NET. Navigare nella cartella che contiene i progetti OpenIDProviderApp
e BffSample
, e eseguire il seguente comando per creare l’applicazione ApiSample
:
dotnet new webapi -n ApiSample
Questa applicazione ASP.NET Core Minimal API fornisce un singolo endpoint con il percorso /weatherforecast
che fornisce i dati del tempo in formato JSON.
Prima di tutto, cambia il numero di porta assegnato casualmente all’applicazione ApiSample
utilizzato localmente in un porto fisso, 5004. Come menzionato prima, questo passaggio non è obbligatorio, ma semplifica il nostro setup. Per fare questo, apri il file ApiSample\Properties\launchSettings.json
, cerca la profilo denominata https
e cambia il valore della proprietà applicationUrl
in https://localhost:5004
.
Ora configuriamo l’API del tempo in modo che sia accessibile solo tramite un token di accesso. Naviga nella cartella del progetto ApiSample
e aggiungi il pacchetto NuGet per l’autenticazione del token JWT Bearer:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Configura il schema di autenticazione e la politica di autorizzazione denominata WeatherApi
nel file ApiSample\Program.cs
:
// ******************* INIZIO *******************
using System.Security.Claims;
// ******************** FINE ********************
var builder = WebApplication.CreateBuilder(args);
// Aggiungi servizi al container.
// Per ulteriori informazioni sulla configurazione di Swagger/OpenAPI, visitare https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ******************* INIZIO *******************
var configuration = builder.Configuration;
builder.Services
.AddAuthentication()
.AddJwtBearer(options => configuration.Bind("JwtBearerAuthentication", options));
const string policyName = "WeatherApi";
builder.Services.AddAuthorization(
options => options.AddPolicy(policyName, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireAssertion(context =>
{
var scopeValue = context.User.FindFirstValue("scope");
if (string.IsNullOrEmpty(scopeValue))
return false;
var scope = scopeValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return scope.Contains("weather", StringComparer.Ordinal);
});
}));
// ******************** FINE ********************
var app = builder.Build();
Questo blocco di codice imposta l’autenticazione leggendo la configurazione dalle impostazioni dell’applicazione, include l’autorizzazione utilizzando JWT (JSON Web Tokens) e configura una politica di autorizzazione denominata WeatherApi
. La politica di autorizzazione WeatherApi
impone i seguenti requisiti:
policy.RequireAuthenticatedUser()
: Garantisce che solo gli utenti autenticati possano accedere ai risorse protette.policy.RequireAssertion(context => ...)
: L’utente deve avere una dichiarazionescope
che include il valoreweather
. Poiché la dichiarazionescope
può contenere più valori separati da spazi secondo RFC 8693, il valore realescope
viene diviso in parti individuali, e l’array risultante viene controllato per contenere il valore richiestoweather
.
Insieme, queste condizioni garantiscono che solo gli utenti autenticati con un token di accesso autorizzato per lo scope weather
possano chiamare l’endpoint protetto da questa politica.
Dobbiamo applicare questa politica all’endpoint /weatherforecast
. Aggiungere il chiamato a RequireAuthorization()
come mostrato qui sotto:
app.MapGet("/weatherforecast", () =>
{
// ...
})
.WithName("GetWeatherForecast")
// ******************* START *******************
.WithOpenApi()
.RequireAuthorization(policyName);
// ******************** END ********************
Aggiungere le impostazioni di configurazione necessarie per il schema di autenticazione al file `appsettings.Development.json` dell’applicazione `ApiSample`:
{
// ******************* START *******************
"JwtBearerAuthentication": {
"Authority": "https://localhost:5001",
"MapInboundClaims": false,
"TokenValidationParameters": {
"ValidTypes": [ "at+jwt" ],
"ValidAudience": "https://localhost:5004",
"ValidIssuer": "https://localhost:5001"
}
},
// ******************** END ********************
"Logging": {
"LogLevel": {
"Default": "Information",
Esaminiamo ora ogni impostazione dettagliatamente:
Authority
: Questo è l’URL che punta all’autorizzazione server OpenID Connect che emette token JWT. Il provider di autenticazione configurato nell’applicazione `ApiSample` userà questo URL per ottenere le informazioni necessarie per la verifica dei token, come le chiavi di firma.MapInboundClaims
: questa impostazione controlla come le dichiarazioni in entrata dal token JWT sono mappate alle dichiarazioni interne in ASP.NET Core. È impostata sufalse
, cioè le dichiarazioni useranno i loro nomi originali dal JWT.TokenValidationParameters
:ValidTypes
: Impostato suat+jwt
, che secondo RFC 9068 2.1 indica un Token d’Accesso in formato JWT.ValidAudience
: Specifica che l’applicazione accetterà token emessi per il clienthttps://localhost:5004
.ValidIssuer
: Specifica che l’applicazione accetterà token emessi dal serverhttps://localhost:5001
.
Configurazione aggiuntiva di OpenIDProviderApp
La combinazione del servizio di autenticazione OpenIDProviderApp
e dell’applicazione client BffSample
funziona bene per fornire l’autenticazione utente. Tuttavia, per consentire chiamate ad un API remoto, è necessario registrare l’applicazione ApiSample
come risorsa con OpenIDProviderApp
. Nel nostro esempio, usiamo l’Abblix OIDC Server
, che supporta RFC 8707: Resource Indicators for OAuth 2.0. Perciò, registreremo l’applicazione ApiSample
come risorsa con l’ambito weather
. Se state utilizzando un altro server OpenID Connect che non supporta gli indicatori delle risorse, rimane comunque consigliabile registrare un ambito univoco per questo API remoto (come ad esempio weather
nel nostro esempio).
Aggiungere il seguente codice al file OpenIDProviderApp\Program.cs
:
// Register and configure Abblix OIDC Server
builder.Services.AddOidcServices(options => {
// ******************* START *******************
options.Resources =
[
new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
];
// ******************** END ********************
options.Clients = new[] {
new ClientInfo("bff_sample") {
In questo esempio, registriamo l’applicazione ApiSample
, specificando come risorsa l’indirizzo base https://localhost:5004
e definendo un scope specifico denominato weather
. Nelle applicazioni reali, specialmente quelle con API complesse costituite da molti endpoint, è consigliabile definire scope separati per ogni endpoint individuale o gruppo di endpoint relazionati. Questo approcio consente un controllo dell’accesso più preciso e offre flessibilità nella gestione dei diritti di accesso. Ad esempio, è possibile creare scope distinti per differenti operazioni, moduli di applicazione o livelli di accesso utenti, permettendo un controllo più granulare su chi può accedere a specifiche parti dell’API.
Elaborazione di BffSample per la mediazione delle richieste verso un API remoto
L’applicazione cliente BffSample
ora deve fare più di solo richiedere un token di accesso per ApiSample
. Deve anche gestire le richieste dalla SPA all’API remoto. Questo implica l’aggiunta del token di accesso ottenuto dal servizio OpenIDProviderApp
the queste richieste, il loro inoltramento al server remoto e il riporto delle risposte del server allo SPA. In sostanza, BffSample
deve funzionare come un server reverse proxy.
Invece di implementare manualmente la mediazione delle richieste nella nostra applicazione cliente, userremo YARP (Yet Another Reverse Proxy), un prodotto pronto-fatto sviluppato da Microsoft. YARP è un server reverse proxy scritto in .NET e disponibile come pacchetto NuGet.
Per utilizzare YARP nell’applicazione BffSample
, prima aggiungiamo il pacchetto NuGet:
dotnet add package Yarp.ReverseProxy
Successivamente, aggiungiamo i seguenti namespace all’inizio del file BffSample\Program.cs
:
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Net.Http.Headers;
using Yarp.ReverseProxy.Transforms;
Prima della chiamata var app = builder.Build();
, aggiungere il codice:
builder.Services.AddHttpForwarder();
E tra le chiamate a app.MapControllerRoute()
e app.MapFallbackToFile()
:
app.MapForwarder(
"/bff/{**catch-all}",
configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
builderContext =>
{
// Rimuovi il prefisso "/bff" dalla path della richiesta
builderContext.AddPathRemovePrefix("/bff");
builderContext.AddRequestTransform(async transformContext =>
{
// Ottieni il token di accesso ricevuto precedentemente durante il processo di autenticazione
var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
// Aggiungi un header con il token di accesso alla richiesta di proxy
transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
});
}).RequireAuthorization();
Scopriamo cosa fa questo codice:
builder.Services.AddHttpForwarder()
registra i servizi necessari di YARP nel contenitore di IoC.app.MapForwarder
imposta il forwarding della richiesta ad un altro server o endpoint."/bff/{**catch-all}"
è il modello di percorso che il reverse proxy risponderà. Tutte le richieste che iniziano con/bff/
saranno processate da YARP.{**catch-all}
viene utilizzato per catturare tutte le parti restanti dell’URL dopo/bff/
.configuration.GetValue<string>("OpenIdConnect:Resource")
usa la configurazione dell’applicazione per ottenere il valore dalla sezioneOpenIdConnect:Resource
. Questo valore specifica l’indirizzo del资源 a cui le richieste verranno indirizzate. Nell’esempio nostro, questo valore saràhttps://localhost:5004
– l’URL di base in cui opera l’applicazioneApiSample
.builderContext => ...
aggiunge le trasformazioni necessarie che YARP eseguirà su ogni richiesta in arrivo dalla SPA. Nel nostro caso, ci saranno due trasformazioni di questo tipo:builderContext.AddPathRemovePrefix("/bff")
rimuove il prefisso/bff
dal percorso originale della richiesta.builderContext.AddRequestTransform(async transformContext => ...)
aggiunge un’intestazione HTTPAuthorization
alla richiesta, contenente il token di accesso precedentemente ottenuto durante l’autenticazione. In questo modo, le richieste dalla SPA all’API remota saranno autenticate utilizzando il token di accesso, anche se la SPA stessa non ha accesso a tale token.
.RequireAuthorization()
specifica che l’autorizzazione è necessaria per tutte le richieste inoltrate. Solo gli utenti autorizzati potranno accedere al percorso/bff/{**catch-all}
.
Per richiedere un token di accesso per il risorsa https://localhost:5004
durante l’autenticazione, aggiungere il parametro Resource
con il valore https://localhost:5004
alla configurazione OpenIdConnect
nel file BffSample/appsettings.Development.json
:
"OpenIdConnect": {
// ******************* INIZIO *******************
"Resource": "https://localhost:5004",
// ******************** FINE ********************
"Authority": "https://localhost:5001",
"ClientId": "bff_sample",
Inoltre, aggiungere un altro valore weather
all’array scope
nel file BffSample/appsettings.json
:
{
"OpenIdConnect": {
// ...
// ******************* INIZIO *******************
"Scope": ["openid", "profile", "email", "weather"],
// ******************** FINE ********************
// ...
}
}
Nota: In un progetto reale, è necessario monitorare l’expirazione del token di accesso. Quando il token sta per scadere, si deve richiedere un nuovo token in anticipo utilizzando un refresh token dal servizio di autenticazione o gestire un errore di negazione dell’accesso dalla API remota ottenendo un nuovo token e riproducendo la richiesta originale. Per sceltavolontà, questo aspetto è deliberatamente stato omesso in questo articolo.
Richiesta dell’API Meteo tramite BFF nell’applicazione SPA
Il backend è ora pronto. Abbiamo l’applicazione ApiSample
, che implementa un API con autorizzazione basata su token, e l’applicazione BffSample
, che include un server reverse proxy incluso per fornire un accesso sicuro a questo API. L’ultimo passaggio è aggiungere la funzionalità per richiedere questo API e visualizzare i dati ottenuti all’interno dell’applicazione React SPA.
Aggiungi il file WeatherForecast.tsx
in BffSample\ClientApp\src\components
con il seguente contenuto:
import React, { useEffect, useState } from 'react';
import { useBff } from './Bff';
interface Forecast {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
}
interface State {
forecasts: Forecast[];
loading: boolean;
}
export const WeatherForecast: React.FC = () => {
const { fetchBff } = useBff();
const [state, setState] = useState<State>({ forecasts: [], loading: true });
const { forecasts, loading } = state;
useEffect(() => {
fetchBff('weatherforecast')
.then(response => response.json())
.then(data => setState({ forecasts: data, loading: false }));
}, [fetchBff]);
const contents = loading
? <p><em>Loading...</em></p>
: (
<table className="table table-striped" aria-labelledby="tableLabel">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{forecasts.map((forecast, index) => (
<tr key={index}>
<td>{forecast.date}</td>
<td align="center">{forecast.temperatureC}</td>
<td align="center">{forecast.temperatureF}</td>
<td>{forecast.summary}</td>
</tr>
))}
</tbody>
</table>
);
return (
<div>
<h2 id="tableLabel">Weather forecast</h2>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
);
};
Scopriamo ora questo codice:
- L’interfaccia
Forecast
definisce la struttura dei dati della previsione del tempo, che include la data, la temperatura in gradi Celsius e Fahrenheit, e un riepilogo del tempo. L’interfacciaState
descrive la struttura dello stato del componente, costituito da un array di previsioni del tempo e un flag di caricamento. - Il componente
WeatherForecast
recupera la funzionefetchBff
dall’hookuseBff
e la utilizza per recuperare i dati del tempo dal server. Lo stato del componente è gestito utilizzando l’hookuseState
, inizializzando con un array vuoto di previsioni e un flag di caricamento impostato a vero. - L’hook
useEffect
attiva la funzionefetchBff
quando il componente viene montato, recuperando i dati di previsione del tempo dal server all’endpoint/bff/weatherforecast
. Una volta ricevuto il response del server e convertito in JSON, i dati vengono memorizzati nello stato del componente ( tramitesetState
) e il flag di caricamento viene aggiornato a falso. - A seconda del valore del flag di caricamento, il componente mostra un messaggio “Caricamento in corso…” oppure visualizza una tabella con i dati della previsione del tempo. La tabella include colonne per la data, la temperatura in gradi Celsius e Fahrenheit, e un riepilogo del tempo per ciascuna previsione.
Adesso, aggiungi il componente WeatherForecast
a BffSample\ClientApp\src\App.tsx
:
// ******************* INIZIO *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************** FINE ********************
// ...
// ******************* INIZIO *******************
// ******************** FINE ********************
Esecuzione e Test
Se tutto è stato fatto correttamente, ora puoi avviare tutti e tre i nostri progetti. Usa il comando della console dotnet run -lp https
per ciascuna applicazione per avviare le applicazioni con HTTPS.
Dopo aver avviato tutte e tre le applicazioni, apri il progetto BffSample
nel tuo browser (https://localhost:5003) e autentica utente utilizzando i credenziali [email protected]
e Jd!2024$3cur3
. Dopo aver autenticato con successo, dovresti vedere l’elenco delle dichiarazioni ricevute dall’autenticatore, come vedemmo prima. Inferiore a questo, vedrai anche la previsione del tempo.
La previsione del tempo è fornita dalla applicazione separata ApiSample
, che utilizza un token d’accesso emesso dall’autenticazione del servizio OpenIDProviderApp
. Vedere la previsione del tempo nell’applicazione BffSample
indica che il nostro SPA ha chiamato correttamente il backend di BffSample
, che a sua volta ha fatto proxy della chiamata a ApiSample
aggiungendo il token d’accesso. ApiSample
ha autenticato la chiamata e ha risposto con un JSON contenente la previsione del tempo.
La Soluzione Completa è Disponibile su GitHub
Se durante l’implementazione dei progetti di test si verificano problemi o errori, è possibile riferirsi alla soluzione completa disponibile nel repository GitHub. Semplicemente clonare il repository Abblix/Oidc.Server.GettingStarted per accedere ai progetti completamente implementati descritti in questo articolo. Questo risorsa serve sia come strumento per la risoluzione dei problemi che come punto di partenza solido per la creazione dei propri progetti.
Conclusione
L’evoluzione dei protocolli di autenticazione come OAuth 2.0 e OpenID Connect riflette le tendenze più ampie nella sicurezza web e nelle capacità del browser. Il passaggio da metodi obsoleti come l’Implicit Flow verso approcchi più sicuri, come il Authorization Code Flow con PKCE, ha notevolmente incrementato la sicurezza. Tuttavia, le vulnerabilità inerenti all’operare in ambienti non controllati rendono difficile la sicurezza di SPA moderni. La memorizzazione esclusiva dei token sul backend e l’adozione del pattern Backend-For-Frontend (BFF) è una strategia efficace per mitigare i rischi e garantire una protezione robusta dei dati utente.
I sviluppatori devono rimanere attenti nell’affrontare il panorama in continuo cambiamento delle minacce implementando nuovi metodi di autenticazione e approcchi architetturali aggiornati. Questo approcchio proattivo è cruciale per la costruzione di applicazioni web sicure e affidabili. In questo articolo abbiamo esplorato e implementato un approcchio moderno all’integrazione di OpenID Connect, BFF e SPA utilizzando la popolare stack tecnologica .NET e React. Questo approcchio può servire come base solida per i vostri futuri progetti.
Mirando al futuro, l’evoluzione continua della sicurezza web richiederà ancora maggiori innovazioni nell’autenticazione e nei pattern architetturali. Vi invitiamo a esplorare il nostro repository GitHub, a contribuire alla realizzazione di soluzioni di autenticazione moderne e a rimanere impegnati nei progressi in corso. Grazie per l’attenzione!
Source:
https://dzone.com/articles/modern-authentication-on-dotnet