Proteja uma API Web com Contas Individuais e Login Local em ASP.NET Web API 2.2
por Mike Wasson
Este tópico mostra como proteger uma API da Web usando 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 de 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 Ative Directory, Office 365 ou Ative Directory local.
- Autenticação do Windows. Esta opção destina-se a aplicações de Intranet e utiliza o módulo IIS de Autenticação do Windows.
As contas individuais fornecem duas formas de um utilizador iniciar sessão:
- Login local. O utilizador regista-se no site, introduzindo um nome de utilizador e uma palavra-passe. O aplicativo armazena o hash de senha no banco de dados de associação. Quando o utilizador inicia sessão, o sistema ASP.NET Identity verifica a palavra-passe.
- 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 login local. Para login local e social, a API da Web usa OAuth2 para autenticar solicitações. No entanto, os fluxos de credenciais são diferentes para login local e social.
Neste artigo, demonstrarei um aplicativo simples que permite que o usuário faça login e envie chamadas AJAX autenticadas para uma API da Web. Você pode baixar o código de exemplo aqui. O readme descreve como criar o exemplo do zero no Visual Studio.
O aplicativo de exemplo usa Knockout.js para vinculaçã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. 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.)
- Client: 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 ao portador só devem ser usados através de uma conexão 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 de API Web segue esse padrão.
Fluxo de credenciais de login local
Para login local, a API da Web usa o fluxo de senha do proprietário do recurso definido no OAuth2.
- O usuário insere um nome e senha no cliente.
- O cliente envia essas credenciais para o servidor de autorização.
- O servidor de autorização autentica as credenciais e retorna um token de acesso.
- Para acessar um recurso protegido, o cliente inclui o token de acesso no cabeçalho Authorization da solicitação HTTP.
Quando você seleciona Contas individuais no modelo de projeto de API da 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.
Nesse cenário, os controladores de API da 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
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 mais adiante neste tutorial.
Enviar um pedido não autorizado
Para começar, execute a app e clique no botão Chamar API. Quando a solicitação for concluída, você verá uma mensagem de erro na caixa de resultados. Isso ocorre porque a solicitação não contém um token de acesso, portanto, a solicitação não é autorizada.
O botão API de chamada
// 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 Portador. Isso indica que o servidor espera um token de portador.
Registar um Utilizador
Na secção Registar da aplicação, introduza um e-mail e uma palavra-passe e clique no botão Registar.
Você não precisa usar um endereço de e-mail válido para este exemplo, mas um aplicativo real confirmaria o endereço. (Consulte Criar um aplicativo Web MVC 5 ASP.NET seguro com login, confirmação de e-mail e redefinição de senha.) Para a senha, use algo como "Senha1!", 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).
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 da palavra-passe:
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 classe AccountController
. Internamente, AccountController
usa ASP.NET Identity para gerenciar o banco de dados de membros.
Se você executar o aplicativo localmente a partir do 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
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 Log In do aplicativo de exemplo, insira o e-mail e a senha e clique em Log In.
O botão Log In envia uma solicitação para o endpoint do token. O corpo da solicitação contém os seguintes dados codificados em url de formulário:
- grant_type: "password"
- nome de usuário: <o e-mail do usuário>
- palavra-passe: <palavra-passe>
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, para usar mais tarde ao enviar solicitações para a API. Ao contrário de algumas formas de autenticação (como a autenticação baseada em cookies), o navegador não incluirá automaticamente o token de acesso em solicitações subsequentes. O pedido deve fazê-lo explicitamente. Isso é bom, porque limita 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 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, pois é bastante longo.
As propriedades access_token
, 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 método TokenEndpoint
, no arquivo /Providers/ApplicationOAuthProvider.cs.
Enviar um pedido autenticado
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 Call API novamente para ver isso.
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."
Terminar sessão
Como o navegador não armazena em cache as credenciais ou o token de acesso, fazer 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
Ao selecionar Contas Individuais no modelo de projeto de Aplicação Web ASP.NET, o projeto inclui:
- Um servidor de autorização OAuth2.
- Um endpoint de API Web para gerir contas de utilizador
- 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 de API Web para gerenciar contas de usuário. A açãoRegister
é a única que usamos neste tutorial. Outros métodos na classe suportam redefinição de senha, logins sociais e outras funcionalidades. -
ApplicationUser
, definido em /Models/IdentityModels.cs. Esta 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.
Configurando o servidor de autorização
No 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 propriedade TokenEndpointPath
é o caminho do URL para o endpoint do servidor de autorização. Esse é o URL que o aplicativo usa para obter os tokens de portador.
A propriedade Provider
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:
- Para obter um token de acesso, o aplicativo envia uma solicitação para ~/Token.
- O middleware OAuth chama
GrantResourceOwnerCredentials
no fornecedor. - O provedor chama o
ApplicationUserManager
para validar as credenciais e criar uma identidade de declarações. - Se isso for bem-sucedido, o provedor criará um tíquete de autenticação, que é usado para gerar o token.
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 da Web para usar tokens de portador
No método WebApiConfig.Register
, 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
O método SuppressDefaultHostAuthentication instrui a API Web a ignorar qualquer autenticação que ocorra antes de a solicitação chegar ao pipeline da API Web, seja pelo IIS ou pelo middleware OWIN. Dessa forma, podemos restringir a API da Web para autenticar apenas 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 da Web, porque não há uma maneira conveniente para a API da Web enviar o token antifalsificação para o cliente. (Para mais informações sobre este problema, consulte Prevenção de ataques CSRF na API da Web.) Ao chamar SuppressDefaultHostAuthentication, garante-se que a API da 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:
- O filtro HostAuthentication chama o middleware OAuth para validar o token.
- O middleware converte o token em uma identidade de reivindicações.
- Neste ponto, a solicitação é autenticada mas não autorizada.
- 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
[Autorizar] 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. - Se as etapas anteriores forem bem-sucedidas, o controlador retornará o recurso protegido. Caso contrário, o cliente recebe um erro 401 (não autorizado).
Recursos adicionais
- ASP.NET Identidade
- Noções básicas sobre recursos de segurança no modelo SPA para VS2013 RC. Postagem no blog do MSDN por Hongye Sun.
- Dissecando o modelo de contas individuais da API da Web – Parte 2: Contas locais. Postagem no blog por Dominick Baier.
-
Autenticação de host e API Web com OWIN. Uma boa explicação de
SuppressDefaultHostAuthentication
eHostAuthenticationFilter
por Brock Allen. - Personalização de informações de perfil no ASP.NET Identity em modelos do VS 2013. Postagem no blog do MSDN por Pranav Rastogi.
-
Gerenciamento de tempo de vida por solicitação para a classe UserManager no ASP.NET Identity. Postagem no blog do MSDN por Suhas Joshi, com uma boa explicação da classe
UserManager
.