Autenticação e autorização com JWT e Bearer

A autenticação JWT no ASP.NET 7 permite que uma API identifique o usuário a partir de um token assinado, enviado no cabeçalho de cada requisição. Neste guia, você vai gerar esses tokens, configurar o esquema Bearer, proteger rotas e aplicar autorização com roles e policies em uma Minimal API.

O que é autenticação JWT com Bearer

Um JWT (JSON Web Token) é um token compacto dividido em três partes: cabeçalho, payload e assinatura. O cabeçalho informa o algoritmo, o payload carrega as informações do usuário e a assinatura garante que ninguém alterou o conteúdo.

Por outro lado, o Bearer define como o token trafega entre cliente e servidor. Por isso ouvimos tanto falar em "JWT Bearer": o Bearer é o meio de transporte, enquanto o JWT é o formato do token.

Na prática, a autenticação JWT funciona assim: o servidor assina o token com uma chave privada, e só quem tem essa chave consegue validar a assinatura. Se o conteúdo mudar, a assinatura deixa de bater e o token se torna inválido.

Caso queira revisar os conceitos antes de seguir, o artigo o que é JWT explica a estrutura do token com mais detalhes. Além disso, a especificação oficial está na RFC 7519.

Criando a API no ASP.NET 7

O primeiro passo funciona com qualquer template do .NET que suporte ASP.NET, como MVC, Razor Pages ou Minimal APIs. Neste exemplo, escolhi as Minimal APIs porque são mais simples e deixam o foco no que realmente interessa.

Também não vamos cobrir acesso a dados aqui. Portanto, as informações do usuário ficam simuladas, já que o foco é a autenticação JWT.

O comando abaixo cria o projeto, entra na pasta e instala o único pacote necessário, o Microsoft.AspNetCore.Authentication.JwtBearer.

dotnet new web -o JwtAspNetBlazor
cd JwtAspNetBlazor
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

O ASP.NET já traz um middleware com suporte nativo à autenticação JWT. Assim, boa parte do trabalho pesado da autenticação JWT já vem pronta dentro do framework.

Chave privada e configuração

Na autenticação JWT, apenas o servidor deve conhecer a chave que assina e valida os tokens. Para fins didáticos, vamos guardá-la em uma classe estática de fácil acesso.

// Configuration.cs
namespace JwtAspNetBlazor;

public static class Configuration
{
    // Apenas para fins didáticos, nunca deixe a chave fixa no código
    public static string PrivateKey { get; set; } = "5+IV)E2glD3xCH2rNTElZ_at9(TbG1N(E=pH)29*";
}

No entanto, essa chave está fixa no código, o que não é uma boa prática. O ideal é guardar esse valor nas configurações da aplicação, em variáveis de ambiente ou em um cofre de segredos.

Também nunca faça commit dessa chave, porque qualquer pessoa com acesso ao repositório passaria a conhecê-la. Por fim, a chave de produção deve ser outra, e ninguém do time deve ter acesso a ela.

Vale lembrar ainda que o algoritmo HMAC SHA256 exige uma chave com pelo menos 256 bits. Em outras palavras, use uma string longa e aleatória, com no mínimo 32 bytes.

Modelo de usuário

Em seguida, criamos um objeto com as informações do usuário, como Id, e-mail e perfis. Ele pode ser simples ou complexo, pois para a geração do token serve apenas para transportar dados.

// Models/User.cs
namespace JwtAspNetBlazor.Models;

public record User(int Id, string Email, string Password, string[] Roles);

Sinta-se à vontade para adicionar ou remover propriedades conforme a necessidade do seu projeto.

Como gerar o token da autenticação JWT

A geração do token na autenticação JWT fica a cargo da classe JwtSecurityTokenHandler. O pacote cuida do cabeçalho e da assinatura, então sobra para nós apenas montar o payload.

Essa classe tem dois métodos principais. O CreateToken cria o token, enquanto o WriteToken serializa o resultado na string que a API devolve.

// Cria uma instância do JwtSecurityTokenHandler
var handler = new JwtSecurityTokenHandler();

// ... montagem do tokenDescriptor

// Gera o token e depois a sua representação em string
var token = handler.CreateToken(tokenDescriptor);
var tokenString = handler.WriteToken(token);

Assinando o token

O próximo passo é assinar o token com a chave privada. Para isso, usamos o SigningCredentials, que recebe uma chave simétrica e o algoritmo de assinatura.

