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
La gestione della riconnessione al server di Blazor in .NET 9
Usare le navigation property in QuickGrid di Blazor
Usare il colore CSS per migliorare lo stile della pagina
.NET Aspire per applicazioni distribuite
Documentare i servizi REST con Swagger e OpenAPI con .NET 9
Effettuare il log delle chiamate a function di GPT in ASP.NET Web API
Configurare lo startup di applicazioni server e client con .NET Aspire
Conoscere il rendering Server o WebAssembly a runtime in Blazor
Creare una custom property in GitHub
Estrarre dati randomici da una lista di oggetti in C#
Utilizzare il metodo IntersectBy per eseguire l'intersection di due liste
Sfruttare GPT-4o realtime su Azure Open AI per conversazioni vocali