Condividi tramite


Filtri di autenticazione nell'API Web ASP.NET 2

di Mike Wasson

Un filtro di autenticazione è un componente che autentica una richiesta HTTP. L'API Web 2 e MVC 5 supportano entrambi i filtri di autenticazione, ma differiscono leggermente, principalmente nelle convenzioni di denominazione per l'interfaccia di filtro. Questo argomento descrive i filtri di autenticazione dell'API Web.

I filtri di autenticazione consentono di impostare uno schema di autenticazione per singoli controller o azioni. In questo modo, l'app può supportare meccanismi di autenticazione diversi per diverse risorse HTTP.

In questo articolo verrà illustrato il codice dell'esempio di autenticazione di base in https://github.com/aspnet/samples. L'esempio mostra un filtro di autenticazione che implementa lo schema di autenticazione di base HTTP (RFC 2617). Il filtro viene implementato in una classe denominata IdentityBasicAuthenticationAttribute. Non mostrerò tutto il codice dell'esempio, ma solo le parti che illustrano come scrivere un filtro di autenticazione.

Impostazione di un filtro di autenticazione

Analogamente ad altri filtri, i filtri di autenticazione possono essere applicati per controller, per azione o a livello globale a tutti i controller API Web.

Per applicare un filtro di autenticazione a un controller, decorare la classe controller con l'attributo di filtro. Il codice seguente imposta il [IdentityBasicAuthentication] filtro su una classe controller, che abilita l'autenticazione di base per tutte le azioni del controller.

[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }
    public IHttpActionResult Post() { . . . }
}

Per applicare il filtro a un'azione, decorare l'azione con il filtro. Il codice seguente imposta il [IdentityBasicAuthentication] filtro sul metodo del Post controller.

[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }

    [IdentityBasicAuthentication] // Enable Basic authentication for this action.
    public IHttpActionResult Post() { . . . }
}

Per applicare il filtro a tutti i controller API Web, aggiungerlo a GlobalConfiguration.Filters.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new IdentityBasicAuthenticationAttribute());

        // Other configuration code not shown...
    }
}

Implementazione di un filtro di autenticazione api Web

Nell'API Web i filtri di autenticazione implementano l'interfaccia System.Web.Http.Filters.IAuthenticationFilter . Devono anche ereditare da System.Attribute, per poter essere applicati come attributi.

L'interfaccia IAuthenticationFilter ha due metodi:

  • AuthenticateAsync autentica la richiesta convalidando le credenziali nella richiesta, se presente.
  • ChallengeAsync aggiunge una richiesta di autenticazione alla risposta HTTP, se necessario.

Questi metodi corrispondono al flusso di autenticazione definito in RFC 2612 e RFC 2617:

  1. Il client invia le credenziali nell'intestazione Autorizzazione. Ciò si verifica in genere dopo che il client riceve una risposta 401 (non autorizzata) dal server. Tuttavia, un client può inviare credenziali con qualsiasi richiesta, non solo dopo aver ottenuto un valore 401.
  2. Se il server non accetta le credenziali, restituisce una risposta 401 (non autorizzata). La risposta include un'intestazione Www-Authenticate che contiene una o più sfide. Ogni richiesta specifica uno schema di autenticazione riconosciuto dal server.

Il server può anche restituire 401 da una richiesta anonima. Questo è in genere il modo in cui viene avviato il processo di autenticazione:

  1. Il client invia una richiesta anonima.
  2. Il server restituisce 401.
  3. I clienti inviano nuovamente la richiesta con le credenziali.

Questo flusso include sia i passaggi di autenticazione che di autorizzazione .

  • L'autenticazione dimostra l'identità del client.
  • L'autorizzazione determina se il client può accedere a una determinata risorsa.

Nell'API Web i filtri di autenticazione gestiscono l'autenticazione, ma non l'autorizzazione. L'autorizzazione deve essere eseguita da un filtro di autorizzazione o dall'interno dell'azione del controller.