O algoritmo mais usado nesse cenário é o HMAC SHA256, disponível na constante SecurityAlgorithms.HmacSha256Signature. Contudo, o SymmetricSecurityKey espera um array de bytes, e não uma string.

// Converte a chave (string) para um array de bytes
var key = Encoding.ASCII.GetBytes(Configuration.PrivateKey);

var credentials = new SigningCredentials(
    new SymmetricSecurityKey(key),
    SecurityAlgorithms.HmacSha256Signature);

Com isso, temos as credenciais que assinam e, mais tarde, validam os tokens da autenticação JWT.

Token descriptor e expiração

O método CreateToken espera um SecurityTokenDescriptor, objeto que reúne credenciais, payload e tempo de vida do token. Já temos as credenciais, então falta pouco.

var tokenDescriptor = new SecurityTokenDescriptor
{
    // Tempo de vida do token, sempre em UTC
    Expires = DateTime.UtcNow.AddHours(2),
    SigningCredentials = credentials,
};

A propriedade Expires define quando o token deixa de valer. Use datas em UTC, principalmente quando os servidores rodam em regiões com fusos diferentes.

Neste exemplo usamos duas horas, mas o tempo de vida fica a seu critério. Tokens curtos reduzem o risco em caso de vazamento, enquanto tokens longos exigem menos logins.

Claims e ClaimsIdentity

O payload pode ser montado de várias formas, porém recomendo usar o ClaimsIdentity por causa da integração com o ASP.NET. Um claim é uma afirmação no formato chave e valor, como dizer que o nome do usuário é "André Baltieri".

Embora você possa usar qualquer nome como chave, o ASP.NET reconhece alguns nomes padrão. Quando você usa as constantes de ClaimTypes, o framework popula o HttpContext automaticamente durante a requisição.

// Chave personalizada: o ASP.NET não reconhece
new Claim("UserName", "André Baltieri");
// User.Identity.Name retorna null

// Chave padrão: o ASP.NET reconhece automaticamente
new Claim(ClaimTypes.Name, "André Baltieri");
new Claim(ClaimTypes.Role, "premium");
// User.Identity.Name retorna "André Baltieri"
// User.IsInRole("premium") retorna true

Dessa forma, basta entregar ao descriptor um ClaimsIdentity, que nada mais é do que um conjunto de claims. O método abaixo recebe um User e gera os claims com base no e-mail e nos perfis.

private static ClaimsIdentity GenerateClaims(User user)
{
    var identity = new ClaimsIdentity();
    identity.AddClaim(new Claim(ClaimTypes.Name, user.Email));

    // Um usuário pode ter vários perfis
    foreach (var role in user.Roles)
        identity.AddClaim(new Claim(ClaimTypes.Role, role));

    return identity;
}

Aqui usamos o e-mail como Name, mas você pode escolher outra propriedade. Além disso, não há problema em ter vários claims do tipo ClaimTypes.Role, ao contrário de Name, Id ou Email.

Token service e injeção de dependência

Para fechar a geração, reunimos a lógica da autenticação JWT em um serviço que podemos chamar sempre que precisarmos de um token. A sugestão é criar o arquivo em Services/TokenService.cs.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using JwtAspNetBlazor.Models;
using Microsoft.IdentityModel.Tokens;

namespace JwtAspNetBlazor.Services;

public class TokenService
{
    public string Generate(User user)
    {
        var handler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(Configuration.PrivateKey);
        var credentials = new SigningCredentials(
            new SymmetricSecurityKey(key),
            SecurityAlgorithms.HmacSha256Signature);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = GenerateClaims(user),
            Expires = DateTime.UtcNow.AddHours(2),
            SigningCredentials = credentials,
        };

        var token = handler.CreateToken(tokenDescriptor);
        return handler.WriteToken(token);
    }

    private static ClaimsIdentity GenerateClaims(User user)
    {
        var identity = new ClaimsIdentity();
        identity.AddClaim(new Claim(ClaimTypes.Name, user.Email));
        foreach (var role in user.Roles)
            identity.AddClaim(new Claim(ClaimTypes.Role, role));

        return identity;
    }
}

O método Generate recebe um User e devolve o token como string. Em seguida, registramos o serviço no container de injeção de dependência com o tempo de vida transiente, pois ele não guarda estado.

