Site icon Ryadel

Autenticazione multi-schema in ASP.NET Core: OIDC, Google OAuth e API keys

Zeus Malware (and modern variants) what it is and how to prevent it

In molte applicazioni ASP.NET Core, soprattutto quando il progetto cresce, l’idea di usare un unico schema di autenticazione smette presto di essere sufficiente. Gli utenti interattivi entrano magari con OpenID Connect tramite Microsoft Entra ID, alcuni collaboratori esterni usano Google OAuth, mentre i client server-to-server o gli script automatici accedono con API key. Finché questi casi restano pochi, si tende a gestirli in modo separato e un po’ artigianale; il problema è che, a un certo punto, iniziano a intrecciarsi con autorizzazione, mapping delle claims, policy di accesso e logica applicativa.

È qui che l’autenticazione multi-schema in ASP.NET Core diventa interessante sul serio. Il framework offre già quasi tutto quello che serve, a patto di impostare bene l’architettura: schemi distinti ma coerenti, selezione runtime quando necessario, claims normalizzate, policy di autorizzazione che riflettano davvero i casi d’uso applicativi, e una separazione chiara tra autenticazione e autorizzazione.

In questo articolo vediamo come costruire un impianto del genere in modo pulito, combinando OIDC, OAuth e API keys, aggiungendo anche alcuni tasselli che nei progetti reali fanno spesso la differenza: AddPolicyScheme per scegliere lo schema a runtime, claims transformation, authorization requirement handlers, uso programmatico di IAuthorizationService.AuthorizeAsync() e gestione del refresh token flow.

Autenticazione vs Autorizzazione

Vale la pena ribadirlo subito, perché è uno di quegli equivoci che continuano a creare architetture confuse. L’autenticazione risponde alla domanda “chi sei?”, l’autorizzazione risponde alla domanda “cosa puoi fare?”. In un sistema multi-schema questa distinzione è ancora più importante, perché lo stesso utente o client può arrivare da canali diversi, ma poi deve essere valutato secondo regole comuni.

Un’applicazione ben progettata può delegare l’identità a provider esterni, usare cookie locali per mantenere la sessione interattiva, accettare API key per chiamate machine-to-machine e poi applicare policy di autorizzazione uniformi basate su ruoli, permessi, tenant, piano sottoscritto o altre caratteristiche di dominio.

Uno scenario tipico

Un modello molto comune è questo:

  • OpenID Connect come schema principale per il login interattivo, ad esempio tramite Microsoft Entra ID;
  • un provider OAuth come schema secondario, come Google, per utenti esterni o collaboratori che non appartengono al tenant principale;
  • API key per integrazioni backend, job automatici, CLI, agent o client non interattivi.

È un’impostazione sensata perché riflette tre bisogni diversi. Gli utenti umani beneficiano del login federato e della sessione gestita dal cookie; gli utenti esterni possono autenticarsi con un provider differente; i sistemi automatici non hanno bisogno di redirect browser, consenso visuale o sessioni interattive, quindi l’API key resta una soluzione pratica, purché venga implementata bene.

Multi-Scheme Auth in ASP.NET Core

ASP.NET Core permette di registrare più schemi contemporaneamente. La parte delicata non è tanto l’aggiunta dei provider, quanto decidere qual è lo schema di default e quando invece conviene selezionarlo dinamicamente.

In questa configurazione ci sono già quattro pezzi distinti: cookie per la sessione locale, OIDC per il challenge principale, Google come schema alternativo e un handler custom per le API key. La vera domanda, a questo punto, è: chi decide quale schema usare per autenticare la richiesta corrente?

AddPolicyScheme: scegliere lo schema a runtime

Qui entra in gioco AddPolicyScheme, che nei sistemi ibridi è spesso la scelta più elegante. Invece di fissare uno schema statico per tutte le richieste, si definisce uno schema “intelligente” che inoltra la richiesta allo schema corretto in base al contesto. In pratica, si lascia che sia il runtime a capire se la richiesta sta arrivando da un browser autenticato via cookie, da un client con header Authorization: ApiKey ..., o magari da un altro schema ancora.

Questo approccio evita parecchi controller pieni di logica condizionale o attributi duplicati. È utile soprattutto quando browser e client automatici convivono sulla stessa applicazione o sulla stessa superficie HTTP. Naturalmente la selezione runtime va progettata con attenzione: la regola deve essere semplice, prevedibile e difficile da ambiguitare.

Se prevedi anche bearer token JWT per API esterne, AddPolicyScheme diventa ancora più utile, perché può instradare verso schema cookie, API key o JWT in base al prefisso dell’header Authorization.

