Filtros de autenticação no ASP.NET Web API 2

por Mike Wasson

Um filtro de autenticação é um componente que autentica uma solicitação HTTP. A API Web 2 e o MVC 5 dão suporte a filtros de autenticação, mas diferem ligeiramente, principalmente nas convenções de nomenclatura para a interface de filtro. Este tópico descreve os filtros de autenticação da API Web.

Os filtros de autenticação permitem definir um esquema de autenticação para controladores ou ações individuais. Dessa forma, seu aplicativo pode dar suporte a diferentes mecanismos de autenticação para diferentes recursos HTTP.

Neste artigo, mostrarei o código do exemplo de Autenticação Básica em https://github.com/aspnet/samples. O exemplo mostra um filtro de autenticação que implementa o esquema de Autenticação de Acesso Básico HTTP (RFC 2617). O filtro é implementado em uma classe chamada IdentityBasicAuthenticationAttribute. Não mostrarei todo o código do exemplo, apenas as partes que ilustram como escrever um filtro de autenticação.

Definindo um filtro de autenticação

Assim como outros filtros, os filtros de autenticação podem ser aplicados por controlador, por ação ou globalmente a todos os controladores de API Web.

Para aplicar um filtro de autenticação a um controlador, decore a classe do controlador com o atributo de filtro. O código a seguir define o [IdentityBasicAuthentication] filtro em uma classe de controlador, que habilita a Autenticação Básica para todas as ações do controlador.

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

Para aplicar o filtro a uma ação, decore a ação com o filtro. O código a seguir define o [IdentityBasicAuthentication] filtro no método do 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 o filtro a todos os controladores de API Web, adicione-o a GlobalConfiguration.Filters.

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

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

Implementando um filtro de autenticação de API Web

Na API Web, os filtros de autenticação implementam a interface System.Web.Http.Filters.IAuthenticationFilter . Eles também devem herdar de System.Attribute para serem aplicados como atributos.

A interface IAuthenticationFilter tem dois métodos:

  • AuthenticateAsync autentica a solicitação validando credenciais na solicitação, se presente.
  • ChallengeAsync adiciona um desafio de autenticação à resposta HTTP, se necessário.

Esses métodos correspondem ao fluxo de autenticação definido em RFC 2612 e RFC 2617:

  1. O cliente envia credenciais no cabeçalho Autorização. Isso normalmente acontece depois que o cliente recebe uma resposta 401 (Não autorizado) do servidor. No entanto, um cliente pode enviar credenciais com qualquer solicitação, não apenas depois de obter um 401.
  2. Se o servidor não aceitar as credenciais, ele retornará uma resposta 401 (Não autorizado). A resposta inclui um cabeçalho Www-Authenticate que contém um ou mais desafios. Cada desafio especifica um esquema de autenticação reconhecido pelo servidor.

O servidor também pode retornar 401 de uma solicitação anônima. Na verdade, normalmente é assim que o processo de autenticação é iniciado:

  1. O cliente envia uma solicitação anônima.
  2. O servidor retorna 401.
  3. Os clientes reenviam a solicitação com credenciais.

Esse fluxo inclui as etapas de autenticação e autorização .

  • A autenticação prova a identidade do cliente.
  • A autorização determina se o cliente pode acessar um recurso específico.

Na API Web, os filtros de autenticação lidam com a autenticação, mas não com a autorização. A autorização deve ser feita por um filtro de autorização ou dentro da ação do controlador.

Aqui está o fluxo no pipeline da API Web 2:

  1. Antes de invocar uma ação, a API Web cria uma lista dos filtros de autenticação para essa ação. Isso inclui filtros com escopo de ação, escopo do controlador e escopo global.
  2. A API Web chama AuthenticateAsync em cada filtro na lista. Cada filtro pode validar credenciais na solicitação. Se qualquer filtro validar credenciais com êxito, o filtro criará um IPrincipal e o anexará à solicitação. Um filtro também pode disparar um erro neste ponto. Nesse caso, o restante do pipeline não é executado.
  3. Supondo que não haja nenhum erro, a solicitação flui pelo restante do pipeline.
  4. Por fim, a API Web chama o método ChallengeAsync de cada filtro de autenticação. Os filtros usam esse método para adicionar um desafio à resposta, se necessário. Normalmente (mas nem sempre) isso aconteceria em resposta a um erro 401.

Os diagramas a seguir mostram dois casos possíveis. No primeiro, o filtro de autenticação autentica com êxito a solicitação, um filtro de autorização autoriza a solicitação e a ação do controlador retorna 200 (OK).

Diagrama de autenticação bem-sucedida

No segundo exemplo, o filtro de autenticação autentica a solicitação, mas o filtro de autorização retorna 401 (Não autorizado). Nesse caso, a ação do controlador não é invocada. O filtro de autenticação adiciona um cabeçalho Www-Authenticate à resposta.

Diagrama de autenticação não autorizada

