APIs e Microsserviços

14 mai, 2018

Autenticação em APIs ASP.Net Core com JWT

Publicidade

A segurança de aplicações sempre foi – ou deveria ser – uma das principais preocupações dos times de desenvolvimento, e nos últimos anos a autenticação de APIs vem sendo bastante discutida nas comunidades técnicas em todo o mundo.

Existem diversas maneiras de resolver a autenticação em APIs, e uma delas é fazendo uso de tokens de acesso, sendo o JWT (Json Web Token) um dos padrões de tokens de acesso mais famosos e seguros da atualidade. Eu falei sobre ele em um artigo anterior.

Hoje irei explicar os principais pontos da criação de uma API de autenticação com JWT em ASP.Net Core. O código completo de exemplo encontra-se em meu GitHub.

Lembrando que neste artigo irei focar em uma implementação simples, mas você também pode usar outras soluções mais robustas para implementar autenticação, como o Identity Server, que é open source e mantido pela comunidade.

Conceitos

Antes de ver como funciona a criação de tokens JWT no ASP.Net Core, vamos lembrar dois conceitos fundamentais.

  • Autenticação: ato de identificar que o usuário é quem ele diz ser, validando suas credenciais de acesso que geralmente são um login e uma senha. Também podem ser usados outros meios para identificar um usuário de um sistema como, por exemplo: usando biometria ou cartões de identificação;
  • Autorização: ato de verificar se o usuário pode ou não ter acesso à um recurso ou executar determinada ação dentro do sistema. Nesse ponto o usuário já foi identificado (autenticado) previamente. Normalmente usamos Roles ou Policies para autorizar o acesso à determinado recurso.

Hoje vamos focar somente na autenticação. Em um próximo artigo irei mostrar como podemos fazer a autorização dos usuários em uma API ASP.Net Core.

De qualquer modo, tenha em mente esses dois conceitos, pois eles são muito importantes.

Como criar um token JWT

No projeto de exemplo existe uma classe chamada JwtService de modo à simplificar a criação de um token JWT. Ela faz uso da classe JwtSecurityTokenHandler (pacote nuget System.IdentityModel.Tokens.Jwt) que é quem realmente irá gerar um Json Web Token devidamente assinado e válido.

As informações contidas no token JWT são armazenadas em formato de Claims. Sendo assim, podemos usar o método GetClaimsIdentity passando uma instância de um usuário previamente *autenticado *através de suas credenciais de acesso para obter a identificação do usuário. Esse método irá ler as propriedades do usuário e criar um objeto ClaimsIdentity, que é armazenado na claim Subject representando a identidade do usuário dentro do token.

Por segurança recomenda-se não armazenar informações confidenciais ou sensíveis no token.

Perceba que durante a criação do token, informamos mais alguns dados, chamados de Reserved Claims, segundo a especificação do JWT, que são atributos não obrigatórios (mas recomendados), usados na validação do token pelos protocolos de segurança das APIs.

Esses dados estão encapsulados em uma classe chamada JwtSettings, onde uma instância de objeto é criada no momento em que a aplicação se inicia e registrada no container de injeção de dependências. Os valores dela estão no arquivo de configuração da aplicação, nosso querido appsettings.json.

Basicamente, essas claims são:

  • Issuer (iss): quem emite o token JWT;
  • Audience (aud): aplicações que podem usar o token JWT. Normalmente temos apenas um valor, mas você pode informar mais de uma;
  • Expires (exp): data e hora em que o token irá expirar;
  • IssuedAt (iat): data e hora em que o token foi emitido;

Você pode conferir a especificação do JWT para obter mais detalhes sobre as claims.

De modo a garantir a segurança, o token deve ser assinado digitalmente, sendo que para isso, normalmente usamos algoritmos como HMAC ou RSA.

Para assinar o token via RSA, você precisa ter um certificado digital válido, já para a assinatura HMAC é necessário apenas uma chave privada. Na aplicação de exemplo estou usando HMAC.

public class JwtService : IJwtService
{
	private readonly JwtSettings _settings;

