Proteger uma API Web com contas individuais e logon local na ASP.NET Web API 2.2

por Mike Wasson

Baixar aplicativo de exemplo

Este tópico mostra como proteger uma API Web usando o OAuth2 para autenticar em um banco de dados de associação.

Versões de software usadas no tutorial

No Visual Studio 2013, o modelo de projeto da API Web oferece três opções de autenticação:

  • Contas individuais. O aplicativo usa um banco de dados de associação.
  • Contas organizacionais. Os usuários entrarão com suas credenciais do Azure Active Directory, Office 365 ou active directory local.
  • Autenticação do Windows. Essa opção destina-se a aplicativos da Intranet e usa o módulo IIS de Autenticação do Windows.

Para obter mais detalhes sobre essas opções, consulte Criando projetos Web ASP.NET em Visual Studio 2013.

Contas individuais fornecem duas maneiras para um usuário fazer logon:

  • Logon local. O usuário se registra no site, inserindo um nome de usuário e uma senha. O aplicativo armazena o hash de senha no banco de dados de associação. Quando o usuário faz logon, o sistema ASP.NET Identity verifica a senha.
  • Logon social. O usuário entra com um serviço externo, como Facebook, Microsoft ou Google. O aplicativo ainda cria uma entrada para o usuário no banco de dados de associação, mas não armazena nenhuma credencial. O usuário é autenticado entrando no serviço externo.

Este artigo analisa o cenário de logon local. Para logon local e social, a API Web usa o OAuth2 para autenticar solicitações. No entanto, os fluxos de credenciais são diferentes para logon local e social.

Neste artigo, demonstrarei um aplicativo simples que permite que o usuário faça logon e envie chamadas AJAX autenticadas para uma API Web. Você pode baixar o código de exemplo aqui. O leiame descreve como criar o exemplo do zero no Visual Studio.

Imagem do formulário de exemplo

O aplicativo de exemplo usa Knockout.js para associação de dados e jQuery para enviar solicitações AJAX. Vou me concentrar nas chamadas do AJAX, para que você não precise saber Knockout.js deste artigo.

Ao longo do caminho, descreverei:

  • O que o aplicativo está fazendo no lado do cliente.
  • O que está acontecendo no servidor.
  • O tráfego HTTP no meio.

Primeiro, precisamos definir alguma terminologia OAuth2.

  • Recurso. Alguns dados que podem ser protegidos.
  • Servidor de recursos. O servidor que hospeda o recurso.
  • Proprietário do recurso. A entidade que pode conceder permissão para acessar um recurso. (Normalmente, o usuário.)
  • Cliente: o aplicativo que deseja acessar o recurso. Neste artigo, o cliente é um navegador da Web.
  • Token de acesso. Um token que concede acesso a um recurso.
  • Token de portador. Um tipo específico de token de acesso, com a propriedade que qualquer pessoa pode usar o token. Em outras palavras, um cliente não precisa de uma chave criptográfica ou outro segredo para usar um token de portador. Por esse motivo, os tokens de portador só devem ser usados em um HTTPS e devem ter tempos de expiração relativamente curtos.
  • Servidor de autorização. Um servidor que fornece tokens de acesso.

Um aplicativo pode atuar como servidor de autorização e servidor de recursos. O modelo de projeto da API Web segue esse padrão.

Fluxo de credenciais de logon local

Para logon local, a API Web usa o fluxo de senha do proprietário do recurso definido no OAuth2.

  1. O usuário insere um nome e uma senha no cliente.
  2. O cliente envia essas credenciais para o servidor de autorização.
  3. O servidor de autorização autentica as credenciais e retorna um token de acesso.
  4. Para acessar um recurso protegido, o cliente inclui o token de acesso no cabeçalho Autorização da solicitação HTTP.

Diagrama do fluxo de credenciais de logon local

Quando você seleciona Contas individuais no modelo de projeto da API Web, o projeto inclui um servidor de autorização que valida as credenciais do usuário e emite tokens. O diagrama a seguir mostra o mesmo fluxo de credenciais em termos de componentes da API Web.

Diagrama quando contas individuais são selecionadas na Web A P I

Nesse cenário, os controladores de API Web atuam como servidores de recursos. Um filtro de autenticação valida tokens de acesso e o atributo [Autorizar] é usado para proteger um recurso. Quando um controlador ou ação tem o atributo [Authorize] , todas as solicitações para esse controlador ou ação devem ser autenticadas. Caso contrário, a autorização será negada e a API Web retornará um erro 401 (Não autorizado).

O servidor de autorização e o filtro de autenticação chamam um componente de middleware OWIN que manipula os detalhes do OAuth2. Descreverei o design mais detalhadamente mais adiante neste tutorial.

Enviando uma solicitação não autorizada

