Filtros de autenticación en ASP.NET Web API 2

de Mike Wasson

Un filtro de autenticación es un componente que autentica una solicitud HTTP. La API web 2 y MVC 5 admiten filtros de autenticación, pero difieren ligeramente, principalmente en las convenciones de nomenclatura de la interfaz de filtro. En este tema se describen los filtros de autenticación de api web.

Los filtros de autenticación permiten establecer un esquema de autenticación para controladores o acciones individuales. De este modo, la aplicación puede admitir diferentes mecanismos de autenticación para distintos recursos HTTP.

En este artículo, mostraré el código del ejemplo de autenticación básica en https://github.com/aspnet/samples. En el ejemplo se muestra un filtro de autenticación que implementa el esquema de autenticación de acceso básico HTTP (RFC 2617). El filtro se implementa en una clase denominada IdentityBasicAuthenticationAttribute. No mostraré todo el código del ejemplo, solo las partes que ilustran cómo escribir un filtro de autenticación.

Establecimiento de un filtro de autenticación

Al igual que otros filtros, los filtros de autenticación se pueden aplicar por controlador, por acción o globalmente a todos los controladores de API web.

Para aplicar un filtro de autenticación a un controlador, decora la clase de controlador con el atributo filter. El código siguiente establece el [IdentityBasicAuthentication] filtro en una clase de controlador, que habilita la autenticación básica para todas las acciones del controlador.

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

Para aplicar el filtro a una acción, decora la acción con el filtro. El código siguiente establece el [IdentityBasicAuthentication] filtro en el método del Post controlador.

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

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

Para aplicar el filtro a todos los controladores de API web, agréguelo a GlobalConfiguration.Filters.

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

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

Implementación de un filtro de autenticación de API web

En la API web, los filtros de autenticación implementan la interfaz System.Web.Http.Filters.IAuthenticationFilter . También deben heredar de System.Attribute para que se apliquen como atributos.

La interfaz IAuthenticationFilter tiene dos métodos:

  • AuthenticateAsync autentica la solicitud validando las credenciales en la solicitud, si está presente.
  • ChallengeAsync agrega un desafío de autenticación a la respuesta HTTP, si es necesario.

Estos métodos corresponden al flujo de autenticación definido en RFC 2612 y RFC 2617:

  1. El cliente envía credenciales en el encabezado Autorización. Esto suele ocurrir después de que el cliente reciba una respuesta 401 (no autorizada) del servidor. Sin embargo, un cliente puede enviar credenciales con cualquier solicitud, no solo después de obtener un 401.
  2. Si el servidor no acepta las credenciales, devuelve una respuesta 401 (no autorizada). La respuesta incluye un encabezado Www-Authenticate que contiene uno o varios desafíos. Cada desafío especifica un esquema de autenticación reconocido por el servidor.

El servidor también puede devolver 401 desde una solicitud anónima. De hecho, es normalmente cómo se inicia el proceso de autenticación:

  1. El cliente envía una solicitud anónima.
  2. El servidor devuelve 401.
  3. El cliente vuelve a enviar la solicitud con sus credenciales.

Este flujo incluye los pasos de autenticación y autorización .

  • La autenticación demuestra la identidad del cliente.
  • La autorización determina si el cliente puede acceder a un recurso determinado.

En la API web, los filtros de autenticación controlan la autenticación, pero no la autorización. La autorización debe realizarse mediante un filtro de autorización o dentro de la acción del controlador.

Este es el flujo del pipeline de la Web API 2:

  1. Antes de invocar una acción, la API web crea una lista de los filtros de autenticación de esa acción. Esto incluye filtros con ámbito de acción, ámbito de controlador y ámbito global.
  2. La API web llama a AuthenticateAsync en cada filtro de la lista. Cada filtro puede validar las credenciales en la solicitud. Si algún filtro valida correctamente las credenciales, el filtro crea un IPrincipal y lo adjunta a la solicitud. Un filtro también puede desencadenar un error en este momento. Si es así, el resto de la canalización no se ejecuta.
  3. Suponiendo que no hay ningún error, la solicitud fluye a través del resto de la canalización.
  4. Por último, la API web llama al método ChallengeAsync de cada filtro de autenticación. Los filtros usan este método para agregar un desafío a la respuesta, si es necesario. Normalmente (pero no siempre) que ocurriría en respuesta a un error 401.

En los diagramas siguientes se muestran dos casos posibles. En la primera, el filtro de autenticación autentica correctamente la solicitud, un filtro de autorización autoriza la solicitud y la acción del controlador devuelve 200 (Correcto).

Diagrama de autenticación correcta

En el segundo ejemplo, el filtro de autenticación autentica la solicitud, pero el filtro de autorización devuelve 401 (no autorizado). En este caso, no se invoca la acción del controlador. El filtro de autenticación agrega un encabezado Www-Authenticate a la respuesta.

Diagrama de autenticación no autorizada

Otras combinaciones son posibles; por ejemplo, si la acción del controlador permite solicitudes anónimas, es posible que tenga un filtro de autenticación pero sin autorización.