	public JwtService(JwtSettings settings)
	{
		_settings = settings;
	}

	public object CreateJwtToken(User user)
	{
		var identity = GetClaimsIdentity(user);
		var handler = new JwtSecurityTokenHandler();
		var securityToken = handler.CreateToken(new SecurityTokenDescriptor
		{
			Subject = identity,
			Issuer = _settings.Issuer,
			Audience = _settings.Audience,
			IssuedAt = _settings.IssuedAt,
			NotBefore = _settings.NotBefore,
			Expires = _settings.Expiration,
			SigningCredentials = _settings.SigningCredentials
		});

		var jwtToken = handler.WriteToken(securityToken);

		return new
		{
			access_token = jwtToken,
			token_type = "bearer",
			expires_in = (int)_settings.ValidFor.TotalSeconds
		};
	}

	private static ClaimsIdentity GetClaimsIdentity(User user)
	{
		return new ClaimsIdentity
		(
			new GenericIdentity(user.Email),
			new[] {
				new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
				new Claim(JwtRegisteredClaimNames.Sub, user.Name)
			}
		);
	}
}

A classe JwtService deverá ser utilizada por alguma classe de aplicação ou até mesmo uma controller do MVC. Eu particularmente gosto muito de utilizar o MediatR em meus projetos, sendo que já falei sobre ele em um artigo anterior. Então no projeto de exemplo você encontrará a classe AuthenticateUserHandler que será responsável por tratar/orquestrar um comando de login, que é representado pela classe AuthenticateUser, que por sua vez encapsula as credenciais de acesso do usuário.

A classe AuthenticateUserHandler recebe em seu construtor dois parâmetros, uma instância do repositório de usuários e uma instância do JwtService que vimos anteriormente. Caso o usuário seja autenticado com sucesso pelo repositório, usamos o serviço para gerar um novo token JWT.

No projeto de exemplo eu uso a classe InMemoryDatabaseContext e uma lista de User para representar o banco de dados e a tabela de usuários, bem simples. O foco deste artigo não é o repositório ou onde o dado está armazenado, mas em um projeto real você deverá usar um banco de dados NoSQL ou relacional.

Veja que também fazemos uso do padrão Notification Pattern para retornar mensagens de erro ao usuário, dessa forma não precisamos levantar Exceptions na aplicação. Eu também falei sobre isso em um artigo anterior.

public class AuthenticateUserHandler : IRequestHandler<AuthenticateUser, Response>
{
	private readonly IJwtService _jwtService;
	private readonly IUserRepository _repository;

	public AuthenticateUserHandler(IJwtService jwtService, IUserRepository repository)
	{
		_jwtService = jwtService;
		_repository = repository;
	}

	public async Task<Response> Handle(AuthenticateUser request, CancellationToken cancellationToken)
	{
		var response = new Response();
		var encodedPassword = new Password(request.Password).Encoded;
		var user = await _repository.Authenticate(request.Email, encodedPassword);

		if (user == null)
		{
			response.AddNotification(new Notification("user", "Usuário ou senha inválidos"));
			return response;
		}

		var jwt = _jwtService.CreateJwtToken(user);
		response.AddValue(jwt);

		return response;
	}
}

Agora só precisamos recepcionar uma solicitação de login na API através de uma Controller e encaminhar para o MediatR executar o processo de autenticação na aplicação, dessa forma nossa Controller fica bem limpa.

[Route("api/[controller]")]
public class AuthController : Controller
{
	private readonly IMediator _mediator;

	public AuthController(IMediator mediator)
	{
		_mediator = mediator;
	}

	[HttpPost, AllowAnonymous, Route("login")]
	public async Task<IActionResult> Authenticate([FromBody] AuthenticateUser command)
	{
		var response = await _mediator.Send(command);
		if (response.HasMessages)
		{
			return BadRequest(response.Messages);
		}

		return Ok(response.Value);
	}
}

Como validar o token JWT

Com um token JWT devidamente criado, ele deverá ser enviado via Authorization header em todas as demais solicitações feitas à nossas APIs, sendo assim, precisamos garantir que esse token esteja válido.

