Destino Dev

Blog

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:

  1. Cliente POST /login com username/password.
  2. Servidor retorna access token (JWT) e refresh token.
  3. Cliente usa Authorization: Bearer {access} para chamar /protected.
  4. Se access expirar, cliente POST /refresh com refresh token.
  5. 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&eacute;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