Compartilhar via


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

por Mike Wasson

Baixar aplicativo de amostra

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 entram com suas credenciais do Azure Active Directory, Office 365 ou Active Directory local.
  • Autenticação do Windows. Essa opção destina-se a aplicativos de intranet e usa o módulo IIS de Autenticação do Windows.

As contas individuais fornecem duas maneiras de um usuário fazer login:

  • Login local. O usuário se registra no site, digitando um nome de usuário e senha. O aplicativo armazena o hash de senha no banco de dados de associação. Quando o usuário faz login, o sistema ASP.NET Identity verifica a senha.
  • Login 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 se autentica 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 amostra

O aplicativo de exemplo usa Knockout.js para associação de dados e jQuery para enviar solicitações AJAX. Vou me concentrar nas chamadas AJAX, então você não precisa saber Knockout.js para este 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. Algum dado que pode ser protegido.
  • 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 de 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 distribui 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 login 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 Authorization da solicitação HTTP.

Diagrama do fluxo de credenciais de login local

Quando você seleciona Contas individuais no modelo de projeto de 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 [Authorize] é 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 lida com os detalhes do OAuth2. Descreverei o design com mais detalhes posteriormente 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 está autorizada.

Imagem da mensagem de erro do resultado

O botão Chamar API envia uma solicitação AJAX para ~/api/values, que invoca uma ação do controlador da API Web. Aqui está 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 login, 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. (Eu usei 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 Bearer. Isso indica que o servidor espera um token de portador.

Registrar um usuário

Na seção Registrar do aplicativo, insira um e-mail 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. (Veja Crie um aplicativo Web MVC 5 de ASP.NET seguro com login, confirmação de email e redefinição de senha.) Para a senha, use algo como "Senha1!", com 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 da senha, você receberá um erro 400 (Bad Request).

Imagem da seção registrar um 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, onde $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 Gerenciador de Servidores e 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 Logon do aplicativo de exemplo, insira o email e a senha e clique em Logon.

Imagem da seção de login

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 url de formulário:

  • grant_type: "senha"
  • Nome de usuário: <o e-mail do usuário>
  • senha: <senha>

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 da sessão, para usar posteriormente ao enviar solicitações para a API. Ao contrário de algumas formas de autenticação (como autenticação baseada em cookies), 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 do 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 da 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, recuei o JSON e trunquei o token de acesso, que é bastante longo.

As access_tokenpropriedades , token_type, e expires_in são definidas pela especificação OAuth2. As outras propriedades (userName, .issued, e .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 Authorization na solicitação. Clique no botão Chamar API novamente para ver isso.

Imagem após a chamada O botão A P I 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, o logout é simplesmente uma questão de "esquecer" o token, removendo-o do armazenamento da 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 de aplicativo Web ASP.NET, o projeto inclui:

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

Aqui estão as principais classes de aplicativos 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, logins sociais e outras funcionalidades.
  • ApplicationUser, definido em /Models/IdentityModels.cs. Essa classe é o modelo 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 persiste 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 das principais classes de aplicação

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.

Aqui está 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 o 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 será 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 ASP.NET Identity. Para obter mais informações sobre como implementar o servidor de autorização, consulte Servidor de autorização OWIN OAuth 2.0.

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 ocorra antes que a solicitação chegue ao pipeline da API Web, seja pelo IIS ou pelo middleware 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 cookies 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 assunto, consulte Prevenção de ataques CSRF na API Web.) Chamar SuppressDefaultHostAuthentication garante que a API Web não seja 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