Ecco il flusso nella pipeline dell'API Web 2:

  1. Prima di richiamare un'azione, l'API Web crea un elenco dei filtri di autenticazione per tale azione. Sono inclusi i filtri con ambito d'azione, ambito controller e ambito globale.
  2. L'API Web chiama AuthenticateAsync per ogni filtro nell'elenco. Ogni filtro può convalidare le credenziali nella richiesta. Se un filtro convalida correttamente le credenziali, il filtro crea un oggetto IPrincipal e lo allega alla richiesta. Un filtro può anche attivare un errore a questo punto. In tal caso, il resto della pipeline non viene eseguito.
  3. Supponendo che non sia presente alcun errore, la richiesta passa attraverso il resto della pipeline.
  4. Infine, l'API Web chiama ogni metodo ChallengeAsync del filtro di autenticazione. I filtri usano questo metodo per aggiungere una richiesta di verifica alla risposta, se necessario. In genere (ma non sempre) questo accade in risposta a un errore 401.

I diagrammi seguenti illustrano due casi possibili. Nel primo, il filtro di autenticazione autentica correttamente la richiesta, un filtro di autorizzazione autorizza la richiesta e l'azione del controller restituisce 200 (OK).

Diagramma dell'autenticazione riuscita

Nel secondo esempio il filtro di autenticazione autentica la richiesta, ma il filtro di autorizzazione restituisce 401 (non autorizzato). In questo caso, l'azione del controller non viene richiamata. Il filtro di autenticazione aggiunge un'intestazione Www-Authenticate alla risposta.

Diagramma dell'autenticazione non autorizzata

Sono possibili altre combinazioni, ad esempio se l'azione del controller consente richieste anonime, potrebbe essere presente un filtro di autenticazione ma nessuna autorizzazione.

Implementazione del metodo AuthenticateAsync

Il metodo AuthenticateAsync tenta di autenticare la richiesta. Ecco la firma del metodo:

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

Il metodo AuthenticateAsync deve eseguire una delle operazioni seguenti:

  1. Nessuna operazione (no-op).
  2. Creare un IPrincipal e impostarlo nella richiesta.
  3. Impostare un risultato di errore.

L'opzione (1) indica che la richiesta non dispone di credenziali che il filtro riconosce. L'opzione (2) indica che il filtro ha autenticato correttamente la richiesta. L'opzione (3) indica che la richiesta ha credenziali non valide (ad esempio la password errata), che attiva una risposta di errore.

Ecco una struttura generale per l'implementazione di AuthenticateAsync.

  1. Cercare le credenziali nella richiesta.
  2. Se non sono presenti credenziali, non eseguire alcuna operazione e restituire (no-op).
  3. Se sono presenti credenziali ma il filtro non riconosce lo schema di autenticazione, non eseguire alcuna operazione e restituire (no-op). Un altro filtro nella pipeline potrebbe comprendere lo schema.
  4. Se sono presenti credenziali che il filtro riconosce, provare ad autenticarle.
  5. Se le credenziali sono errate, restituire 401 configurando context.ErrorResult.
  6. Se le credenziali sono valide, creare un IPrincipal e impostare context.Principal.

Il codice seguente mostra il metodo AuthenticateAsync dell'esempio di autenticazione di base . I commenti indicano ogni passaggio. Il codice mostra diversi tipi di errore: un'intestazione di autorizzazione senza credenziali, credenziali in formato non valido e nome utente/password non valida.

public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
    // 1. Look for credentials in the request.
    HttpRequestMessage request = context.Request;
    AuthenticationHeaderValue authorization = request.Headers.Authorization;

    // 2. If there are no credentials, do nothing.
    if (authorization == null)
    {
        return;
    }

    // 3. If there are credentials but the filter does not recognize the 
    //    authentication scheme, do nothing.
    if (authorization.Scheme != "Basic")
    {
        return;
    }

    // 4. If there are credentials that the filter understands, try to validate them.
    // 5. If the credentials are bad, set the error result.
    if (String.IsNullOrEmpty(authorization.Parameter))
    {
        context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
        return;
    }

    Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
    if (userNameAndPassword == null)
    {
        context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
    }

    string userName = userNameAndPassword.Item1;
    string password = userNameAndPassword.Item2;

    IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
    if (principal == null)
    {
        context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
    }

    // 6. If the credentials are valid, set principal.
    else
    {
        context.Principal = principal;
    }

}

Impostazione di un risultato di errore

Se le credenziali non sono valide, il filtro deve impostare context.ErrorResult su IHttpActionResult che genera una risposta d'errore. Per altre informazioni su IHttpActionResult, vedere Risultati delle azioni nell'API Web 2.

L'esempio di autenticazione di base include una AuthenticationFailureResult classe adatta a questo scopo.

public class AuthenticationFailureResult : IHttpActionResult
{
    public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
    {
        ReasonPhrase = reasonPhrase;
        Request = request;
    }

    public string ReasonPhrase { get; private set; }

    public HttpRequestMessage Request { get; private set; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute());
    }

    private HttpResponseMessage Execute()
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
        response.RequestMessage = Request;
        response.ReasonPhrase = ReasonPhrase;
        return response;
    }
}

Implementazione di ChallengeAsync

Lo scopo del metodo ChallengeAsync è aggiungere sfide di autenticazione alla risposta, se necessario. Ecco la firma del metodo:

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

Il metodo viene chiamato su ogni filtro di autenticazione nella pipeline di richiesta.

È importante comprendere che ChallengeAsync viene chiamato prima della creazione della risposta HTTP e possibilmente anche prima dell'esecuzione dell'azione del controller. Quando viene chiamato ChallengeAsync , context.Result contiene un oggetto IHttpActionResult, che viene usato in un secondo momento per creare la risposta HTTP. Quindi, quando viene chiamato ChallengeAsync , non si conosce ancora nulla sulla risposta HTTP. Il metodo ChallengeAsync deve sostituire il valore originale di context.Result con un nuovo oggetto IHttpActionResult. Questo IHttpActionResult deve incapsulare l'originale context.Result.

Diagramma di ChallengeAsync

Chiamerò l'originale IHttpActionResult il risultato interno e il nuovo IHttpActionResult il risultato esterno. Il risultato esterno deve eseguire le operazioni seguenti:

  1. Richiamare il risultato interno per creare la risposta HTTP.
  2. Esaminare la risposta.
  3. Aggiungere una richiesta di autenticazione alla risposta, se necessario.

L'esempio seguente è tratto dall'esempio di autenticazione di base. Definisce un oggetto IHttpActionResult per il risultato esterno.

public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
    public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
    {
        Challenge = challenge;
        InnerResult = innerResult;
    }

    public AuthenticationHeaderValue Challenge { get; private set; }

    public IHttpActionResult InnerResult { get; private set; }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            // Only add one challenge per authentication scheme.
            if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
            {
                response.Headers.WwwAuthenticate.Add(Challenge);
            }
        }

        return response;
    }
}

La InnerResult proprietà contiene l'oggetto IHttpActionResult interno. La Challenge proprietà rappresenta un'intestazione Www-Authentication. Si noti che ExecuteAsync prima chiama InnerResult.ExecuteAsync per creare la risposta HTTP e quindi aggiunge la richiesta, se necessario.

Controllare il codice di risposta prima di aggiungere la richiesta di verifica. La maggior parte degli schemi di autenticazione aggiunge una richiesta solo se la risposta è 401, come illustrato di seguito. Tuttavia, alcuni schemi di autenticazione aggiungono una sfida a una risposta riuscita. Ad esempio, vedere Negotiate (RFC 4559).

Data la AddChallengeOnUnauthorizedResult classe , il codice effettivo in ChallengeAsync è semplice. È sufficiente creare il risultato e collegarlo a context.Result.

public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
    var challenge = new AuthenticationHeaderValue("Basic");
    context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
    return Task.FromResult(0);
}

Nota: l'esempio di autenticazione di base astrae leggermente questa logica inserendola in un metodo di estensione.

Combinazione di filtri di autenticazione con autenticazione a livello di host

L'autenticazione a livello di host viene eseguita dall'host (ad esempio IIS), prima che la richiesta raggiunga il framework API Web.

Spesso, è possibile abilitare l'autenticazione a livello di host per il resto dell'applicazione, ma disabilitarla per i controller API Web. Ad esempio, uno scenario tipico consiste nell'abilitare l'autenticazione basata su form a livello di host, ma usare l'autenticazione basata su token per l'API Web.

Per disabilitare l'autenticazione a livello di host all'interno della pipeline dell'API Web, chiamare config.SuppressHostPrincipal() nella configurazione. In questo modo, l'API Web rimuove iPrincipal da qualsiasi richiesta che entra nella pipeline dell'API Web. In effetti, "annulla l'autenticazione" della richiesta.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.SuppressHostPrincipal();

        // Other configuration code not shown...
    }
}

Risorse aggiuntive

filtri di sicurezza dell'API Web ASP.NET (MSDN Magazine)