ASP.NET Core 3 – Autenticação e Autorização com Bearer e JWT
A autenticação JWT no ASP.NET Core 3.0 permite proteger uma API com tokens assinados e liberar cada endpoint de acordo com o perfil (role) do usuário. Neste guia, você monta esse esquema da forma mais simples e funcional possível, sem banco de dados e sem bibliotecas extras.
Importante: já existe uma versão mais atual deste conteúdo, escrita para o ASP.NET 5. Você pode consultá-la em autenticação e autorização com Bearer e JWT no ASP.NET 5.
Por que revisitar a autenticação JWT no ASP.NET Core 3
Há algum tempo publiquei um artigo sobre autenticação, autorização, tokens, claims e policies no ASP.NET Core. Ele cobria praticamente tudo o que você precisa saber sobre essa parte da segurança de APIs.
No entanto, o ASP.NET Core 3 trouxe mudanças importantes no pipeline. Além disso, aquele material ficou um pouco complexo para quem só quer trabalhar com roles, que é o cenário mais comum.
Por isso, este artigo foca no essencial. Você vai criar um esquema de autenticação JWT e autorização baseada em perfis no ASP.NET Core 3.0, com o mínimo de código.
O que mudou no pipeline do ASP.NET Core 3
A principal novidade da versão 3.0 é o endpoint routing como modelo padrão. Com isso, o UseMvc deu lugar à dupla UseRouting e UseEndpoints.
Além disso, a autorização ganhou um middleware próprio, o UseAuthorization. Nas versões anteriores, o atributo [Authorize] funcionava apenas como filtro do MVC.
Segundo o guia oficial de migração do ASP.NET Core 2.2 para o 3.0, as chamadas de UseAuthentication e UseAuthorization precisam ficar entre UseRouting e UseEndpoints. Fora dessa posição, elas simplesmente não têm efeito.
Em outras palavras, a ordem dos middlewares passou a importar ainda mais. Vamos voltar a esse ponto quando montarmos o Startup.cs.
Como funciona um token JWT
Antes do código, vale entender o que é o token que vamos gerar. O JWT (JSON Web Token) é um padrão aberto, descrito na RFC 7519, para transportar informações entre duas partes de forma compacta.
Na prática, um JWT tem três partes separadas por ponto: header, payload e assinatura. O header indica o algoritmo, o payload carrega as claims e a assinatura garante que ninguém alterou o conteúdo.
Um detalhe importante é que o payload não é criptografado, apenas codificado em Base64Url. Portanto, qualquer pessoa com o token consegue ler as claims, mas só quem tem a chave consegue gerar uma assinatura válida.
Dessa forma, nunca coloque senhas ou dados sensíveis dentro do token. Guarde nele apenas o que a API precisa para identificar e autorizar o usuário, como nome e perfil.
O formato Bearer, por sua vez, define como o cliente envia o token. Ele vai no cabeçalho Authorization, precedido da palavra Bearer e de um espaço.
Preparando o projeto para a autenticação JWT
Para executar a autenticação, precisamos de um usuário, uma senha e um perfil. Como o objetivo aqui não é cobrir acesso a dados, os usuários ficam fixos (hard coded) em um repositório.
Além disso, a geração do token fica em um serviço separado, que você pode reutilizar depois. Basta informar um usuário com um perfil e ele devolve o token, então ficamos totalmente independentes de banco de dados.
Para testar, vamos criar dois itens: Models/User.cs e Repositories/UserRepository.cs. O primeiro modela o usuário, enquanto o segundo "recupera" esse usuário da fonte de dados.
Criando a aplicação
Primeiro, crie a aplicação executando os comandos abaixo no terminal. Para acompanhar este artigo, você precisa do SDK do .NET Core 3.0 instalado.
dotnet new webapi -o Shop
cd Shop
dotnet restore
dotnet add package Microsoft.AspNetCore.Authentication
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 3.0.0
O pacote Microsoft.AspNetCore.Authentication.JwtBearer traz o handler que valida o token a cada requisição. Por isso, mantenha a versão do pacote alinhada com a versão do framework, que neste caso é a 3.0.
Sem esse pacote, a autenticação JWT não funciona, porque o ASP.NET Core não sabe ler o cabeçalho Bearer sozinho.
Em seguida, abra o projeto no Visual Studio Code e remova os arquivos WeatherForecast gerados pelo template. Existe um na raiz do projeto e outro na pasta Controllers.
Modelando o usuário
Agora, crie uma pasta chamada Models na raiz e, dentro dela, o arquivo User.cs com este código:
namespace Shop.Models
{
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Role { get; set; }
}
}
A propriedade Role é a peça central do exemplo. Afinal, na autenticação JWT é ela que vai parar dentro do token e definir o que cada usuário pode acessar.
Criando o repositório de usuários
O próximo item é o repositório. Para isso, crie a pasta Repositories na raiz e, dentro dela, o arquivo UserRepository.cs:
using System.Collections.Generic;
using System.Linq;
using Shop.Models;
namespace Shop.Repositories
{
public static class UserRepository
{
public static User Get(string username, string password)
{
// Usuários fixos apenas para fins de demonstração
var users = new List<User>
{
new User { Id = 1, Username = "batman", Password = "batman", Role = "manager" },
new User { Id = 2, Username = "robin", Password = "robin", Role = "employee" }
};
return users.FirstOrDefault(x =>
x.Username.ToLower() == username.ToLower()
&& x.Password == password);
}
}
}
Repare que a comparação usa o parâmetro password recebido. Assim, a busca só devolve o usuário quando o nome e a senha conferem.
Em um sistema real, obviamente, as senhas ficam no banco e sempre com hash. Aqui, contudo, o foco é a autenticação JWT, então simplificamos essa parte de propósito.
Definindo a chave secreta
Para finalizar a preparação, precisamos de uma chave para assinar o token. Essa chave é uma string que só o servidor conhece, por isso a chamamos de chave privada.
Para este cenário, crie o arquivo Settings.cs na raiz da aplicação:
namespace Shop
{
public static class Settings
{
// Em produção, leia este valor da configuração ou de um cofre de segredos
public static string Secret = "fedaf7d8863b48e197b9287d492b708e";
}
}
Esse arquivo serve apenas para expor a chave usada pela autenticação JWT, nada além disso. Se quiser deixá-la nas configurações, fica ainda melhor, mas aqui usamos uma classe estática para facilitar.
Vale reforçar que o algoritmo HMAC SHA256 exige uma chave de pelo menos 256 bits. Ou seja, uma string ASCII de 32 caracteres, como a do exemplo, atende o mínimo.
Gerando o token da autenticação JWT
O pacote System.IdentityModel.Tokens.Jwt, que chega como dependência do pacote de JwtBearer, contém a classe JwtSecurityTokenHandler. Ela gera um token a partir das informações que fornecemos.
Essas informações ficam em um SecurityTokenDescriptor, que define, entre outras opções, o usuário, o perfil e a validade. Além disso, as claims definidas ali ficam disponíveis na ClaimsIdentity do ASP.NET Core quando o token volta nas requisições.
Em outras palavras, o handler gera o token e ainda deixa o usuário acessível por User.Identity.Name dentro dos controllers.
Você poderia chamar JwtSecurityTokenHandler.CreateToken direto em um controller. No entanto, prefiro externalizar a geração para um serviço, que pode ser chamado de qualquer lugar sem duplicar código.
Criando o TokenService
Crie uma pasta Services na raiz e, dentro dela, o arquivo TokenService.cs:
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Shop.Models;
namespace Shop.Services
{
public static class TokenService
{
public static string GenerateToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(Settings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
// Claims que vão dentro do payload do token
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role)
}),
// Validade de duas horas
Expires = DateTime.UtcNow.AddHours(2),
// Assinatura com a chave privada
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
}
Acredite ou não, isso é tudo o que precisamos para gerar um token no formato JWT com o ASP.NET Core 3.0.
Entendendo cada parte do token
A claim ClaimTypes.Name guarda o nome do usuário. Por sua vez, a claim ClaimTypes.Role guarda o perfil, que o ASP.NET Core usa para avaliar o [Authorize(Roles = "...")].
A propriedade Expires define até quando o token vale. Depois desse prazo, o middleware recusa o token e a API responde com 401.
Já o SigningCredentials usa a chave do Settings.cs para assinar o token da autenticação JWT. Assim, só o nosso servidor consegue produzir uma assinatura válida, e qualquer alteração no payload invalida o token.
Configurando a autenticação JWT no Startup
Até aqui, temos o usuário, o repositório e um serviço que gera o token com as claims do usuário. Agora precisamos informar à aplicação que vamos trabalhar com autenticação, autorização e tokens no formato JWT.
Dessa forma, a autenticação JWT vai validar o token a cada requisição. Com isso, conseguiremos definir quais perfis têm acesso a cada ação dos controllers.
Registrando o esquema Bearer da autenticação JWT
O primeiro passo é avisar ao ASP.NET Core que usamos autenticação. Para isso, chamamos AddAuthentication no método ConfigureServices do Startup.cs:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
});
Ao registrar a autenticação, também informamos o esquema padrão, neste caso o JwtBearer. Além disso, definimos o mesmo esquema para o challenge, que é a resposta enviada quando o usuário não está autenticado.
Configurando a validação do token
Depois de escolher o JwtBearer, precisamos configurá-lo com AddJwtBearer, ainda no ConfigureServices. O método final fica assim:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
var key = Encoding.ASCII.GetBytes(Settings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
// Apenas para desenvolvimento, em produção mantenha true
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
}
O ValidateIssuerSigningKey garante que o middleware da autenticação JWT confira a assinatura com a nossa chave. Por outro lado, desligamos a validação de issuer e audience porque o token não define esses valores neste exemplo.
Em cenários com mais de uma API ou com um provedor de identidade, vale ligar essas validações. Nesse caso, preencha ValidIssuer e ValidAudience com os mesmos valores usados na geração do token.
Ativando os middlewares da autenticação JWT
Para fechar, precisamos dizer ao ASP.NET Core que de fato usamos autenticação e autorização. Isso acontece no método Configure, com as linhas abaixo:
app.UseAuthentication();
app.UseAuthorization();
Como resultado, o Startup.cs completo fica assim:
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace Shop
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
var key = Encoding.ASCII.GetBytes(Settings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
// A ordem importa: autenticação antes da autorização
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Observe a sequência: UseRouting, UseCors, UseAuthentication, UseAuthorization e, por fim, UseEndpoints. Essa é exatamente a ordem que a documentação do ASP.NET Core 3.0 recomenda.
Se você inverter autenticação e autorização, o middleware de autorização vai avaliar um usuário ainda anônimo. Consequentemente, todos os endpoints protegidos vão responder com 401, mesmo com um token válido.
Isso é tudo que precisamos informar ao ASP.NET Core para a autenticação JWT e a autorização funcionarem.
Autenticando o usuário
Para este exemplo, criei o arquivo HomeController.cs na pasta Controllers. Toda a autenticação e a autorização ficam concentradas nele.
Se você ainda não conhece o novo modelo de controllers do ASP.NET Core 3, veja este vídeo que publiquei no YouTube. Ele serve de base para o código abaixo.
Seguindo esse modelo, o método de login fica assim:
[HttpPost]
[Route("login")]
[AllowAnonymous]
public ActionResult<dynamic> Authenticate([FromBody] User model)
{
// Recupera o usuário
var user = UserRepository.Get(model.Username, model.Password);
// Verifica se o usuário existe
if (user == null)
return NotFound(new { message = "Invalid username or password" });
// Gera o token
var token = TokenService.GenerateToken(user);
// Oculta a senha antes de devolver os dados
user.Password = "";
// Retorna o usuário e o token
return new
{
user = user,
token = token
};
}
Como o método não executa nenhuma operação assíncrona, ele devolve ActionResult<dynamic> diretamente. Assim, evitamos o aviso do compilador sobre um método async sem await.
Agora, rode a API com dotnet watch run para testar a autenticação JWT. Em seguida, faça uma requisição POST no Postman para https://localhost:5001/v1/account/login com o seguinte corpo:
{ "username": "robin", "password": "robin" }
Como resultado, você recebe os dados do usuário Robin e um token com o nome e o perfil dele. A partir de então, esse token libera as próximas requisições à API.
Autorizando com roles na autenticação JWT
Para deixar a brincadeira mais interessante, vamos criar mais quatro métodos no HomeController. Um deles aceita qualquer usuário, outro exige apenas autenticação e os dois últimos ficam restritos a funcionários e gerentes.
Essas regras ficam a cargo de três atributos:
[AllowAnonymous]permite o acesso sem autenticação alguma.[Authorize]exige que o usuário esteja autenticado, mas não verifica o perfil.[Authorize(Roles = "Role1,Role2")]exige autenticação e que o usuário pertença a um dos perfis listados, separados por vírgula.
Dessa forma, os métodos ficam decorados assim:
[HttpGet]
[Route("anonymous")]
[AllowAnonymous]
public string Anonymous() => "Anonymous";
[HttpGet]
[Route("authenticated")]
[Authorize]
public string Authenticated() => $"Authenticated - {User.Identity.Name}";
[HttpGet]
[Route("employee")]
[Authorize(Roles = "employee,manager")]
public string Employee() => "Employee";
[HttpGet]
[Route("manager")]
[Authorize(Roles = "manager")]
public string Manager() => "Manager";
Quando você lista vários perfis no mesmo atributo, basta que o usuário pertença a um deles. Por outro lado, se aplicar dois atributos [Authorize] no mesmo método, o usuário precisa satisfazer os dois.
O HomeController completo
Como resultado final, o HomeController fica com este código:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Shop.Models;
using Shop.Repositories;
using Shop.Services;
namespace Shop.Controllers
{
[Route("v1/account")]
public class HomeController : Controller
{
[HttpPost]
[Route("login")]
[AllowAnonymous]
public ActionResult<dynamic> Authenticate([FromBody] User model)
{
var user = UserRepository.Get(model.Username, model.Password);
if (user == null)
return NotFound(new { message = "Invalid username or password" });
var token = TokenService.GenerateToken(user);
user.Password = "";
return new
{
user = user,
token = token
};
}
[HttpGet]
[Route("anonymous")]
[AllowAnonymous]
public string Anonymous() => "Anonymous";
[HttpGet]
[Route("authenticated")]
[Authorize]
public string Authenticated() => $"Authenticated - {User.Identity.Name}";
[HttpGet]
[Route("employee")]
[Authorize(Roles = "employee,manager")]
public string Employee() => "Employee";
[HttpGet]
[Route("manager")]
[Authorize(Roles = "manager")]
public string Manager() => "Manager";
}
}
Testando a autenticação JWT nos endpoints protegidos
Agora salve o arquivo e faça quatro requisições GET no Postman:
https://localhost:5001/v1/account/anonymous
https://localhost:5001/v1/account/authenticated
https://localhost:5001/v1/account/employee
https://localhost:5001/v1/account/manager
Não esqueça de enviar o token na aba Headers das requisições que exigem autenticação. O cabeçalho segue este formato:
Authorization: Bearer YOUR_TOKEN
Com o token do Robin, as três primeiras rotas respondem normalmente. Já a rota manager devolve 403, porque o Robin está autenticado, mas não tem o perfil exigido.
Sem token, por sua vez, apenas a rota anonymous responde. As demais retornam 401, indicando que a requisição não trouxe uma credencial válida.
Na autenticação JWT, essa diferença entre 401 e 403 ajuda bastante no diagnóstico. Afinal, o 401 aponta problema no token, enquanto o 403 aponta falta de permissão.
Recuperando o usuário logado
Você deve ter percebido que o método Authenticated lê o nome do usuário logado. Isso acontece pela propriedade User.Identity.Name, que o middleware preenche sempre que um token válido chega no cabeçalho.
Além disso, o ClaimsPrincipal do ASP.NET Core oferece métodos como User.IsInRole("manager") e User.FindFirst(ClaimTypes.Role). Todos esses valores vêm das claims que a autenticação JWT recebeu do TokenService.
Portanto, se precisar de mais dados do usuário, como o id ou o e-mail, basta adicionar novas claims na geração do token. Se quiser se aprofundar nesse assunto, veja como customizar claims no ASP.NET.
Cuidados ao levar a autenticação JWT para produção
O exemplo deste artigo foi pensado para ensinar o fluxo, não para ir direto à produção. Por isso, alguns pontos merecem atenção antes de publicar a sua API.
Primeiro, tire a chave secreta da autenticação JWT do código e leve para a configuração, para variáveis de ambiente ou para um cofre de segredos. Assim, ela não vai parar no controle de versão.
Em seguida, mantenha RequireHttpsMetadata como true e sirva a API apenas por HTTPS. Afinal, o token trafega no cabeçalho e precisa de um canal protegido.
Além disso, nunca guarde senhas em texto puro. Use hash com algoritmos próprios para senha e compare sempre o hash, nunca o valor original.
Também vale restringir o CORS. O AllowAnyOrigin facilita os testes, porém em produção o ideal é liberar apenas as origens que de fato consomem a API.
Por fim, defina uma validade curta para o token e avalie o uso de refresh tokens. Para uma lista mais completa, confira as boas práticas de segurança em APIs ASP.NET Core.
Confira também
Conclusão
Neste artigo, você montou a autenticação JWT no ASP.NET Core 3.0 do zero, com um repositório simples, um serviço de token e um controller com rotas protegidas por roles. Tudo isso sem banco de dados e com poucas linhas de configuração.
O ponto mais sensível da versão 3.0 é a ordem dos middlewares. Portanto, mantenha UseAuthentication e UseAuthorization entre UseRouting e UseEndpoints, nessa ordem.
Com essa base, a autenticação JWT fica pronta para evoluir com policies, claims customizadas e um provedor de identidade. Para projetos novos, contudo, prefira a versão atualizada deste conteúdo, que acompanha as versões mais recentes do framework.
FAQ
Preciso do pacote JwtBearer para usar autenticação JWT no ASP.NET Core 3?
Sim, o pacote Microsoft.AspNetCore.Authentication.JwtBearer traz o handler que valida o token. Use a versão compatível com o framework, como a 3.0.0.
O token JWT é criptografado?
Não, um JWT assinado tem o payload apenas codificado em Base64Url. Por isso, não coloque senhas ou dados sensíveis dentro dele.
Por que minha API retorna 401 mesmo com o token correto?
Na maioria dos casos, a ordem dos middlewares está errada. Confira se UseAuthentication vem antes de UseAuthorization e se ambos ficam entre UseRouting e UseEndpoints.
Qual a diferença entre 401 e 403?
O 401 indica que o token está ausente, expirado ou inválido. Já o 403 indica que o usuário está autenticado, mas não tem o perfil exigido.
Como exigir mais de um perfil ao mesmo tempo?
Aplique vários atributos [Authorize] no mesmo método. Dentro de um único atributo, os perfis separados por vírgula funcionam como "ou".