Filtros de autenticación en ASP.NET Web API 2
Por Mike Wasson
Un filtro de autenticación es un componente que autentica una solicitud HTTP. Web API 2 y MVC 5 admiten filtros de autenticación, pero difieren ligeramente, principalmente en las convenciones de nomenclatura de la interfaz del filtro. En este tema, se describen los filtros de autenticación de Web API.
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 deBasic Authentication de 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ásica 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 Web API.
Para aplicar un filtro de autenticación a un controlador, decora la clase del controlador con el atributo filter. El código siguiente establece el filtro [IdentityBasicAuthentication]
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 filtro [IdentityBasicAuthentication]
en el método Post
del 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 Web API, 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 Web API
En Web API, 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án presentes.
- 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:
- El cliente envía credenciales en el encabezado Autorización. Esto suele ocurrir después de que el cliente recibe una respuesta 401 (Unauthorized) del servidor. Sin embargo, un cliente puede enviar credenciales con cualquier solicitud, no solo después de obtener un error 401.
- Si el servidor no acepta las credenciales, devuelve una respuesta 401 (Unauthorized). 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 un código 401 por una solicitud anónima. De hecho, así es normalmente cómo se inicia el proceso de autenticación:
- El cliente envía una solicitud anónima.
- El servidor devuelve un error 401.
- Los clientes vuelven a enviar la solicitud con credenciales.
Este flujo incluye los pasos de autenticación y autorización.
- La autenticación prueba la identidad del cliente.
- La autorización determina si el cliente puede acceder a un recurso determinado.
En Web API, 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 de la canalización de Web API 2:
- Antes de invocar una acción, Web API crea una lista de los filtros de autenticación para esa acción. Esto incluye filtros con ámbito global, de acción y de controlador.
- Web API 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.
- Suponiendo que no haya ningún error, la solicitud fluye a través del resto de la canalización.
- Por último, Web API llama al método ChallengeAsync de todos los filtros de autenticación. Los filtros usan este método para agregar un desafío a la respuesta, si es necesario. Normalmente (pero no siempre), eso ocurriría en respuesta a un error 401.
En los diagramas siguientes, se muestran dos casos posibles. En el primero, el filtro de autenticación autentica correctamente la solicitud, un filtro de autorización autoriza la solicitud y la acción del controlador devuelve el código 200 (OK).
En el segundo ejemplo, el filtro de autenticación autentica la solicitud, pero el filtro de autorización devuelve el error 401 (Unauthorized). En ese caso, no se invoca la acción del controlador. El filtro de autenticación agrega un encabezado Www-Authenticate a la respuesta.
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:
- Ninguna (sin operación)
- Crear una IPrincipal y establecerla en la solicitud
- Establecer un resultado de error
La opción (1) significa que la solicitud no tenía credenciales que el filtro pueda comprender. 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), lo que desencadena una respuesta de error.
El siguiente es un esquema general para implementar AuthenticateAsync.
- Busque credenciales en la solicitud.
- Si no hay credenciales, no haga nada y continúe con “return” (sin operación).
- Si hay credenciales, pero el filtro no reconoce el esquema de autenticación, no haga nada y continúe con “return” (sin operación). Otro filtro de la canalización podría comprender el esquema.
- Si hay credenciales que el filtro comprenda, intente autenticarlas.
- Si las credenciales son incorrectas, devuelva 401 estableciendo
context.ErrorResult
. - Si las credenciales son válidas, cree una IPrincipal y establezca
context.Principal
.
El código siguiente muestra el método AuthenticateAsync del ejemplo de Basic Authentication. Los comentarios indican cada paso. El código muestra varios tipos de error: un encabezado de autorización 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;
}
}
Establecimiento de un resultado de error
Si las credenciales no son válidas, el filtro debe establecer context.ErrorResult
en una IHttpActionResult que cree una respuesta de error. Para obtener más información sobre IHttpActionResult, consulte Action Results en Web API 2.
El ejemplo de Basic Authentication incluye una clase AuthenticationFailureResult
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 importar comprender que se llama a ChallengeAsyncantes de que se cree la respuesta HTTP y, posiblemente, incluso antes de que se ejecute la acción del controlador. Cuando se llama a ChallengeAsync, context.Result
contiene una IHttpActionResult, que se usa más adelante para crear la respuesta HTTP. Por lo tanto, cuando se llama a ChallengeAsync, todavía no se sabe nada sobre la respuesta HTTP. El método ChallengeAsync debe reemplazar al valor original de context.Result
con una nueva IHttpActionResult. Esta IHttpActionResult debe encapsular al context.Result
original.
Llamaré a la IHttpActionResult original el inner result (resultado interno), y a la nueva IHttpActionResult el outer result (resultado externo). El resultado externo debe hacer lo siguiente:
- Invocar el resultado interno para crear la respuesta HTTP
- Examine la respuesta.
- Agregar un desafío de autenticación a la respuesta, si es necesario
El ejemplo siguiente se toma del ejemplo de Basic Authentication. Define una IHttpActionResult para “outer result”.
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 propiedad InnerResult
tiene la IHttpActionResult interna. La propiedad Challenge
representa un encabezado Www-Authentication. Observe que ExecuteAsync primero llama a InnerResult.ExecuteAsync
para crear la respuesta HTTP y, luego, 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 agregan un desafío a una respuesta correcta. Por ejemplo, consulte Negotiate (RFC 4559).
Dada la clase AddChallengeOnUnauthorizedResult
, 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 Basic Authentication 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 nivel de host
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 Web API.
A menudo, es posible que deba habilitar la autenticación de nivel de host para el resto de la aplicación, pero deshabilitarla para los controladores de Web API. 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 Web API.
Para deshabilitar la autenticación de nivel de host dentro de la canalización de Web API, llame a config.SuppressHostPrincipal()
en la configuración. Esto hace que Web API quite la IPrincipal de cualquier solicitud que entre en la canalización de Web API. 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 ASP.NET Web API (MSDN Magazine)