OIDC e OAuth: come "federare" l’identità

Per gli utenti interattivi, OpenID Connect resta la scelta più naturale. In ASP.NET Core viene spesso usato insieme a un cookie locale: il provider esterno autentica l’utente, il middleware valida il token ricevuto e poi materializza la sessione nell’applicazione sotto forma di cookie. È un modello collaudato e molto comodo per applicazioni MVC, Razor Pages e anche per molte web app ibride.

Usare un provider secondario, come Google, è altrettanto semplice dal punto di vista tecnico; la complessità vera arriva quando i dati identitari non sono omogenei. Il subject identifier cambia, le claims disponibili cambiano, il formato di certi attributi cambia, e a volte cambia perfino il modo in cui vengono valorizzati email, nome e gruppi. Ecco perché conviene ragionare presto sulla normalizzazione delle claims.

Claims transformation: uniformare i dati di provider diversi

Quando l’app riceve principal provenienti da provider diversi, una delle cose più utili che si possano fare è trasformare o arricchire le claims in un punto centralizzato. Senza questo passaggio, il rischio è ritrovarsi policy e controller pieni di condizioni ad hoc del tipo “se arriva da Entra usa questa claim, se arriva da Google usa quell’altra”. È una strada che diventa ingestibile in fretta.

IClaimsTransformation permette di intercettare il principal autenticato e produrre una versione più coerente per il resto dell’applicazione. È il posto giusto per normalizzare ruoli, mappare claim custom, aggiungere tenant context, copiare identificatori in tipi standardizzati o trasformare informazioni esterne in permessi applicativi interni.

Questa fase è anche un buon punto per collegare identità esterne a un modello locale, per esempio una allowlist, ruoli applicativi persistiti su database, appartenenza a tenant o permessi granulari. È una scelta che nei progetti enterprise tende a pagare molto più di quanto sembri all’inizio.

API key authentication

Le API key continuano a essere una soluzione pratica per i client non interattivi, ma solo se vengono trattate con lo stesso rigore che useremmo per qualsiasi altro materiale sensibile. La cosa da evitare è il classico approccio pigro: chiave generata una volta, salvata in chiaro, confrontata in stringa, senza scope, rotazione o revoca.

Una buona implementazione dovrebbe almeno prevedere:

  • chiavi generate in modo robusto e mostrate in chiaro una sola volta;
  • persistenza come hash con salt, non in plaintext;
  • scope o permessi associati alla chiave;
  • revoca immediata e, meglio ancora, scadenza o rotazione;
  • audit degli utilizzi e dei fallimenti rilevanti.

Un handler custom può leggere l’header Authorization: ApiKey {key}, validare la chiave e costruire un ClaimsPrincipal coerente con il resto dell’applicazione:

Autorizzazione: Policy e Requirement Handlers

Una volta autenticata la richiesta, inizia la parte che interessa di più al dominio: capire se quel principal può eseguire l’operazione richiesta. Per i casi banali bastano ruoli o claim checks direttamente nelle policy, ma appena entrano in gioco tenant, ownership, piani di servizio, permessi composti o regole contestuali, conviene passare ai requirement handlers.

Un requirement custom permette di esprimere una regola di business in modo leggibile e riusabile. È molto meglio di spargere User.HasClaim(...) e User.IsInRole(...) in giro per controller, page model e servizi.

Questo approccio scala bene anche quando i permessi non arrivano direttamente dal token o dalla API key, ma devono essere risolti in base a dati applicativi. In quel caso l’handler può consultare servizi di dominio, database o tenant context, purché lo faccia in modo efficiente e senza trasformare ogni richiesta in una catena di query costose.

IAuthorizationService.AuthorizeAsync

Gli attributi [Authorize] restano comodissimi, ma non coprono tutto. Appena la decisione dipende da una risorsa concreta, da un record caricato a runtime o da una logica applicativa più articolata, conviene usare IAuthorizationService.AuthorizeAsync() in modo programmatico.

È il caso tipico delle policy resource-based: un utente può modificare un documento solo se appartiene al tenant corretto, oppure se è owner della risorsa, oppure se possiede un certo permesso amministrativo.

Questa forma programmatica è spesso più espressiva e più corretta degli attributi quando la risorsa da proteggere non è nota a compile time.

Refresh token: serve davvero?

Il tema dei refresh token merita un po’ di chiarezza, perché viene spesso evocato anche in scenari dove non serve. In una classica web app ASP.NET Core con login OIDC e cookie locale, spesso è il cookie a rappresentare la sessione applicativa, mentre i token del provider restano dietro le quinte. In questo caso il refresh token può servire se l’app deve chiamare API esterne per conto dell’utente in modo continuativo, non semplicemente per mantenere il login locale.

