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.
🐛 Se a autenticação falhar com
MethodNotFoundemBase64UrlEncoder.Decode: é conflito de versões do IdentityModel — o NuGet resolveu umMicrosoft.IdentityModel.*antigo ao lado do JwtBearer 10.x. A correção é alinhar a família, fixando explicitamenteMicrosoft.IdentityModel.JsonWebTokens(e, se necessário,Microsoft.IdentityModel.Protocols.OpenIdConnect) na versão 8 atual.
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"
}
}
Gere uma chave forte e aleatória (não invente à mão) com:
openssl rand -base64 32
🔒 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.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
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
NameClaimType = JwtRegisteredClaimNames.Sub, // User.Identity.Name = sub
RoleClaimType = "role" // [Authorize(Roles=...)] lê o claim "role"
};
});
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.
RoleClaimType e NameClaimType — consequência direta do ponto acima, e a pegadinha mais traiçoeira. Ao desligar o mapeamento e emitir o papel no claim role, você precisa dizer ao ASP.NET Core onde ler o papel. Sem RoleClaimType = "role", o [Authorize(Roles = "Admin")] procura o papel na URI padrão do .NET, não encontra e devolve 403 mesmo com um token de Admin válido. Por isso as duas linhas são obrigatórias aqui.
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.Text;
public sealed class TokenService(IConfiguration config)
{
// Criados uma vez: o handler é thread-safe e cacheia o provedor de assinatura
// por instância de chave — então a SigningCredentials também tem de ser reutilizada.
private static readonly JsonWebTokenHandler Handler = new();
private readonly SigningCredentials _credentials = new(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]!)),
SecurityAlgorithms.HmacSha256); // gera "alg": "HS256"
private readonly string? _issuer = config["Jwt:Issuer"];
private readonly string? _audience = config["Jwt:Audience"];
public string CreateAccessToken(Guid userId, string email, string role)
{
var now = DateTime.UtcNow;
var descriptor = new SecurityTokenDescriptor
{
Issuer = _issuer,
Audience = _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 Handler.CreateToken(descriptor);
}
}
TokenServiceé registrado como singleton (AddSingleton), então a chave e as credenciais são montadas uma única vez — não a cada requisição.
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 System.Text;
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; }
}
Em produção, crie um índice na coluna
TokenHash— cada validação de refresh é uma busca por esse hash.
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) return Results.Unauthorized();
// Detecção de reúso ANTES da expiração: um token roubado e já expirado
// ainda dispara a revogação da família (não perde o sinal de theft detection).
if (stored.IsUsed || stored.IsRevoked)
{
await store.RevokeFamilyAsync(stored.FamilyId); // mata a cadeia inteira
return Results.Unauthorized();
}
if (stored.ExpiresUtc < DateTime.UtcNow) return Results.Unauthorized();
// Busca o usuário ANTES de escrever: se foi deletado, nem rotaciona.
var user = await users.GetByIdAsync(stored.UserId);
if (user is null) 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 access = tokens.CreateAccessToken(user.Id, user.Email, user.Role);
return Results.Ok(new { accessToken = access, refreshToken = newRefresh });
});
Concorrência:
MarkUsedAsync+AddAsyncdeveriam rodar na mesma transação (ou com concorrência otimista — ex.: uma colunarowversionnoRefreshToken). Sem isso, dois refreshes simultâneos com o mesmo token podem gerar duas cadeias válidas. Em produção, envolva a rotação numa transação do EF Core.
Body vs. cookie: para o tutorial ficar legível, os endpoints devolvem os tokens no corpo JSON. Em produção, o mais seguro é enviar o refresh token num cookie
HttpOnly+Secure+SameSite— fora do alcance de scripts (veja as armadilhas de segurança adiante).
Logout: revogar o refresh token
Como o access token é stateless, “deslogar” de verdade é cortar a fonte de novos access tokens — ou seja, revogar o refresh token no servidor. O endpoint de logout revoga a família inteira:
app.MapPost("/auth/logout", async (RefreshDto dto, IRefreshTokenStore store) =>
{
var stored = await store.FindByHashAsync(RefreshTokens.Hash(dto.RefreshToken));
if (stored is not null)
await store.RevokeFamilyAsync(stored.FamilyId);
return Results.NoContent();
});
Depois disso, nenhum refresh gera novo token. O access token atual continua válido até expirar (por isso ele é curto) — se precisar de revogação imediata, mantenha uma denylist de jti até o exp.
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 = "...")]
No
Program.cshá uma chamadaAddAuthorization(a do Passo 3). Para registrar políticas, troque-a pela sobrecarga comoptions =>acima — não chameAddAuthorizationduas vezes. O trecho mostra só o miolo da configuração.
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..."}'
Um aviso sobre o
dotnet user-jwts: essa ferramenta do SDK gera JWTs de desenvolvimento, mas com configuração própria — issuerdotnet-user-jwts, audience da URL de launch e uma chave gravada nouser-secrets. Nada disso bate com oJwt:Issuer/Jwt:Audience/Jwt:Keyque fixamos noTokenValidationParameters, então o token dela toma 401 neste tutorial. Ela só funciona out-of-the-box quando oAddJwtBearer()lê a seçãoAuthentication:Schemes:Bearerautomaticamente (semTokenValidationParametersmanual). Aqui, gere os tokens pelo próprio/auth/login.
Cole o
accessTokendo/auth/loginno 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 com JWT stateless revoga o refresh token (o
/auth/logoutacima), não o access token: o access atual continua válido até oexp— daí ele ser curto. Para revogação imediata, use uma denylist dejti. O refresh token, server-side, é o seu ponto de controle para deslogar e para theft detection. - Rate limiting em
/auth/logine/auth/refresh: sem isso, viram alvo de brute force e credential stuffing. Use o rate limiter nativo do ASP.NET Core (builder.Services.AddRateLimiter(...)+.RequireRateLimiting(...)).
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.