Supportare l'autenticazione basata su API Key in ASP.NET Core

di Marco De Sanctis, in ASP.NET Core,

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

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi