Share via


Filtri di autenticazione in 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. In questo argomento vengono descritti 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 accesso di base HTTP (RFC 2617). Il filtro viene implementato in una classe denominata IdentityBasicAuthenticationAttribute. Non visualizzerò 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 dell'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 Authorization. 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 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ù problematiche. Ogni richiesta specifica uno schema di autenticazione riconosciuto dal server.

Il server può anche restituire 401 da una richiesta anonima. In realtà, 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 client invia 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 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 IPrincipal e lo associa 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 vi sia alcun errore, la richiesta scorre attraverso il resto della pipeline.
  4. Infine, l'API Web chiama il metodo ChallengeAsync di ogni filtro di autenticazione. I filtri usano questo metodo per aggiungere una richiesta di verifica alla risposta, se necessario. In genere (ma non sempre) che si verifica 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. Niente (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 negative, restituire 401 impostando 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 valido.

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 errore

Se le credenziali non sono valide, il filtro deve essere impostato su context.ErrorResultun oggetto IHttpActionResult che crea una risposta di 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 le sfide di autenticazione alla risposta, se necessario. Ecco la firma del metodo:

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

Il metodo viene chiamato per 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 della risposta HTTP. Il metodo ChallengeAsync deve sostituire il valore originale di context.Result con un nuovo oggetto IHttpActionResult. Questo oggetto IHttpActionResult deve eseguire il wrapping dell'oggetto originale context.Result.

Diagramma di ChallengeAsync

Chiamerò l'oggetto IHttpActionResult originale 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 viene 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 di Www-Authentication. Si noti che ExecuteAsync prima chiama InnerResult.ExecuteAsync per creare la risposta HTTP e quindi aggiunge la sfida se necessario.

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

Dato la classe, il AddChallengeOnUnauthorizedResult 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 questa logica, inserendola in un metodo di estensione.

Combinazione di filtri di autenticazione con autenticazione Host-Level

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 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 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

API Web ASP.NET Filtri di sicurezza (MSDN Magazine)