Para começar, execute o aplicativo e clique no botão Chamar API . Quando a solicitação for concluída, você deverá ver uma mensagem de erro na caixa Resultado . Isso ocorre porque a solicitação não contém um token de acesso, portanto, a solicitação não é autorizada.

Imagem da mensagem de erro de resultado

O botão Chamar API envia uma solicitação AJAX para ~/api/values, que invoca uma ação do controlador de API Web. Esta é a seção do código JavaScript que envia a solicitação AJAX. No aplicativo de exemplo, todo o código do aplicativo JavaScript está localizado no arquivo Scripts\app.js.

// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}

$.ajax({
    type: 'GET',
    url: 'api/values/1',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);

Até que o usuário faça logon, não há nenhum token de portador e, portanto, nenhum cabeçalho de autorização na solicitação. Isso faz com que a solicitação retorne um erro 401.

Aqui está a solicitação HTTP. (Usei o Fiddler para capturar o tráfego HTTP.)

GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/

Resposta HTTP:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

Observe que a resposta inclui um cabeçalho Www-Authenticate com o desafio definido como Portador. Isso indica que o servidor espera um token de portador.

Registrar um usuário

Na seção Registrar do aplicativo, insira um email e uma senha e clique no botão Registrar .

Você não precisa usar um endereço de email válido para este exemplo, mas um aplicativo real confirmaria o endereço. (Consulte Criar um aplicativo Web seguro ASP.NET MVC 5 com logon, confirmação de email e redefinição de senha.) Para a senha, use algo como "Password1!", com uma letra maiúscula, letra minúscula, número e caractere não alfanumérico. Para manter o aplicativo simples, deixei de fora a validação do lado do cliente, portanto, se houver um problema com o formato de senha, você receberá um erro 400 (Solicitação Incorreta).

Imagem de registrar uma seção de usuário

O botão Registrar envia uma solicitação POST para ~/api/Account/Register/. O corpo da solicitação é um objeto JSON que contém o nome e a senha. Aqui está o código JavaScript que envia a solicitação:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};

$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);

Solicitação HTTP, em que $CREDENTIAL_PLACEHOLDER$ é um espaço reservado para o par chave-valor de senha:

POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84

{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}

Resposta HTTP:

HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0

Essa solicitação é tratada pela AccountController classe . Internamente, AccountController usa ASP.NET Identity para gerenciar o banco de dados de associação.

Se você executar o aplicativo localmente no Visual Studio, as contas de usuário serão armazenadas no LocalDB, na tabela AspNetUsers. Para exibir as tabelas no Visual Studio, clique no menu Exibir, selecione Servidor Explorer e, em seguida, expanda Conexões de Dados.

Imagem de conexões de dados

Obter um token de acesso

Até agora, não fizemos nenhum OAuth, mas agora veremos o servidor de autorização OAuth em ação, quando solicitarmos um token de acesso. Na área Fazer logon do aplicativo de exemplo, insira o email e a senha e clique em Fazer logon.

Imagem da seção de logon

O botão Fazer Logon envia uma solicitação para o ponto de extremidade do token. O corpo da solicitação contém os seguintes dados codificados em form-url:

  • grant_type: "password"
  • username: <o email do usuário>
  • password: <password>

Aqui está o código JavaScript que envia a solicitação AJAX:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};

$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Cache the access token in session storage.
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

Se a solicitação for bem-sucedida, o servidor de autorização retornará um token de acesso no corpo da resposta. Observe que armazenamos o token no armazenamento de sessão, a ser usado posteriormente ao enviar solicitações para a API. Ao contrário de algumas formas de autenticação (como autenticação baseada em cookie), o navegador não incluirá automaticamente o token de acesso em solicitações subsequentes. O aplicativo deve fazer isso explicitamente. Isso é uma coisa boa, porque limita as vulnerabilidades de CSRF.

Solicitação HTTP:

POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68

grant_type=password&username=alice%40example.com&password=Password1!

Você pode ver que a solicitação contém as credenciais do usuário. Você deve usar HTTPS para fornecer segurança de camada de transporte.

Resposta HTTP:

HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT

{
  "access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
  "token_type":"bearer",
  "expires_in":1209599,
  "userName":"alice@example.com",
  ".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
  ".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}

Para facilitar a leitura, indenizei o JSON e truncei o token de acesso, o que é muito longo.

As access_tokenpropriedades , token_typee expires_in são definidas pela especificação OAuth2. As outras propriedades (userName, .issuede .expires) são apenas para fins informativos. Você pode encontrar o código que adiciona essas propriedades adicionais no TokenEndpoint método , no arquivo /Providers/ApplicationOAuthProvider.cs.

Enviar uma solicitação autenticada

Agora que temos um token de portador, podemos fazer uma solicitação autenticada para a API. Isso é feito definindo o cabeçalho De autorização na solicitação. Clique no botão Chamar API novamente para ver isso.

Imagem após chamar o botão API foi clicado

Solicitação HTTP:

GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest

Resposta HTTP:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27

"Hello, alice@example.com."

Logoff

Como o navegador não armazena em cache as credenciais ou o token de acesso, fazer logoff é simplesmente uma questão de "esquecer" o token, removendo-o do armazenamento de sessão:

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}

