Uno degli aspetti in cui ASP.NET Core ha ricevuto un'importante rivisitazione è quello relativo alla sicurezza e al flusso di autenticazione. Esso è basato su una collezione di AuthenticationHandler, ossia di classi che, a turno, ispezionano la richiesta per verificare se il suo contenuto consenta di autenticare l'utente. Nel caso questa operazione abbia successo, il risultato è la creazione di un ClaimsPrincipal nel contesto della richiesta, che può essere poi utilizzato per la successiva autorizzazione.
ASP.NET Core fornisce una serie di AuthenticationHandler che permettono di integrare facilmente diverse logiche e provider: per esempio, la classe CookieAuthenticationHandler crea un ClaimsPrincipal se è presente un determinato security cookie, mentre JwtBearerHandler ispeziona gli header alla ricerca di un bearer token valido.
Ovviamente si tratta di un sistema perfettamente estendibile, tant'è che con poco sforzo possiamo creare un nostro handler personalizzato.
Immaginiamo per esempio di voler autenticare l'accesso alla nostra API tramite una specifica chiave passata in querystring. Come prima cosa, dobbiamo definire una classe ApiKeyOptions che conterrà tutte le informazioni necessarie per configurare il nostro sistema di security con il nome della chiave e il valore atteso.
public class ApiKeyOptions : AuthenticationSchemeOptions { public string KeyName { get; set; } public string KeyValue { get; set; } }
Questa classe deve ereditare da AuthenticationSchemeOptions, che espone delle ulteriori proprietà il cui utilizzo, però, va al di là di quanto tratteremo in questo script.
A questo punto, possiamo dedicarci all'handler vero e proprio, che dovrà ereditare da AuthenticationHandler
internal class ApiKeyHandler : AuthenticationHandler<ApiKeyOptions> { public ApiKeyHandler(IOptionsMonitor<ApiKeyOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } }
Questa classe base permette di personalizzare diversi aspetti del processo di autenticazione, come ad esempio decidere cosa fare nel caso in cui l'autenticazione fallisca. Per il nostro esempio, tuttavia, è sufficiente implementare il metodo HandleAuthenticateAsync come nello snippet in basso:
protected async override Task<AuthenticateResult> HandleAuthenticateAsync() { if (!this.Context.Request.Query.ContainsKey(this.Options.KeyName)) { // nessuna key, quindi non possiamo autenticare l'utente return AuthenticateResult.NoResult(); } if (this.Context.Request.Query[this.Options.KeyName] == this.Options.KeyValue) { var claims = new List<Claim>() { new Claim(ClaimTypes.Name, "ApiUser") }; var scheme = this.Scheme.Name; var identity = new ClaimsIdentity(claims, scheme); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, this.Scheme.Name); return AuthenticateResult.Success(ticket); } return AuthenticateResult.Fail("Invalid key"); }
La logica è piuttosto semplice, ed è sostanzialmente volta a ritornare l'AuthenticateResult più appropriato. Nel nostro caso:
- Se la chiave non è presente nella querystring, restituiamo un NoResult. Questo perchè il nostro handler non è quello più idoneo per processare l'autenticazione. Se nessuno degli handler presente nella pipeline è in grado di autenticare l'utente, il contesto sarà quello di un utente anonimo.
- Se la chiave è presente e valida, costruiamo un ClaimsPrincipal con un nome fittizio e lo utilizziamo per ritornare un AuthenticateResult che indichi il successo dell'operazione. In un caso reale, magari, potremmo in realtà effettuare una ricerca su un database e associare una chiave a uno specifico utente.
- Se la chiave è presente, ma invalida, restituiremo invece un messaggio di failure per "Invalid Key".
Per rendere semplice l'utilizzo del nostro ApiKeyHandler, conviene anche costruire un opportuno extension method che ne agevoli la configurazione:
public static class ApiKeyExtensions { public const string ApiKeyScheme = "ApiKey"; public static AuthenticationBuilder AddApiKey(this AuthenticationBuilder builder, Action<ApiKeyOptions> configureOptions) { return builder.AddScheme<ApiKeyOptions, ApiKeyHandler>(ApiKeyScheme, displayName:null, configureOptions); } }
Per utilizzarlo, non dobbiamo far altro che sfruttare questo metodo nella classe Startup, come nel codice in basso
public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(ApiKeyExtensions.ApiKeyScheme) .AddApiKey(options => { options.KeyName = "secretKey"; options.KeyValue = "secretValue"; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); }
e successivamente richiedere un utente autenticato tramite l'attributo Authorize:
[Authorize] [Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { }
Commenti
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
Approfondimenti
Gestione dell'annidamento delle regole dei layer in CSS
Sfruttare GPT-4o realtime su Azure Open AI per conversazioni vocali
Collegare applicazioni server e client con .NET Aspire
Potenziare la ricerca su Cosmos DB con Full Text Search
Gestire eccezioni nei plugin di Semantic Kernel in ASP.NET Core Web API
Triggerare una pipeline su un altro repository di Azure DevOps
Utilizzare la funzione EF.Parameter per forzare la parametrizzazione di una costante con Entity Framework
Sfruttare gli embedding e la ricerca vettoriale con Azure SQL Database
Utilizzare il nuovo modello GPT-4o con Azure OpenAI
Gestire i dati con Azure Cosmos DB Data Explorer
Generare un hash con SHA-3 in .NET
Garantire la provenienza e l'integrità degli artefatti prodotti su GitHub