// Program.cs
using JwtAspNetBlazor.Models;
using JwtAspNetBlazor.Services;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<TokenService>();

var app = builder.Build();

// Endpoint de login simplificado, sem validação de senha
app.MapPost("/login", (User user, TokenService tokenService)
    => tokenService.Generate(user));

app.Run();

Se quiser entender melhor os tempos de vida dos serviços, o artigo sobre injeção de dependência no ASP.NET Core aprofunda o tema. Lembre também que este endpoint é só um exemplo, sem tratamento de erros nem padronização de retorno.

Testando a geração do token

Antes de proteger qualquer rota, vale confirmar que o token da autenticação JWT sai corretamente. Execute a aplicação com dotnet run e faça uma requisição POST para /login, de preferência pelo Postman.

Postman com requisição POST para o endpoint de login da API de autenticação JWT, com corpo JSON contendo e-mail e perfis
Requisição de login montada no Postman, com o método POST selecionado.

A porta da URL vai variar na sua máquina. No corpo da requisição, selecione Raw e o formato JSON, com o conteúdo abaixo.

{
    "email": "test@balta.io",
    "roles": ["admin", "premium"]
}

Como resposta, a API devolve uma string longa no formato xxxxx.yyyyy.zzzzz. Cada bloco separado por ponto corresponde a uma das três partes do token.

Inspecionando o token no jwt.io

Com o token em mãos, cole o valor no jwt.io para ver o conteúdo decodificado.

Site jwt.io exibindo o cabeçalho e o payload decodificados de um token da autenticação JWT
O jwt.io separa cabeçalho, payload e assinatura do token recebido.

Se tudo deu certo, você verá algo parecido com o conteúdo abaixo. Repare que o ClaimTypes.Name virou unique_name e os perfis aparecem em role.

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "unique_name": "test@balta.io",
  "role": ["admin", "premium"],
  "nbf": 1684503947,
  "exp": 1684511147,
  "iat": 1684503947
}

Configurando a autenticação JWT no pipeline

Na autenticação JWT, gerar tokens e proteger rotas são tarefas ligadas, mas distintas. Inclusive, seria possível ter uma API dedicada apenas a emitir tokens.

Agora vamos restringir o acesso usando os serviços AddAuthentication e AddAuthorization. O primeiro descobre quem o usuário é, e o segundo decide o que ele pode fazer.

Ordem dos middlewares

Adicionar um middleware ao pipeline costuma ter duas partes: registrar o serviço (Add) e usar o middleware (Use). Além disso, a ordem importa, porque cada middleware é um passo a mais na execução da requisição.

Primeiro autenticamos e depois autorizamos, afinal precisamos saber quem é o usuário antes de dizer o que ele pode fazer. Por isso, deixo essas chamadas o mais perto possível da criação do builder e do app.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

Se a API não trabalha com perfis, claims ou policies, você pode dispensar a autorização e manter apenas a autenticação. Os middlewares adicionam passos à requisição, mas são bem otimizados.

Esquema Bearer na autenticação JWT

Até aqui, só dissemos ao ASP.NET que queremos autenticação. Falta informar como ela acontece, ou seja, que o token chega no formato JWT pelo cabeçalho.

No padrão Bearer, o cliente envia um cabeçalho Authorization com a palavra Bearer, um espaço e o token. Essa palavra é necessária porque existem outros esquemas, como o Basic, que apenas codifica usuário e senha em Base64.

curl --location 'http://localhost:5049/hello' \
  --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

O ASP.NET já sabe extrair esse valor sozinho. Portanto, basta informar o esquema padrão pelo DefaultAuthenticateScheme e o DefaultChallengeScheme.

O challenge é o processo de desafiar o cliente quando ele não está autenticado. O token poderia vir de provedores externos, como Google ou Microsoft, mas aqui a autenticação JWT é interna: o token nasce neste servidor e vale apenas para ele.

Validando o token da autenticação JWT

Por fim, o ASP.NET precisa fazer o caminho inverso do TokenService. Ele valida a assinatura e popula o ClaimsPrincipal, que representa o usuário logado, e para isso só precisa conhecer a chave.

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        // Permite HTTP sem TLS (use true em produção)
        options.RequireHttpsMetadata = false;
        // Salva o token nas AuthenticationProperties
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            // Mesma chave usada para assinar o token
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.ASCII.GetBytes(Configuration.PrivateKey)),
            // Validações de emissor e público desligadas neste exemplo
            ValidateIssuer = false,
            ValidateAudience = false
        };
    });