Para isso, o ASP.Net Core já possui um middleware responsável pela validação de tokens de acesso. Se você usa o meta-package Microsoft.AspNetCore.All então você já tem esse middleware disponível e pode começar a usá-lo de imediato, caso contrário, você pode instalar o pacote nuget Microsoft.AspNetCore.Authentication.JwtBearer.

Essa configuração deverá ser feita em todas as APIs que irão receber o token JWT, então você pode componentizar isso caso ache necessário.

private static void AddJwtAuthorization(IServiceCollection services)
{
	var jwtSettings = services.BuildServiceProvider().GetRequiredService<JwtSettings>();

	services
		.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
		.AddJwtBearer(jwtBearerOptions =>
		{
			jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
			{
				ValidateIssuer = true,
				ValidateActor = true,
				ValidateAudience = true,
				ValidateLifetime = true,
				ValidateIssuerSigningKey = true,
				ValidIssuer = jwtSettings.Issuer,
				ValidAudience = jwtSettings.Audience,
				IssuerSigningKey = jwtSettings.SigningCredentials.Key
			};
		});

	services.AddAuthorization();
}

Nesse método apenas estamos adicionando os middlewares necessários no injetor de dependências nativo do ASP.Net Core e configurando alguns parâmetros que serão verificados quando um token JWT for recepcionado pela API, como o Issuer, Audience, assinatura e tempo de vida do token.

Além disso, você deve instruir o pipeline do ASP.Net Core a usar a Autenticação. Isso deve ser feito no método Configure dentro da classe Startup da API.

public class Startup
{
	// ... restante do arquivo ocultado

	public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
	{
		// .. restante do método ocultado
		
		app.UseAuthentication();
		app.UseMvc();
	}
}

Testando

Podemos usar o Postman para fazer as chamadas à nossa API. Se você não quiser instalar o Postman, não tem problema, a aplicação de exemplo faz uso do Swagger para documentar e disponibilizar uma forma de testar a API de forma simples.

Primeiramente devemos registrar um usuário em nossa API informando seu nome, e-mail e senha de acesso.

Com o usuário criado, podemos usar o e-mail e senha para fazer a autenticação. Um token JWT é retornado em caso de sucesso. Junto com o token informamos seu tipo “bearer” e o tempo de expiração do mesmo.

Caso as credenciais de acesso não estejam corretas, apenas retornamos uma mensagem informando o problema. Neste caso: “Usuário ou senha inválidos”.

Nunca retorne exatamente qual foi o o motivo pelo qual a autenticação não foi feita, por exemplo: “Senha inválida”, pois isso já mostra para um possível atacante que possivelmente o e-mail informado existe na base e então ele pode apenas ficar testando as senhas para obter acesso indevido.

Com um token JWT válido, podemos fazer a chamada à recursos restritos onde somente usuários autenticados têm acesso. Nesse exemplo ele apenas retorna os dados do próprio usuário.

Para isso você deve enviar o token no header Authorization da requisição HTTP, usando a palavra “Bearer” como prefixo.

Authorization: Bearer [token_jwt]

Caso você tente consultar esse endpoint sem informar um token JWT válido, a API irá retornar um HTTP Status Code 401 (Unauthorized) informando que você não está autorizado à acessar esse recurso.

Conclusão

Como você pode ver, a autenticação em APIs é realmente necessária hoje em dia. Não podemos expor nossas APIs para o mundo sem garantir o mínimo de segurança, a não ser que a intenção seja realmente deixá-la aberta.

Existem muitos outros pontos a observar, como uso de Refresh Tokens, autenticação externa via Facebook, Google e Twitter, por exemplo.

Este exemplo é uma implementação simples de uma API de autenticação, e conforme eu disse anteriormente, você pode utilizar uma solução mais robusta, como o Identity Server para usar em ambiente de produção.

Espero que tenham gostado, e se ficou alguma dúvida ou caso tenham críticas e sugestões, entrem em contato.

Abraços!

Referências