Autenticazione moderna su .NET: OpenID Connect, BFF, SPA

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

Shell

 

dotnet new install Abblix.Templates

Ora possiamo usare il template denominato abblix-react. Usiamolo per creare una nuova applicazione chiamata BffSample:

Shell

 

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:

Shell

 

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:

Shell

 

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:

C#

 

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:

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:

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 schema Cookies, e DefaultChallengeScheme delega l’esecuzione dell’autenticazione allo schema OpenIdConnect 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 e SignOutScheme impostano il Cookies 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 e ClientSecret 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 applicazione BffClient richiede l’accesso. In questo caso, gli standard scope openid (identificativo utente), profile (profilo utente) e email (email) sono richiesti.
    • MapInboundClaims è responsabile della trasformazione delle claim in entrata dal server OpenID Connect in claim usate nella applicazione. Un valore di false significa che le claim saranno salvate nella sessione dell’utente autenticato nella forma in cui sono ricevute dal server OpenID Connect.
    • ResponseType con il valore code 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:

C#

 

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():

C#

 

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:

Shell

 

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:

C#

 

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:

C#

 

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 e Logout 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 codice 401 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 nome CorsPolicyName 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 chiamata CheckSession ha restituito 401 Non autorizzato. Assicura che l’utente non sia ancora autenticato e inizia il processo configurato Challenge, 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 di BffSample 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:

C#

 


// ******************* 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():

C#

 

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:

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:

TypeScript

 

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, e logout. Ѐ 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:

TypeScript

 

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:

TypeScript

 

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:

Plain Text

 

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:

Shell

 

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 o badidea, a seconda della versione di Chrome in uso. Queste sequenze di tasti funzionano come comandi di bypass in Chrome, permettendovi di procedere al sito di localhost.

È 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:

Shell

 

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:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Configura il schema di autenticazione e la politica di autorizzazione denominata WeatherApi nel file ApiSample\Program.cs:

C#

 

// ******************* 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 dichiarazione scope che include il valore weather. Poiché la dichiarazione scope può contenere più valori separati da spazi secondo RFC 8693, il valore reale scope viene diviso in parti individuali, e l’array risultante viene controllato per contenere il valore richiesto weather.

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:

C#

 

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`:

JSON

 

{
  // ******************* 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 su false, cioè le dichiarazioni useranno i loro nomi originali dal JWT.
  • TokenValidationParameters:
    • ValidTypes: Impostato su at+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 client https://localhost:5004.
    • ValidIssuer: Specifica che l’applicazione accetterà token emessi dal server https://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:

C#

 

// 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:

Shell

 

dotnet add package Yarp.ReverseProxy

Successivamente, aggiungiamo i seguenti namespace all’inizio del file BffSample\Program.cs:

C#

 

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:

C#

 

builder.Services.AddHttpForwarder();

E tra le chiamate a app.MapControllerRoute() e app.MapFallbackToFile():

C#

 

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 sezione OpenIdConnect: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’applicazione ApiSample.
  • 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 HTTP Authorization 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:

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:

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:

TypeScript

 

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’interfaccia State 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 funzione fetchBff dall’hook useBff e la utilizza per recuperare i dati del tempo dal server. Lo stato del componente è gestito utilizzando l’hook useState, inizializzando con un array vuoto di previsioni e un flag di caricamento impostato a vero.
  • L’hook useEffect attiva la funzione fetchBff 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 ( tramite setState) 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:

TypeScript

 

// ******************* 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