Noções básicas sobre o modelo de projeto de contas individuais

Quando você seleciona Contas Individuais no modelo de projeto ASP.NET Aplicativo Web, o projeto inclui:

  • Um servidor de autorização OAuth2.
  • Um ponto de extremidade da API Web para gerenciar contas de usuário
  • Um modelo de EF para armazenar contas de usuário.

Aqui estão as classes de aplicativo main que implementam esses recursos:

  • AccountController. Fornece um ponto de extremidade da API Web para gerenciar contas de usuário. A Register ação é a única que usamos neste tutorial. Outros métodos na classe dão suporte à redefinição de senha, logons sociais e outras funcionalidades.
  • ApplicationUser, definido em /Models/IdentityModels.cs. Essa classe é o modelo de EF para contas de usuário no banco de dados de associação.
  • ApplicationUserManager, definido em /App_Start/IdentityConfig.cs Essa classe deriva de UserManager e executa operações em contas de usuário, como criar um novo usuário, verificar senhas e assim por diante e persistir automaticamente as alterações no banco de dados.
  • ApplicationOAuthProvider. Esse objeto se conecta ao middleware OWIN e processa eventos gerados pelo middleware. Ele deriva de OAuthAuthorizationServerProvider.

Imagem de classes de aplicativo main

Configurando o servidor de autorização

Em StartupAuth.cs, o código a seguir configura o servidor de autorização OAuth2.

PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // Note: Remove the following line before you deploy to production:
    AllowInsecureHttp = true
};

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

A TokenEndpointPath propriedade é o caminho da URL para o ponto de extremidade do servidor de autorização. Essa é a URL que o aplicativo usa para obter os tokens de portador.

A Provider propriedade especifica um provedor que se conecta ao middleware OWIN e processa eventos gerados pelo middleware.

Este é o fluxo básico quando o aplicativo deseja obter um token:

  1. Para obter um token de acesso, o aplicativo envia uma solicitação para ~/Token.
  2. O middleware OAuth chama GrantResourceOwnerCredentials no provedor.
  3. O provedor chama o ApplicationUserManager para validar as credenciais e criar uma identidade de declarações.
  4. Se isso for bem-sucedido, o provedor criará um tíquete de autenticação, que é usado para gerar o token.

Diagrama do fluxo de autorização

O middleware OAuth não sabe nada sobre as contas de usuário. O provedor se comunica entre o middleware e o ASP.NET Identity. Para obter mais informações sobre como implementar o servidor de autorização, consulte OWIN OAuth 2.0 Authorization Server.

Configurando a API Web para usar tokens de portador

WebApiConfig.Register No método , o código a seguir configura a autenticação para o pipeline da API Web:

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

A classe HostAuthenticationFilter habilita a autenticação usando tokens de portador.

O método SuppressDefaultHostAuthentication informa à API Web para ignorar qualquer autenticação que aconteça antes que a solicitação atinja o pipeline da API Web, seja pelo middleware IIS ou OWIN. Dessa forma, podemos restringir a API da Web para autenticar somente usando tokens de portador.

Observação

Em particular, a parte MVC do seu aplicativo pode usar a autenticação de formulários, que armazena credenciais em um cookie. A autenticação baseada em cookie requer o uso de tokens antifalsificação para evitar ataques CSRF. Isso é um problema para APIs Web, pois não há uma maneira conveniente para a API Web enviar o token antifalsificação para o cliente. (Para obter mais informações sobre esse problema, consulte Preventing CSRF Attacks in Web API.) Chamar SuppressDefaultHostAuthentication garante que a API Web não esteja vulnerável a ataques CSRF de credenciais armazenadas em cookies.

Quando o cliente solicita um recurso protegido, aqui está o que acontece no pipeline da API Web:

  1. O filtro HostAuthentication chama o middleware OAuth para validar o token.
  2. O middleware converte o token em uma identidade de declarações.
  3. Neste ponto, a solicitação é autenticada , mas não autorizada.
  4. O filtro de autorização examina a identidade das declarações. Se as declarações autorizarem o usuário para esse recurso, a solicitação será autorizada. Por padrão, o atributo [Authorize] autorizará qualquer solicitação autenticada. No entanto, você pode autorizar por função ou por outras declarações. Para obter mais informações, consulte Autenticação e autorização na API Web.
  5. Se as etapas anteriores forem bem-sucedidas, o controlador retornará o recurso protegido. Caso contrário, o cliente receberá um erro 401 (Não autorizado).

Diagrama de quando o cliente solicita um recurso protegido

Recursos adicionais