Outras combinações são possíveis, por exemplo, se a ação do controlador permitir solicitações anônimas, você poderá ter um filtro de autenticação, mas sem autorização.

Implementando o método AuthenticateAsync

O método AuthenticateAsync tenta autenticar a solicitação. Veja a assinatura do método:

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

O método AuthenticateAsync deve fazer um dos seguintes procedimentos:

  1. Nada (sem operações).
  2. Crie um IPrincipal e defina-o na solicitação.
  3. Defina um resultado de erro.

A opção (1) significa que a solicitação não tinha nenhuma credencial que o filtro entende. Opção (2) significa que o filtro autenticou com êxito a solicitação. Opção (3) significa que a solicitação tinha credenciais inválidas (como a senha errada), o que dispara uma resposta de erro.

Aqui está uma estrutura de tópicos geral para implementar AuthenticateAsync.

  1. Procure credenciais na solicitação.
  2. Se não houver credenciais, não faça nada e retorne (sem operações).
  3. Se houver credenciais, mas o filtro não reconhecer o esquema de autenticação, não faça nada e retorne (sem operações). Outro filtro no pipeline pode entender o esquema.
  4. Se houver credenciais que o filtro entenda, tente autenticá-las.
  5. Se as credenciais estiverem incorretas, retorne 401 definindo context.ErrorResult.
  6. Se as credenciais forem válidas, crie um IPrincipal e defina context.Principal.

O código a seguir mostra o método AuthenticateAsync do exemplo de Autenticação Básica . Os comentários indicam cada etapa. O código mostra vários tipos de erro: um cabeçalho de autorização sem credenciais, credenciais malformadas e nome de usuário/senha incorretos.

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

}

Definindo um resultado de erro

Se as credenciais forem inválidas, o filtro deverá ser definido context.ErrorResult como um IHttpActionResult que cria uma resposta de erro. Para obter mais informações sobre IHttpActionResult, consulte Resultados da ação na API Web 2.

O exemplo de Autenticação Básica inclui uma AuthenticationFailureResult classe adequada para essa finalidade.

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

Implementando ChallengeAsync

A finalidade do método ChallengeAsync é adicionar desafios de autenticação à resposta, se necessário. Veja a assinatura do método:

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

O método é chamado em cada filtro de autenticação no pipeline de solicitação.

É importante entender que ChallengeAsync é chamado antes da resposta HTTP ser criada e, possivelmente, até mesmo antes da execução da ação do controlador. Quando ChallengeAsync é chamado, context.Result contém um IHttpActionResult, que é usado posteriormente para criar a resposta HTTP. Portanto, quando ChallengeAsync é chamado, você ainda não sabe nada sobre a resposta HTTP. O método ChallengeAsync deve substituir o valor original de context.Result por um novo IHttpActionResult. Esse IHttpActionResult deve encapsular o original context.Result.

Diagrama de ChallengeAsync

Chamarei o IHttpActionResult original de resultado interno e o novo IHttpActionResult como o resultado externo. O resultado externo deve fazer o seguinte:

  1. Invoque o resultado interno para criar a resposta HTTP.
  2. Examine a resposta.
  3. Adicione um desafio de autenticação à resposta, se necessário.

O exemplo a seguir é obtido do exemplo de Autenticação Básica. Ele define um IHttpActionResult para o 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;
    }
}

A InnerResult propriedade contém o IHttpActionResult interno. A Challenge propriedade representa um cabeçalho Www-Authentication. Observe que ExecuteAsync primeiro chama InnerResult.ExecuteAsync para criar a resposta HTTP e, em seguida, adiciona o desafio, se necessário.

Verifique o código de resposta antes de adicionar o desafio. A maioria dos esquemas de autenticação só adicionará um desafio se a resposta for 401, conforme mostrado aqui. No entanto, alguns esquemas de autenticação adicionam um desafio a uma resposta de sucesso. Por exemplo, consulte Negociar (RFC 4559).

Dada a AddChallengeOnUnauthorizedResult classe , o código real em ChallengeAsync é simples. Basta criar o resultado e anexá-lo 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);
}

Observação: o exemplo de Autenticação Básica abstrai essa lógica um pouco, colocando-a em um método de extensão.

Combinando filtros de autenticação com autenticação Host-Level

"Autenticação no nível do host" é a autenticação executada pelo host (como o IIS), antes que a solicitação atinja a estrutura da API Web.

Geralmente, talvez você queira habilitar a autenticação no nível do host para o restante do aplicativo, mas desabilitá-la para seus controladores de API Web. Por exemplo, um cenário típico é habilitar a Autenticação de Formulários no nível do host, mas usar a autenticação baseada em token para a API Web.

Para desabilitar a autenticação no nível do host dentro do pipeline da API Web, chame config.SuppressHostPrincipal() em sua configuração. Isso faz com que a API Web remova o IPrincipal de qualquer solicitação que insira o pipeline de API Web. Efetivamente, ele "não autentica" a solicitação.

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

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

Recursos adicionais

filtros de segurança ASP.NET Web API (Revista MSDN)