JWT em Minimal APIs (ASP.NET Core): access e refresh tokens do zero
Por John Pires · 16 de março de 2026

JWT em Minimal APIs (ASP.NET Core): access e refresh tokens do zero
O que é e por que importa
Definição prática
JWT (JSON Web Token) é um padrão para representar declarações a serem transmitidas entre duas partes de forma compacta e autônoma. Em aplicações web modernas, JWTs são usados para autenticação e autorização: um servidor emite um token assinado e o cliente o envia em cada requisição para provar sua identidade.
Em Minimal APIs do ASP.NET Core, a simplicidade do roteamento combina bem com JWTs porque você frequentemente precisa de uma forma rápida e leve de proteger endpoints sem a sobrecarga de controllers.
Por que usar access + refresh tokens
Um access token deve ser curto (por exemplo, 5-15 minutos). Isso limita o dano se for vazado. Um refresh token permite ao cliente pedir um novo access token sem que o usuário faça login de novo. Essa combinação é padrão em aplicações que priorizam segurança e boa experiência do usuário.
Sem refresh tokens, você teria que prolongar o tempo de expiração do access token, aumentando janela de ataque. Com refresh tokens, você valida e revoga refrescos no servidor, controlando sessões de forma mais segura.
Conceitos fundamentais com analogias
Access token como crachá temporário
Pense no access token como um crachá temporário que dá acesso a áreas restritas por um curto período. Se alguém fotografar seu crachá, a foto expira cedo e deixa de funcionar.
Na prática, o access token contém claims (nome, roles, id) e uma assinatura para garantir que não foi alterado.
Refresh token como ingresso associado
O refresh token é como um ingresso que permite obter outro crachá quando o original expira. Esse ingresso é guardado pelo servidor (ou pode ser detectável) e pode ser invalidado (revogado) se houver suspeita de uso indevido.
Por ser de longa duração, o refresh token precisa ser guardado com cuidado (HttpOnly cookie, armazenamento seguro no cliente) e verificado no servidor antes de emitir novos access tokens.
Passo a passo com código comentado
Requisitos e pacotes
Para seguir os exemplos você precisa do SDK .NET 7+ (ou 6) e dos pacotes:
- Microsoft.AspNetCore.Authentication.JwtBearer
- System.IdentityModel.Tokens.Jwt
- Opcional: Entity Framework Core se quiser persistir refresh tokens em banco
Vamos usar uma loja de refresh tokens em memória para deixar o exemplo simples e didático.
Arquitetura mínima
Arquivos principais que veremos: Program.cs (Minimal API), TokenService.cs (gera tokens), DTOs e uma implementação simples de repositório de refresh tokens.
A seguir vem o código do projeto. Cada bloco tem contexto antes e explicação depois.
Program.cs: configuração e endpoints
Este bloco configura autenticação JWT, registra serviços e cria endpoints: /login, /refresh e /protected.
// Program.cs
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// Chave simétrica para assinar os tokens (em producao, guarde em segredo)
var key = Encoding.ASCII.GetBytes("minha-chave-super-secreta-12345");
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
});
// Serviços locais
builder.Services.AddSingleton<TokenService>(new TokenService(key));
builder.Services.AddSingleton<RefreshTokenStore>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Endpoints: /login, /refresh, /protected
app.MapPost("/login", async (LoginRequest req, TokenService tokenService, RefreshTokenStore store) =>
{
// Usuario simples hardcoded para exemplo
if (req.Username != "alice" || req.Password != "123")
return Results.Unauthorized();
var access = tokenService.GenerateAccessToken(req.Username);
var refresh = tokenService.GenerateRefreshToken();
store.Save(req.Username, refresh, DateTime.UtcNow.AddDays(7));
return Results.Ok(new AuthResponse { AccessToken = access, RefreshToken = refresh });
});
app.MapPost("/refresh", async (RefreshRequest r, TokenService tokenService, RefreshTokenStore store) =>
{
var entry = store.Get(r.RefreshToken);
if (entry == null || entry.Expires < DateTime.UtcNow)
return Results.Unauthorized();
// opcional: invalidar o refresh anterior e emitir novo
store.Remove(r.RefreshToken);
var newAccess = tokenService.GenerateAccessToken(entry.Username);
var newRefresh = tokenService.GenerateRefreshToken();
store.Save(entry.Username, newRefresh, DateTime.UtcNow.AddDays(7));
return Results.Ok(new AuthResponse { AccessToken = newAccess, RefreshToken = newRefresh });
});
app.MapGet("/protected", () => Results.Ok("Protegido")).RequireAuthorization();
app.Run();
// DTOs abaixo para manter tudo em um arquivo curto
record LoginRequest(string Username, string Password);
record RefreshRequest(string RefreshToken);
record AuthResponse(string AccessToken, string RefreshToken);
O que esse bloco faz: configura autenticação JWT com uma chave simétrica, registra TokenService e RefreshTokenStore e cria três endpoints. O /login valida usuário hardcoded e emite tokens; /refresh troca refresh token por novos tokens; /protected exige autorização.
TokenService: gera access e refresh
Este serviço encapsula a geração de tokens. Access token com claims e curto tempo de vida; refresh token aleatório seguro.
// TokenService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
public class TokenService
{
private readonly byte[] _key;
public TokenService(byte[] key) => _key = key;
public string GenerateAccessToken(string username)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(_key);
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);
var claims = new[] { new Claim(ClaimTypes.Name, username) };
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.UtcNow.AddMinutes(10),
signingCredentials: creds);
return tokenHandler.WriteToken(token);
}
public string GenerateRefreshToken()
{
var random = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(random);
return Convert.ToBase64String(random);
}
}
O que acontece após executar esse código: você obtém um JWT assinável para autorizar rotas e um refresh token aleatório (base64) para renovar sessões. A assinatura garante integridade do token.
RefreshTokenStore: armazenamento simples em memória
Para controlar e revogar refresh tokens, precisamos armazená-los. No exemplo, usamos uma implementação thread-safe em memória. Em produção, use banco de dados.
// RefreshTokenStore.cs
using System.Collections.Concurrent;
public class RefreshTokenStore
{
private readonly ConcurrentDictionary<string, (string Username, DateTime Expires)> _store = new();
public void Save(string username, string refreshToken, DateTime expires)
{
_store[refreshToken] = (username, expires);
}
public (string Username, DateTime Expires)? Get(string refreshToken)
{
if (_store.TryGetValue(refreshToken, out var value))
return value;
return null;
}
public void Remove(string refreshToken)
{
_store.TryRemove(refreshToken, out _);
}
}
Depois de salvar um refresh token, você pode validar e revogar. A remoção é recomendada quando o token é usado (rotina de rotating refresh tokens).
Exemplo prático completo
Fluxo completo: login, usar, refresh
Passos na prática:
- Cliente POST /login com username/password.
- Servidor retorna access token (JWT) e refresh token.
- Cliente usa Authorization: Bearer {access} para chamar /protected.
- Se access expirar, cliente POST /refresh com refresh token.
- Servidor valida refresh, emite novo access + refresh e revoga o antigo.
Esse fluxo permite que a sessão seja renovada sem pedir credenciais novamente.
Exemplo de requisições cURL
Contexto: aqui mostramos como testar com cURL para validar o fluxo end-to-end.
# Login e obter tokens
curl -X POST http://localhost:5000/login -H "Content-Type: application/json" -d '{"username":"alice","password":"123"}'
# Usar access token (supondo que RESPONSE contém accessToken)
curl http://localhost:5000/protected -H "Authorization: Bearer {accessToken}"
# Refresh quando access expirar
curl -X POST http://localhost:5000/refresh -H "Content-Type: application/json" -d '{"refreshToken":"{refreshToken}"}'
Após login, o access permite chamadas autenticadas. Quando expira, envie o refresh. Se o refresh for inválido ou revogado, o servidor retorna 401 e o usuário precisa logar outra vez.
Demonstração com resposta JSON
Exemplo de resposta ao /login (JSON):
{
"accessToken": "eyJhbGciOiJIUzI1...",
"refreshToken": "QkFTRTY0QmFzZTY0VG9rZW4="
}
Esse JSON é o contrato entre cliente e servidor. O cliente deve armazenar o refresh token de forma segura (ex.: HttpOnly cookie em web apps).
Erros comuns, armadilhas e dicas práticas
Armadilha 1: armazenar refresh token em localStorage
Guardar refresh tokens em localStorage deixa seus tokens vulneráveis a XSS. Prefira cookies HttpOnly (quando possível) ou armazenamento seguro no app nativo.
Dica: reduza superfície de ataque com CSP, validação de input e minimizando bibliotecas que possam introduzir XSS.
Armadilha 2: tempo de vida (expirations) mal calculado
Um access token muito longo diminui segurança; muito curto pode irritar usuários com requisições de refresh frequentes. Ajuste para o seu caso: 10-15 minutos costuma ser boa prática para access; refresh de dias a meses dependendo do nível de confiança.
Em APIs públicas, prefira access curto + rotating refresh tokens para balancear segurança e usabilidade.
Armadilha 3: não invalidar refresh tokens em logout ou quando comprometido
Se você não revogar refresh tokens, um token comprometido pode conceder acesso indefinidamente (até expirar). Sempre remova/invalidar o refresh no logout e mantenha uma lista de tokens revogados se necessário.
Armadilhas técnicas rápidas
- Esquecer de configurar ClockSkew pode permitir tokens ligeiramente expirados.
- Usar a mesma chave simétrica para múltiplos ambientes sem rotação é risco operacional.
- Deserializar JWT sem validar assinatura abre para tampering.
Próximos passos para aprofundar
Migrar refresh store para banco de dados
Substitua RefreshTokenStore por uma tabela no banco (EF Core). Armazene token hash (não o token em claro) para maior segurança. Ao receber um refresh token, compare hash em vez do token em claro.
Hashing: use HMAC-SHA256 com secret ou algoritmo adaptativo (ex.: Argon2) dependendo do modelo de ameaça.
Rotating refresh tokens e deteção de abuso
Rotating refresh tokens: ao trocar um refresh token por outro, invalide o antigo. Se você detectar o mesmo refresh usado duas vezes, há provável theft — aí revogue todos tokens dessa conta e force re-login.
Adicione logs de auditoria, IP e user-agent ao salvar refresh tokens para ajudar a detectar sessões suspeitas.
Conclusão prática
Resumo conciso
Implementar JWT em Minimal APIs é direto: configure JWT bearer, gere access tokens curtos e refresh tokens longos, armazene e valide refresh tokens no servidor, e trate logout e rotas protegidas. A prática segura exige revogação, armazenamento seguro e rotação de tokens.
Checklist prático
- Usar HS256/HMAC ou RSA para assinar tokens (HS para simplicidade, RSA para maior segurança e rotação).
- Access token curto (10–15 min).
- Refresh token guardado com segurança e revogado ao usar/logout.
- Evitar storage inseguro no cliente; preferir cookies HttpOnly quando possível.
Com os exemplos acima você tem uma base funcional: Program.cs com endpoints, TokenService para gerar tokens e RefreshTokenStore para controlar refreshes. O próximo passo é migrar a store para banco, adicionar hashing dos refresh tokens e implementar monitoramento de abuso.
Se algo ainda não estiver claro, pegue o código completo acima e execute localmente. Teste o fluxo com cURL e observe o comportamento ao expirar o access token. Ajuste tempos e políticas conforme seu caso de uso.
Assuntos: C#, aspnet-core, jwt, minimal-apis, Segurança
Leia também
Minimal APIs + SSE: enviar progresso de jobs para clientes Blazor passo a passo
Guia passo a passo para usar Minimal APIs + SSE e enviar progresso de jobs para clientes Blazor. Do setup ao código funcional: servidor com endpoint SSE…
Multitenancy em ASP.NET Core com EF Core: discriminator, schema e database
Comparação prática entre discriminator, schema e database para multitenancy em ASP.NET Core com EF Core. Implementação de TenantProvider, DbContext dinâmico…
Implementando paginação cursor-based com EF Core e Minimal APIs (passo a passo)
Guia prático para implementar paginação cursor-based com EF Core e Minimal APIs: modelagem do cursor, consultas keyset em LINQ, geração segura de cursor…
Autenticação JWT em Minimal APIs: refresh tokens seguros com HttpOnly
Guia prático para implementar refresh tokens seguros com cookies HttpOnly em Minimal APIs ASP.NET Core: conceitos, passos comentados, exemplos em C# e…
Upload de arquivos com ASP.NET Core Minimal APIs: streaming e progresso
Guia prático para implementar upload de arquivos com ASP.NET Core Minimal APIs: streaming para evitar OOM, reporting de progresso via polling e dicas para…
Soft delete no EF Core: filtros globais, auditoria e restauração
Soft delete com EF Core evita perda de dados, permite auditoria e restauração. Aprenda o jeito errado (filtros manuais) e o jeito certo (global query filters…