Se invece stai costruendo una SPA o un client che lavora direttamente con access token e refresh token, allora il flow diventa centrale e va trattato con molta attenzione: storage sicuro, scadenze, revoca, rotazione e protezione dai furti.

Nel contesto OIDC server-side, abilitare il salvataggio dei token è spesso il primo passo:

La scope offline_access è in genere quella che abilita l’emissione del refresh token, se il provider la supporta e la configurazione lato IdP lo consente. Da lì in avanti, però, il punto non è “avere il refresh token”, ma gestirlo correttamente. Se va conservato lato server, deve essere protetto come qualsiasi altro secret sensibile; se viene usato per ottenere nuovi access token verso API downstream, è bene centralizzare la logica di refresh e gestire gli errori in modo prevedibile.

In molti progetti enterprise conviene evitare implementazioni manuali troppo creative e appoggiarsi alle librerie del provider o a componenti già pensati per token caching e renewal. Il refresh token è uno di quei punti in cui una semplificazione ingenua può trasformarsi rapidamente in un problema di sicurezza serio.

Claims, ruoli locali e modello di autorizzazione

Un sistema multi-schema funziona davvero bene quando le identità esterne vengono ricondotte a un modello locale chiaro. In altre parole, autenticarsi con Entra, Google o API key non dovrebbe cambiare il modo in cui il dominio ragiona su ruoli, permessi, tenant scope e abilitazioni operative. Cambia l’origine dell’identità, non la logica di accesso.

È spesso una buona idea mantenere in locale almeno:

  • una allowlist degli utenti o client autorizzati a entrare davvero nel sistema;
  • ruoli applicativi distinti da quelli eventualmente forniti dal provider esterno;
  • membership o scope legati al tenant corrente;
  • audit uniforme per login, accessi negati, uso di chiavi, revoche e operazioni sensibili.

Questa separazione evita di legare troppo il modello autorizzativo alle particolarità del provider identitario scelto in quel momento.

Sign-out, revoca e lifecycle delle credenziali

In un sistema con più schemi di autenticazione, anche il sign-out e la revoca vanno pensati bene. Per le sessioni browser, di solito bisogna chiudere sia il cookie locale sia, quando opportuno, la sessione presso il provider OIDC. Per le API key il concetto è diverso: non c’è sign-out, ma revoca, rotazione, scadenza e audit degli utilizzi successivi.

La tentazione di trattare tutti i canali come equivalenti è comprensibile, ma non funziona. Ogni schema ha un lifecycle diverso, e il codice dovrebbe rifletterlo in modo esplicito.

Conclusioni

Quando si disegna un sistema di autenticazione multi-schema, la cosa più utile che si possa fare è resistere alla voglia di inventare troppo. ASP.NET Core offre già i mattoni giusti per costruire il proprio identity system: provider esterni per OIDC e OAuth, cookie authentication, handler custom, policy scheme, claims transformation, authorization handlers e autorizzazione programmatica. Il vero lavoro sta nel combinarli senza confondere i livelli.

Uno schema ragionevole, nella maggior parte dei casi, è questo: provider esterni per l’identità, cookie per la sessione utente interattiva, API key per i client automatici; il tutto orchestrato da un sistema di claims gestite trasformate in un formato coerente, e relative policy applicative locali per decidere chi può fare cosa. Non è l’unico modello possibile, ma è uno di quelli che reggono meglio quando l’applicazione cresce.

Combinare OIDC, OAuth e API keys in ASP.NET Core non è particolarmente difficile sul piano tecnico; la difficoltà vera sta nel farlo senza introdurre incoerenze tra autenticazione, claims, policy di accesso e logica di dominio. AddPolicyScheme aiuta a scegliere lo schema corretto a runtime, IClaimsTransformation permette di normalizzare identità provenienti da fonti diverse, i requirement handlers rendono l’autorizzazione più espressiva, mentre IAuthorizationService.AuthorizeAsync() copre quei casi in cui le decisioni dipendono da risorse concrete e non da semplici attributi statici.

Come spesso accade in ambito security, il problema non è tanto quello di mettere insieme più componenti, ma impedire che nel tempo si accumulino eccezioni, shortcut e regole implicite difficili da governare: un impianto multi-schema ben progettato, come quello quello che abbiamo presentato in questo articolo, serve esattamente a evitare questo.

Riferimenti

Exit mobile version