builder.Services.AddAuthorization();

Em produção, considere ativar a validação de emissor e público. Assim, a API só aceita tokens emitidos por quem você espera e destinados a ela.

Restringindo rotas e obtendo o usuário logado

Com a autenticação JWT configurada, restringimos as rotas com RequireAuthorization nas Minimal APIs ou com o atributo [Authorize] em MVC e Razor Pages.

// Minimal APIs
app.MapGet("/hello", () => "Hello World")
    .RequireAuthorization();

// MVC
[Authorize]
public IActionResult Get() => Ok();

Embora o nome remeta à autorização, sem perfil, claim ou policy o recurso serve apenas como autenticação. Na prática, um acesso sem token retorna 401 Unauthorized, enquanto um token sem o requisito exigido retorna 403 Forbidden.

Imagine agora uma rota em que o usuário altera os próprios dados. Se o Id vier na URL, ele pode trocar o valor e tentar editar outra conta.

A forma mais segura é usar as informações do token da autenticação JWT, porque ninguém consegue alterá-las sem invalidar a assinatura. Como usamos ClaimTypes.Name, o HttpContext já traz o nome em User.Identity.Name.

// MVC: o controller expõe a propriedade User
[Authorize]
public IActionResult Get()
{
    var userName = User.Identity?.Name ?? string.Empty;
    return Ok(userName);
}

// Minimal APIs: injete o HttpContext ou o ClaimsPrincipal
app.MapGet("/me", (HttpContext context)
        => Results.Ok(context.User.Identity?.Name ?? string.Empty))
    .RequireAuthorization();

O usuário e o Name podem ser nulos, por isso usamos a coalescência nula. Além disso, só faz sentido ler esses dados em rotas protegidas.

Autorização com roles, claims e policies

A autenticação JWT diz quem é o usuário, mas isso não basta, já que muitas vezes precisamos classificá-lo. Por exemplo, batman@balta.io pode ser administrador, robin@balta.io apenas visualiza dados e alfred@balta.io também acessa o financeiro.

Um role é um perfil, uma string simples que qualifica o usuário. Já um claim é uma afirmação, como dizer que o usuário X possui o perfil Y.

As policies são o recurso mais poderoso, pois são trechos de código que podem exigir perfis ou até consultar dados. Contudo, elas rodam em toda chamada a uma rota protegida, então evite acessos caros ao banco ou a serviços externos.

Criando policies na autenticação JWT

Criar uma policy é simples: chame o AddPolicy com um nome e as regras que ela aplica. Em seguida, registre as policies como opção do AddAuthorization.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Admin", policy => policy.RequireRole("manager"));
    options.AddPolicy("Employee", policy => policy.RequireRole("employee"));
});

A policy Admin exige o perfil manager, enquanto a policy Employee exige o perfil employee. Depois, basta informar o nome da policy no RequireAuthorization de cada rota.

app.MapGet("/employee", (ClaimsPrincipal user) =>
    Results.Ok(new { message = $"Authenticated as {user.Identity?.Name}" }))
    .RequireAuthorization("Employee");

app.MapGet("/manager", (ClaimsPrincipal user) =>
    Results.Ok(new { message = $"Authenticated as {user.Identity?.Name}" }))
    .RequireAuthorization("Admin");

Nas Minimal APIs, o ClaimsPrincipal encurta o caminho até o usuário logado. Comece sempre pelo simples e só escale quando precisar, assim a API continua performática.

Testando a autenticação JWT no Postman

Para facilitar os testes da autenticação JWT, o repositório do projeto no GitHub traz uma coleção do Postman com os endpoints prontos. Rode a API com dotnet run e importe o arquivo endpoints.postman_collection em File, Import.

Tela de importação do Postman com o arquivo da coleção de endpoints selecionado
Importação da coleção de endpoints pelo menu File do Postman.

Com isso, a coleção aparece no menu lateral. Se a sua API subir em outra porta, ajuste a URL de cada requisição.

Menu lateral do Postman listando a coleção Segurança em APIs ASP.NET com JWT e Bearer Authentication
A coleção importada fica disponível no menu lateral.

Comece pelo login, que neste exemplo não valida senha. Guarde o token retornado, pois ele será usado nas próximas chamadas.

