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
Creare gruppi di client per Event Grid MQTT
Garantire la provenienza e l'integrità degli artefatti prodotti su GitHub
Definire stili a livello di libreria in Angular
Configurare lo startup di applicazioni server e client con .NET Aspire
Generare HTML a runtime a partire da un componente Razor in ASP.NET Core
Sfruttare MQTT in cloud e in edge con Azure Event Grid
Creare una libreria CSS universale: Immagini
Applicare un filtro per recuperare alcune issue di GitHub
Persistere la ChatHistory di Semantic Kernel in ASP.NET Core Web API per GPT
Bloccare l'esecuzione di un pod in mancanza di un'artifact attestation di GitHub
Eseguire una query su SQL Azure tramite un workflow di GitHub
Evitare il flickering dei componenti nel prerender di Blazor 8
I più letti di oggi
- Simulare Azure Cosmos DB in locale con Docker
- Utilizzare il metodo Index di LINQ per scorrere una lista sapendo anche l'indice dell'elemento
- ecco tutte le novità pubblicate sui nostri siti questa settimana: https://aspit.co/wkly buon week-end!
- .NET Conference Italia 2024 - Milano
- .NET Conference Italia 2023 - Milano e Online