No artigo sobre o JWT Decoder você viu o que existe dentro de um token: header, payload e assinatura. Agora vamos para o outro lado do balcão — emitir e validar esses tokens numa API ASP.NET Core, protegendo endpoints e implementando o fluxo completo de autenticação: login, access token, endpoints protegidos e refresh token com rotação.
Autenticação é uma daquelas coisas que parecem simples até você reparar nos detalhes que quebram a segurança: uma chave fraca, a expiração que nunca chega, o refresh token que nunca é invalidado. Este guia é um passo a passo prático para .NET 10, usando as APIs atuais (nada de código legado) e apontando as armadilhas no caminho.
Como o JWT funciona (versão rápida)
Um JWT tem três partes separadas por ponto: header.payload.signature. Header e payload são apenas Base64Url — não são criptografados, qualquer um lê. Quem garante que o token não foi adulterado é a assinatura, calculada com uma chave secreta que só o servidor conhece.
A consequência prática, que vale gravar: a assinatura protege a integridade, não a confidencialidade. Nunca coloque senha ou dado sensível no payload — colar o token em qualquer decoder revela tudo.
O fluxo que vamos montar usa dois tokens com papéis diferentes:
- Access token — um JWT de vida curta (5 a 15 minutos), enviado a cada requisição no header
Authorization: Bearer. É stateless: a API valida a assinatura e a expiração sem consultar banco. - Refresh token — uma string opaca de vida longa (dias), guardada no servidor. Serve para obter um novo access token quando o antigo expira, sem pedir login de novo. Como vive no servidor, pode ser revogado.
Pré-requisitos
- SDK do .NET 10 instalado (
dotnet --version→10.0.x). - Uma API Web. Se estiver começando do zero:
dotnet new webapi -n AuthDemo.
Passo 1 — Instalar os pacotes
# Valida os tokens no middleware
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
# Cria e assina os tokens (handler moderno)
dotnet add package Microsoft.IdentityModel.JsonWebTokens
Versões: use a família
10.xdoJwtBearer(alinhada ao runtime) e a8.xdoMicrosoft.IdentityModel.*. OJwtBearerjá trazMicrosoft.IdentityModel.Tokenscomo dependência, então os tipos base (TokenValidationParameters,SymmetricSecurityKey) vêm junto.
⚠️ Não use
System.IdentityModel.Tokens.Jwt(JwtSecurityTokenHandler) em código novo — é considerado legado. Desde o .NET 8 o padrão é oJsonWebTokenHandler, ~30% mais rápido, assíncrono e compatível com AOT.
Passo 2 — Configurar a chave e o appsettings
Adicione uma seção Jwt ao appsettings.json. A chave do HS256 precisa ter no mínimo 32 bytes (256 bits) — uma chave menor lança exceção na hora de assinar.
{
"Jwt": {
"Issuer": "https://minha-api.com",
"Audience": "https://meu-app.com",
"Key": "troque-esta-chave-por-uma-secreta-com-32-bytes-ou-mais"
}
}
🔒 Nunca faça commit da chave real. Em desenvolvimento use
dotnet user-secrets set "Jwt:Key" "..."; em produção, variáveis de ambiente ou um cofre (Google Secret Manager, Azure Key Vault). A chave emappsettings.jsonacima é só para o tutorial rodar.
Passo 3 — Configurar a autenticação no Program.cs
Aqui a API aprende a validar os tokens que chegam:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var jwt = builder.Configuration.GetSection("Jwt");
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["Key"]!));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.MapInboundClaims = false; // veja a nota abaixo — importante!
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwt["Issuer"],
ValidateAudience = true,
ValidAudience = jwt["Audience"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ClockSkew = TimeSpan.Zero // expiração estrita
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication(); // SEMPRE antes de UseAuthorization
app.UseAuthorization();
app.Run();
Dois detalhes que quase todo tutorial erra:
ClockSkew = TimeSpan.Zero — por padrão o ClockSkew é de 5 minutos, uma tolerância para relógios dessincronizados entre servidores. Na prática, isso faz um token “expirado” continuar válido por até 5 min. Se seus servidores têm relógio sincronizado (NTP) e os access tokens são curtos, zere o skew para expiração exata.
MapInboundClaims = false — este é o gotcha clássico. Por padrão, o JwtBearer remapeia o claim sub para uma URI gigante (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier). Com MapInboundClaims = false, os claims chegam com o nome original (sub, email, role), como você espera. Sem isso, User.FindFirst("sub") volta null e você perde tempo achando que é bug.
Ordem importa: UseAuthentication() sempre vem antes de UseAuthorization(). Autenticar é “quem é você”; autorizar é “você pode”.
Passo 4 — Gerar o access token
O JsonWebTokenHandler.CreateToken recebe um SecurityTokenDescriptor e devolve a string do JWT direto (o handler legado exigia um passo a mais com WriteToken).
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
public sealed class TokenService(IConfiguration config)
{
public string CreateAccessToken(Guid userId, string email, string role)
{
var keyBytes = Encoding.UTF8.GetBytes(config["Jwt:Key"]!);
var credentials = new SigningCredentials(
new SymmetricSecurityKey(keyBytes),
SecurityAlgorithms.HmacSha256); // gera "alg": "HS256"
var now = DateTime.UtcNow;
var descriptor = new SecurityTokenDescriptor
{
Issuer = config["Jwt:Issuer"],
Audience = config["Jwt:Audience"],
IssuedAt = now,
NotBefore = now,
Expires = now.AddMinutes(10), // access token curto
SigningCredentials = credentials,
Claims = new Dictionary<string, object>
{
[JwtRegisteredClaimNames.Sub] = userId.ToString(),
[JwtRegisteredClaimNames.Email] = email,
["role"] = role,
[JwtRegisteredClaimNames.Jti] = Guid.NewGuid().ToString()
}
};
return new JsonWebTokenHandler().CreateToken(descriptor);
}
}
Registre o serviço no Program.cs: builder.Services.AddSingleton<TokenService>();
Passo 5 — O refresh token (com rotação)
Aqui está a parte que separa um tutorial de brinquedo de uma implementação segura. O refresh token não é um JWT — é uma string aleatória opaca, guardada no servidor com hash. Regras de ouro (alinhadas ao RFC 9700 e ao OWASP):
- Aleatório e opaco: gerado com um RNG criptográfico.
- Armazenado com hash (nunca em texto puro), associado ao usuário, com expiração.
- Rotação: a cada refresh, o token antigo é invalidado e um novo é emitido.
- Detecção de reúso: se um token já rotacionado for usado de novo, é sinal de roubo → revogue toda a família de tokens e force novo login.
Gerar um token seguro e o hash de armazenamento:
using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication;
public static class RefreshTokens
{
// 256 bits de aleatoriedade, em base64url
public static string Generate() =>
Base64UrlTextEncoder.Encode(RandomNumberGenerator.GetBytes(32));
// guardamos apenas o hash no banco
public static string Hash(string token) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
}
A entidade persistida (via EF Core, por exemplo) guarda o essencial para rotação e revogação:
public sealed class RefreshToken
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string TokenHash { get; set; } = ""; // nunca o token em si
public Guid FamilyId { get; set; } // agrupa a cadeia de rotações
public DateTime ExpiresUtc { get; set; }
public bool IsUsed { get; set; } // já rotacionado?
public bool IsRevoked { get; set; }
}
Passo 6 — Endpoints de login e refresh
Com as peças no lugar, os dois endpoints ficam diretos. (A validação de credenciais — IUserService — fica abstraída; o essencial é que a senha seja verificada com um hash forte, como o PasswordHasher do ASP.NET Core Identity, nunca em texto puro.)
app.MapPost("/auth/login", async (LoginDto dto, IUserService users,
TokenService tokens, IRefreshTokenStore store) =>
{
var user = await users.ValidateCredentialsAsync(dto.Email, dto.Password);
if (user is null) return Results.Unauthorized();
var access = tokens.CreateAccessToken(user.Id, user.Email, user.Role);
var refresh = RefreshTokens.Generate();
await store.AddAsync(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = user.Id,
TokenHash = RefreshTokens.Hash(refresh),
FamilyId = Guid.NewGuid(), // nova família a cada login
ExpiresUtc = DateTime.UtcNow.AddDays(14)
});
return Results.Ok(new { accessToken = access, refreshToken = refresh });
});
app.MapPost("/auth/refresh", async (RefreshDto dto, IUserService users,
TokenService tokens, IRefreshTokenStore store) =>
{
var stored = await store.FindByHashAsync(RefreshTokens.Hash(dto.RefreshToken));
if (stored is null || stored.ExpiresUtc < DateTime.UtcNow)
return Results.Unauthorized();
// REÚSO de um token já rotacionado ou revogado = possível roubo
if (stored.IsUsed || stored.IsRevoked)
{
await store.RevokeFamilyAsync(stored.FamilyId); // mata a cadeia inteira
return Results.Unauthorized();
}
await store.MarkUsedAsync(stored.Id); // rotação: invalida o antigo
var newRefresh = RefreshTokens.Generate();
await store.AddAsync(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = stored.UserId,
TokenHash = RefreshTokens.Hash(newRefresh),
FamilyId = stored.FamilyId, // mantém a mesma família
ExpiresUtc = DateTime.UtcNow.AddDays(14)
});
var user = await users.GetByIdAsync(stored.UserId);
var access = tokens.CreateAccessToken(user.Id, user.Email, user.Role);
return Results.Ok(new { accessToken = access, refreshToken = newRefresh });
});
Passo 7 — Proteger endpoints e ler o usuário
Com a autenticação configurada, exigir um token válido é uma linha:
// Requer qualquer usuário autenticado
app.MapGet("/me", (ClaimsPrincipal user) =>
{
var sub = user.FindFirstValue(JwtRegisteredClaimNames.Sub);
var email = user.FindFirstValue(JwtRegisteredClaimNames.Email);
return Results.Ok(new { sub, email });
}).RequireAuthorization();
// Requer o papel "Admin"
app.MapDelete("/admin/users/{id}", (Guid id) => Results.NoContent())
.RequireAuthorization(policy => policy.RequireRole("Admin"));
Em controllers, o equivalente é o atributo [Authorize] / [Authorize(Roles = "Admin")].
Para regras mais ricas que “tem o papel X”, use políticas:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("GerenteFinanceiro", policy =>
policy.RequireRole("Manager")
.RequireClaim("department", "finance"));
});
// uso: .RequireAuthorization("GerenteFinanceiro") ou [Authorize(Policy = "...")]
Passo 8 — Testar
Faça login, capture o token e chame um endpoint protegido:
# 1) login
curl -X POST http://localhost:5000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@exemplo.com","password":"senha123"}'
# { "accessToken": "eyJ...", "refreshToken": "Rm9v..." }
# 2) acessar rota protegida com o access token
curl http://localhost:5000/me \
-H "Authorization: Bearer eyJ..."
# 3) renovar quando o access token expirar
curl -X POST http://localhost:5000/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"Rm9v..."}'
Quer testar endpoints protegidos sem montar o login? O SDK traz o dotnet user-jwts, que gera JWTs de desenvolvimento reaproveitando a chave do user-secrets:
dotnet user-jwts create --name TesteAdmin --role Admin --valid-for 2h
# use o token retornado no header Authorization
Cole qualquer um desses
accessTokenno JWT Decoder para ver, na prática, os claims que você acabou de emitir.
Armadilhas de segurança (leia antes de ir para produção)
- Nada de segredo no payload. JWT é Base64, não criptografia. Se dá para ler no decoder, um atacante também lê. Só claims não sensíveis.
- Chave de assinatura fora do código.
user-secretsem dev, cofre em produção. Vazou a chave, vazou tudo. - Onde guardar o token no cliente:
localStorageé vulnerável a XSS (qualquer script lê o token). O recomendado é cookieHttpOnly+Secure+SameSite, idealmente com o backend intermediando (padrão BFF). - Sempre HTTPS, sempre validando
issuereaudience, sempre com expiração curta no access token. - Logout de verdade não existe com JWT stateless: não dá para revogar um access token antes de expirar. É justamente por isso que o refresh token vive no servidor — ele é revogável, e é o seu ponto de controle para “deslogar” e para theft detection.
HS256 ou RS256?
Usamos HS256 (chave simétrica) por simplicidade: a mesma chave assina e valida. É adequado quando o mesmo serviço emite e valida os tokens — o caso deste tutorial.
Em produção com múltiplos serviços, prefira RS256/ES256 (assimétrico): a chave privada assina, a pública valida. Os serviços validadores nunca veem o segredo, e você pode publicar a chave pública num endpoint JWKS. E, sempre que possível em produção, não reinvente o login: apoie-se em OpenID Connect / OAuth 2.0, que é o padrão do mercado por bons motivos.
Conclusão
Autenticação JWT bem-feita não é sobre gerar um token — é sobre os detalhes ao redor: expiração curta, validação estrita, chave protegida e um refresh token server-side com rotação e detecção de reúso. Com o esqueleto deste guia você tem um fluxo de login seguro e moderno em .NET 10, usando as APIs atuais e evitando as armadilhas que costumam passar despercebidas.
Precisa de uma API segura, com autenticação e autorização bem arquitetadas? A DevPlus projeta e constrói back-ends .NET em produção. Fale com a gente.
Veja também: JWT Decoder: como decodificar e inspecionar um token, Clean Architecture no .NET do zero e Clean Code em C#: práticas essenciais.