Requisição de login no Postman retornando um token de autenticação JWT
O endpoint de login devolve o token que usaremos nos testes.

Erro 401 Unauthorized

Ao chamar o endpoint de funcionários sem token, recebemos 401 Unauthorized. Apesar da mensagem, esse erro indica que não estamos autenticados.

Postman exibindo status 401 Unauthorized ao acessar o endpoint employee sem token
Sem token no cabeçalho, a API responde com 401.

Para enviar o token, abra a aba Authorization, selecione o tipo Bearer Token e cole o valor no campo indicado.

Aba Authorization do Postman com o tipo Bearer Token selecionado para a autenticação JWT
O Postman monta o cabeçalho Authorization a partir do tipo Bearer Token.

Erro 403 Forbidden

Com o token informado, a autenticação JWT funciona e o retorno muda para 403 Forbidden. Agora estamos autenticados, mas o token não tem o perfil employee exigido pela policy.

Postman exibindo status 403 Forbidden ao acessar o endpoint employee com token sem o perfil exigido
Autenticado, mas sem permissão: a API responde com 403.

Então geramos um novo token, desta vez com o perfil employee no corpo do login.

Requisição de login no Postman enviando o perfil employee para gerar um novo token
Novo login incluindo o perfil employee entre os roles.

Por fim, atualizamos o cabeçalho da requisição com o novo token. A API responde 200 OK, já que o token agora atende à policy do endpoint.

Postman exibindo status 200 OK no endpoint employee com a mensagem de usuário autenticado
Com o perfil correto, o endpoint devolve 200 OK.

Novidades do ASP.NET 7 para autenticação JWT

O ASP.NET Core 7 trouxe mudanças que reduzem o código de configuração. Primeiro, quando a aplicação registra um único esquema, ele passa a ser o padrão, conforme a nota de mudança da Microsoft.

Na prática, AddAuthentication().AddJwtBearer() já basta quando o JWT é o único esquema. Mesmo assim, informar o esquema de forma explícita, como fizemos, deixa a intenção clara e evita surpresas ao adicionar outros esquemas.

Além disso, o WebApplication registra os middlewares de autenticação e autorização automaticamente quando os serviços existem. Você ainda pode chamá-los de forma explícita para controlar a ordem, por exemplo depois do CORS.

Outra novidade é a ferramenta dotnet user-jwts, que cria tokens para testes locais. Ela também adiciona ao appsettings.json as opções que validam esses tokens, o que facilita testar a autenticação JWT sem um endpoint de login.

# Cria um token de desenvolvimento com o perfil admin
dotnet user-jwts create --role "admin"

Para um panorama mais amplo de proteção de APIs, veja também as boas práticas de segurança em APIs ASP.NET Core.

Conclusão sobre autenticação JWT

Neste guia, implementamos um fluxo completo de autenticação JWT no ASP.NET 7. Geramos tokens assinados, configuramos o esquema Bearer, protegemos rotas e aplicamos autorização com roles e policies.

O ponto central é separar bem as responsabilidades: autenticação diz quem é o usuário, enquanto autorização diz o que ele pode fazer. Com isso em mente, fica fácil evoluir a API para cenários mais exigentes.

Antes de ir para produção, tire a chave do código e ative a validação de emissor e público. Todo o código está no repositório do balta.io no GitHub.

FAQ

Qual a diferença entre JWT e Bearer?

No JWT temos o formato do token, com cabeçalho, payload e assinatura. Já o Bearer é o esquema que define como o token trafega, no cabeçalho Authorization.

Quando a API retorna 401 e quando retorna 403?

Um 401 indica que o usuário não está autenticado, geralmente por falta de token ou token inválido. O 403 indica que ele está autenticado, mas não atende ao perfil ou à policy da rota.

Preciso chamar UseAuthentication e UseAuthorization no .NET 7?

Não necessariamente, pois o WebApplication registra esses middlewares automaticamente quando os serviços existem. Chame de forma explícita quando precisar controlar a ordem no pipeline.

Onde devo guardar a chave privada do token?

Nunca no código ou no repositório. Use configurações por ambiente, variáveis de ambiente ou um cofre de segredos, com uma chave exclusiva para produção.

Posso ter vários perfis no mesmo token?

Sim, basta adicionar vários claims do tipo ClaimTypes.Role. O ASP.NET avalia todos eles no IsInRole e nas policies.