Implementación del método AuthenticateAsync

El método AuthenticateAsync intenta autenticar la solicitud. Esta es la firma del método:

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

El método AuthenticateAsync debe realizar una de las siguientes acciones:

  1. Nada (no-op).
  2. Cree un IPrincipal y asígnelo a la solicitud.
  3. Establezca un resultado de error.

La opción (1) significa que la solicitud no tenía credenciales que comprenda el filtro. La opción (2) significa que el filtro ha autenticado correctamente la solicitud. La opción (3) significa que la solicitud tenía credenciales no válidas (como la contraseña incorrecta), que desencadena una respuesta de error.

Este es un esquema general para implementar AuthenticateAsync.

  1. Busque credenciales en la solicitud.
  2. Si no hay credenciales, no hacer nada y retornar (no-op).
  3. Si hay credenciales, pero el filtro no reconoce el esquema de autenticación, no haga nada y devuelva (no-op). Otro filtro de la canalización podría comprender el esquema.
  4. Si hay credenciales que entiende el filtro, intente autenticarlos.
  5. Si las credenciales son incorrectas, devuelva 401 estableciendo context.ErrorResult.
  6. Si las credenciales son válidas, cree un IPrincipal y establezca context.Principal.

El código siguiente muestra el método AuthenticateAsync del ejemplo de autenticación básica . Los comentarios indican cada paso. El código muestra varios tipos de error: un encabezado authorization sin credenciales, credenciales con formato incorrecto y un nombre de usuario o contraseña incorrectos.

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

}

Establecer un resultado de error

Si las credenciales no son válidas, el filtro debe establecer context.ErrorResult en un IHttpActionResult que cree una respuesta de error. Para obtener más información sobre IHttpActionResult, vea Resultados de la acción en Web API 2.

El ejemplo de autenticación básica incluye una AuthenticationFailureResult clase que es adecuada para este propósito.

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

Implementación de ChallengeAsync

El propósito del método ChallengeAsync es agregar desafíos de autenticación a la respuesta, si es necesario. Esta es la firma del método:

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

Se llama al método en cada filtro de autenticación de la canalización de solicitudes.

Es importante comprender que ChallengeAsync se llama antes de crear la respuesta HTTP y, posiblemente, incluso antes de que se ejecute la acción del controlador. Cuando se llama a ChallengeAsync, context.Result contiene un IHttpActionResult, que se usa más adelante para crear la respuesta HTTP. Por lo tanto, cuando se invoca ChallengeAsync, todavía no se sabe nada sobre la respuesta HTTP. El método ChallengeAsync debe reemplazar el valor original de context.Result por un nuevo IHttpActionResult. Este IHttpActionResult debe encapsular el original context.Result.

Diagrama de ChallengeAsync

Llamaré al IHttpActionResult original el resultado interno y el nuevo IHttpActionResult el resultado externo. El resultado externo debe hacer lo siguiente:

  1. Invoque el resultado interno para crear la respuesta HTTP.
  2. Examine la respuesta.
  3. Agregue un desafío de autenticación a la respuesta, si es necesario.

El ejemplo siguiente se toma del ejemplo de autenticación básica. Define un IHttpActionResult para el resultado externo.

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 propiedad contiene el IHttpActionResult interno. La Challenge propiedad representa un encabezado WWW-Authentication. Observe que ExecuteAsync llama InnerResult.ExecuteAsync primero a para crear la respuesta HTTP y, a continuación, agrega el desafío si es necesario.

Compruebe el código de respuesta antes de agregar el desafío. La mayoría de los esquemas de autenticación solo agregan un desafío si la respuesta es 401, como se muestra aquí. Sin embargo, algunos esquemas de autenticación añaden un desafío a una respuesta exitosa. Por ejemplo, vea Negotiate (RFC 4559).

Dada la AddChallengeOnUnauthorizedResult clase , el código real de ChallengeAsync es sencillo. Solo tiene que crear el resultado y adjuntarlo 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: El ejemplo de autenticación básica abstrae esta lógica un poco, colocándolo en un método de extensión.

Combinación de filtros de autenticación con autenticación de Host-Level

La autenticación de nivel de host es la autenticación realizada por el host (por ejemplo, IIS), antes de que la solicitud llegue al marco de api web.

A menudo, es posible que desee habilitar la autenticación de nivel de host para el resto de la aplicación, pero deshabilitarla para los controladores de API web. Por ejemplo, un escenario típico es habilitar la autenticación de formularios en el nivel de host, pero usar la autenticación basada en tokens para la API web.

Para deshabilitar la autenticación de nivel de host en la canalización de API Web, llame a config.SuppressHostPrincipal() en la configuración. Esto hace que la API Web elimine el IPrincipal de todas las solicitudes que ingresan en el flujo de la API Web. De hecho, "anula la autenticación" de la solicitud.

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

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

Recursos adicionales

Filtros de seguridad de api web de ASP.NET (MSDN Magazine)