MÓDULO 11 · CONCEITO 05 DE 12

JWT em Profundidade — claims, assinatura e armadilhas

Anatomia do JWT, HS256 vs RS256 vs ES256, validação correta de todas as claims, a vulnerabilidade alg:none, revogação e blacklisting, rotação de chaves com JWK endpoint

Tempo de leitura ~22 min Pré-requisito 04 · OAuth 2.1 e OpenID Connect Próximo 06 · RBAC, ABAC e ReBAC

JSON Web Token (JWT, RFC 7519) é o formato de token mais ubíquo em sistemas modernos — usado para access tokens OAuth, ID tokens OIDC, tokens de sessão stateless, e em sistemas de troca de assertions entre serviços. Sua popularidade é merecida: um JWT é auto-descritivo (contém as claims necessárias), verificável sem round-trip ao servidor (desde que a chave pública esteja disponível), e padronizado o suficiente para que bibliotecas de todas as linguagens interoperem. Mas essa popularidade trouxe um problema: desenvolvedores usam JWT sem entender o mecanismo, resultando em implementações que parecem corretas e são fundamentalmente inseguras.

A vulnerabilidade alg:none — descoberta em 2015 por Tim McLean e documentada na CVE-2015-9235 — afetou dezenas de bibliotecas JWT populares e expôs sistemas em produção que pareciam usar JWTs "corretamente". O problema não era um bug obscuro na criptografia: era a ausência de uma validação que a especificação tornava fácil de omitir. Entender JWT em profundidade significa entender não apenas como assinar e verificar, mas quais as armadilhas que a especificação permite por design e como evitá-las explicitamente.

Anatomia do JWT — header.payload.signature

Um JWT é uma string com três seções separadas por pontos, cada uma codificada em Base64URL (sem padding, URL-safe):

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0yMDI0LTAxIn0
.
eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vYXV0aC5hcHAuY29tIiwiYXVkIjoiYXBpLmFwcC5jb20iLCJleHAiOjE3MTYzMjE2MDAsImlhdCI6MTcxNjMxODAwMCwicm9sZXMiOlsiZWRpdG9yIl19
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decodificados:

// Header
{
  "alg": "RS256",           // algoritmo de assinatura
  "typ": "JWT",             // tipo do token
  "kid": "key-2024-01"     // key ID — identifica qual chave usar para verificar
}

// Payload
{
  "sub": "user_123",                        // subject — identificador do usuário
  "iss": "https://auth.app.com",           // issuer — quem emitiu o token
  "aud": "api.app.com",                    // audience — para quem o token é válido
  "exp": 1716321600,                        // expiration — Unix timestamp
  "iat": 1716318000,                        // issued at — quando foi emitido
  "nbf": 1716318000,                        // not before — válido a partir de
  "jti": "uuid-único-por-token",           // JWT ID — para prevenção de replay
  "roles": ["editor"],                      // claim customizada — papéis do usuário
  "email": "user@app.com"                  // claim customizada
}

// Signature = RS256(base64url(header) + "." + base64url(payload), chave_privada)

A assinatura é calculada sobre base64url(header) + "." + base64url(payload) com o algoritmo declarado no header. Se qualquer byte do header ou payload mudar, a assinatura invalida. Isso torna o JWT tamper-evident: qualquer modificação no payload (como trocar "roles": ["editor"] por "roles": ["admin"]) é detectada na verificação da assinatura.

armadilha clássica

JWT garante integridade — que o payload não foi modificado desde a assinatura — mas não garante confidencialidade. O payload é apenas Base64URL-encoded, não criptografado. Qualquer um que tenha o token pode ler as claims. Não coloque dados sensíveis (senhas, números de cartão, PII além do necessário) no payload de um JWT. Para tokens que precisam ser opacos, use JWE (JSON Web Encryption, RFC 7516).

HS256 vs RS256 vs ES256 — quando usar cada algoritmo

A escolha do algoritmo de assinatura não é cosmética — define o modelo de confiança do sistema.

HS256 (HMAC-SHA256) usa uma chave simétrica: a mesma chave que assina também verifica. Isso significa que qualquer serviço que precisa verificar JWTs precisa ter acesso à chave secreta. Se você tem 5 microserviços verificando JWTs e a chave vaza de um deles, todos os tokens do sistema estão comprometidos. HS256 é adequado quando um único serviço assina e verifica (o mesmo processo, ou dois processos que compartilham o segredo de forma segura). É inadequado para sistemas onde múltiplos serviços verificam tokens sem necessidade de assinar.

RS256 (RSA com SHA-256) e ES256 (ECDSA com curva P-256 e SHA-256) usam criptografia assimétrica: chave privada para assinar, chave pública para verificar. A chave pública pode ser distribuída amplamente sem risco — ela permite verificar tokens, não criar. O authorization server guarda a chave privada com acesso restrito; qualquer serviço que precisa verificar tokens busca a chave pública via JWKS endpoint. Essa é a arquitetura correta para sistemas com múltiplos verificadores.

ES256 é preferível a RS256 em sistemas novos: chaves menores (256 bits vs 2048+ bits), assinaturas menores, e performance de assinatura/verificação significativamente melhor. RS256 continua prevalente por compatibilidade com sistemas legados e bibliotecas antigas. Ambos oferecem segurança equivalente quando usados com tamanhos de chave adequados.

C# — emissão e verificação com RSA assimétrico
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;

// Emissão (authorization server — tem a chave privada)
var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText("private.pem"));
var signingKey = new RsaSecurityKey(rsa) { KeyId = "key-2024-01" };

var token = new JwtSecurityTokenHandler().CreateEncodedJwt(new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity([
        new Claim("sub", "user_123"),
        new Claim("roles", "editor"),
    ]),
    Issuer = "https://auth.app.com",
    Audience = "api.app.com",
    Expires = DateTime.UtcNow.AddMinutes(15),
    SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256)
});

// Verificação (resource server — tem apenas a chave pública)
var rsaPub = RSA.Create();
rsaPub.ImportFromPem(File.ReadAllText("public.pem"));
var verifyKey = new RsaSecurityKey(rsaPub);

var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(token, new TokenValidationParameters
{
    ValidateIssuer = true,             // nunca desabilitar
    ValidIssuer = "https://auth.app.com",
    ValidateAudience = true,           // nunca desabilitar
    ValidAudience = "api.app.com",
    ValidateLifetime = true,           // nunca desabilitar
    ValidAlgorithms = ["RS256"],       // allowlist explícita — nunca aceitar qualquer alg
    IssuerSigningKey = verifyKey,
    ClockSkew = TimeSpan.FromSeconds(30),
}, out var validatedToken);

O parâmetro ValidAlgorithms é a defesa contra alg:none e algorithm confusion attacks. Sempre especificar explicitamente; nunca aceitar o algoritmo declarado no token sem validação.

Python — PyJWT com RS256 e validação completa
import jwt
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key

# Emissão (authorization server)
with open('private.pem', 'rb') as f:
    private_key = load_pem_private_key(f.read(), password=None)

token = jwt.encode(
    payload={
        'sub': 'user_123',
        'iss': 'https://auth.app.com',
        'aud': 'api.app.com',
        'exp': int(time.time()) + 900,   # 15 min
        'iat': int(time.time()),
        'roles': ['editor'],
    },
    key=private_key,
    algorithm='RS256',
    headers={'kid': 'key-2024-01'}
)

# Verificação (resource server)
with open('public.pem', 'rb') as f:
    public_key = load_pem_public_key(f.read())

claims = jwt.decode(
    token,
    key=public_key,
    algorithms=['RS256'],                # allowlist — nunca ['RS256', 'none']
    options={
        'require': ['exp', 'iss', 'aud', 'sub'],
        'verify_exp': True,
        'verify_iss': True,
        'verify_aud': True,
    },
    issuer='https://auth.app.com',
    audience='api.app.com',
    leeway=30                            # tolerância de clock skew em segundos
)

PyJWT: nunca passar algorithms=None ou deixar o parâmetro de fora — o padrão sem algoritmo especificado aceita HS256 com a chave pública como segredo, abrindo para algorithm confusion.

Go — golang-jwt/jwt v5 com RS256
import (
    "github.com/golang-jwt/jwt/v5"
    "crypto/rsa"
    "os"
)

type Claims struct {
    Roles []string `json:"roles"`
    jwt.RegisteredClaims
}

// Emissão
func IssueToken(userID string, privateKey *rsa.PrivateKey) (string, error) {
    claims := Claims{
        Roles: []string{"editor"},
        RegisteredClaims: jwt.RegisteredClaims{
            Subject:   userID,
            Issuer:    "https://auth.app.com",
            Audience:  jwt.ClaimStrings{"api.app.com"},
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(privateKey)
}

// Verificação
func VerifyToken(tokenStr string, publicKey *rsa.PublicKey) (*Claims, error) {
    parser := jwt.NewParser(
        jwt.WithValidMethods([]string{"RS256"}),  // allowlist explícita
        jwt.WithIssuer("https://auth.app.com"),
        jwt.WithAudience("api.app.com"),
        jwt.WithLeeway(30 * time.Second),
    )
    token, err := parser.ParseWithClaims(tokenStr, &Claims{},
        func(t *jwt.Token) (interface{}, error) {
            return publicKey, nil
        })
    if err != nil || !token.Valid { return nil, err }
    return token.Claims.(*Claims), nil
}

golang-jwt/jwt v5 introduziu WithValidMethods após o problema de algorithm confusion. Em versões anteriores, era necessário verificar manualmente o método no callback do keyfunc.

A vulnerabilidade alg:none — mecanismo e impacto

Em 2015, Tim McLean publicou uma análise de bibliotecas JWT onde encontrou um padrão alarmante: muitas implementações confiavam no campo alg do header do token para determinar como verificar a assinatura. Isso permitia o seguinte ataque:

  1. Atacante decodifica um JWT legítimo (Base64URL é reversível)
  2. Modifica o payload — troca "roles": ["editor"] por "roles": ["admin"]
  3. Modifica o header — troca "alg": "RS256" por "alg": "none"
  4. Remove a assinatura — mantém apenas header.payload. (com ponto final)
  5. Envia o token modificado ao servidor

Bibliotecas vulneráveis ao ver alg: "none" simplesmente saltavam a verificação da assinatura — afinal, o próprio token declarava que não havia assinatura. O payload modificado era aceito como autêntico. A fix é trivial conceitualmente: nunca confiar no alg declarado no token; sempre verificar com o algoritmo esperado pelo servidor, configurado explicitamente.

Uma variação mais sutil é o algorithm confusion attack (também chamado RS256/HS256 confusion): quando um servidor usa RS256 (assimétrico) mas a biblioteca aceita qualquer algoritmo, um atacante pode forjar um token HS256 usando a chave pública do servidor como segredo simétrico. A chave pública é por definição pública — o atacante tem acesso a ela via JWKS endpoint. A defesa é a mesma: whitelist de algoritmos aceitos configurada pelo servidor, nunca pelo token.

Validação correta — o que verificar e em qual ordem

Uma validação correta de JWT deve verificar, em ordem:

  1. Estrutura: três segmentos separados por pontos, cada um Base64URL válido.
  2. Algoritmo: o alg no header está na allowlist configurada pelo servidor (nunca aceitar none).
  3. Assinatura: verificar com a chave correspondente ao kid no header, obtida do JWKS endpoint ou configurada localmente.
  4. Issuer (iss): deve ser exatamente o authorization server esperado — comparação de string exata.
  5. Audience (aud): deve conter o identificador do resource server atual. Um token emitido para api-a.app.com não deve ser aceito por api-b.app.com.
  6. Expiração (exp): exp deve ser no futuro. Aceitar uma tolerância pequena (30-60 segundos) para clock skew.
  7. Not Before (nbf): se presente, o token não deve ser aceito antes desse timestamp.
  8. Claims de negócio: verificar claims customizadas como roles, scope, e qualquer outra necessária para a autorização.

Pular qualquer uma dessas verificações cria uma superfície de ataque. Omitir a verificação de aud é particularmente perigosa: um token emitido para um serviço menos crítico pode ser usado contra um serviço mais crítico — o token é válido, assinado corretamente, apenas não era destinado a esse serviço.

armadilha em produção

Desabilitar verificação de expiração em testes e esquecer de reabilitar em produção. É mais comum do que parece — a mensagem de erro "token expired" atrapalha testes de longa duração, e o desenvolvedor adiciona verify_exp: false "temporariamente". O temporário que vai para produção cria tokens que nunca expiram. Use tokens de vida longa nos testes em vez de desabilitar a verificação.

JWK endpoint e rotação de chaves

JSON Web Key Set (JWKS, RFC 7517) é o formato padrão para publicar chaves públicas. O JWKS endpoint — geralmente em /.well-known/jwks.json — retorna um objeto com um array de chaves, cada uma identificada por um kid (key ID):

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "key-2024-01",
      "n": "...",           // modulus — a chave pública RSA
      "e": "AQAB"          // exponent
    },
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "key-2024-02", // nova chave em rollout
      "n": "...",
      "e": "AQAB"
    }
  ]
}

A rotação de chaves sem downtime funciona assim: um novo par de chaves é gerado; a chave pública nova é adicionada ao JWKS endpoint (agora há duas chaves); o authorization server começa a emitir tokens com a nova chave privada (novo kid); os resource servers verificam tokens com o kid correspondente — tokens antigos (ainda não expirados) continuam verificáveis com a chave antiga; após o TTL máximo de um token (15 minutos para access tokens, por exemplo), nenhum token assinado com a chave antiga ainda existe; a chave antiga é removida do JWKS endpoint.

Resource servers devem implementar cache do JWKS com lógica de invalidação: cache por 1-6 horas normalmente, mas se um token chegar com um kid que não está no cache, buscar o JWKS novamente antes de rejeitar o token — isso permite que a rotação aconteça sem que tokens recém-emitidos sejam rejeitados.

Revogação — o problema sem solução perfeita

JWTs stateless não podem ser revogados sem algum mecanismo central — é a tensão fundamental do formato. As estratégias disponíveis, em ordem de complexidade:

A escolha depende do requisito de revogação imediata. Para a maioria dos sistemas, TTL de 15 minutos + refresh token revogável no logout é suficiente. Para sistemas financeiros ou com requisito regulatório de logout imediato efetivo, blocklist de JTI ou token introspection são necessários.

Decisões de engenharia

HS256 vs RS256/ES256

Use HS256 quando há um único serviço que assina e verifica tokens (monolito ou dois processos com segredo compartilhado seguro). Simples, sem infra de PKI.

Use RS256/ES256 quando múltiplos serviços precisam verificar tokens independentemente. Chave privada permanece isolada no authorization server; qualquer serviço verifica via JWKS público. ES256 preferível em sistemas novos — chaves e assinaturas menores, melhor performance.

JWT vs token opaco

Use JWT quando resource servers precisam ler claims sem round-trip ao authorization server. Escalabilidade horizontal sem estado central de sessão.

Use token opaco quando revogação imediata é requisito hard (logout efetivo, banimento instantâneo). O resource server sempre chama token introspection — troca latência por controle total. Ou use JWT com TTL de 5 min para aproximar o comportamento.

Estratégia de revogação

TTL curto (15 min) resolve 95% dos casos. Tokens comprometidos expiram sozinhos; logout invalida o refresh token impedindo novos access tokens.

Blocklist JTI no Redis quando logout deve ser imediatamente efetivo mas não há requisito de introspection obrigatório. SET <jti> com TTL = exp do token — Redis expire limpa automaticamente.

Token introspection para requisito regulatório de revogação síncrona (PSD2, saúde, fintech). Aceitar o round-trip ao authorization server em cada request.

Rotação de chaves JWKS

Janela de sobreposição: nunca remover uma chave do JWKS antes de todos os tokens por ela assinados expirarem. Sobreposição mínima = TTL do access token.

Cache inteligente nos verificadores: cache JWKS por 1h, mas se um token chegar com kid desconhecido, re-fetch imediato antes de rejeitar. Isso absorve rotações sem downtime.

Frequência de rotação: chaves RS256 a cada 90 dias é razoável. Incidentes (vazamento suspeito) exigem rotação de emergência — o protocolo de sobreposição funciona igualmente.

Como praticar

  1. Reproduzir a vulnerabilidade alg:none em ambiente controlado. Em Python, usando PyJWT com configuração incorreta (algorithms=None ou sem o parâmetro), decodifique um JWT RS256 válido, modifique uma claim como roles ou sub, substitua o header para "alg": "none", remova a assinatura (mantendo o ponto final), e verifique que a biblioteca aceita o token. Implemente então a proteção com allowlist explícita e confirme que o ataque é rejeitado com erro claro.
    Critério: o token forjado é aceito pela versão vulnerável e rejeitado pela versão corrigida; o log de erro indica exatamente qual validação falhou.
  2. Implementar JWK endpoint e rotação de chaves sem downtime. Crie um authorization server mínimo que gera par de chaves RSA, expõe a chave pública em /.well-known/jwks.json com o kid, e emite JWTs com kid no header. Implemente um resource server que verifica tokens buscando a chave pelo kid com cache de 1h + re-fetch em kid desconhecido. Então rotacione a chave (gere novo par, adicione ao JWKS, comece a assinar com a nova) e verifique que tokens antigos ainda são válidos enquanto novos usam a nova chave.
    Critério: tokens emitidos com a chave antiga continuam válidos durante a janela de sobreposição; tokens novos são verificados com a nova chave sem que o resource server precise ser reiniciado.
  3. Implementar blocklist JTI com Redis para logout imediato. Adicione um jti (UUID v4) a cada JWT emitido. No endpoint de logout, grave o JTI no Redis com TTL igual ao exp do token — SET jti:<jti> 1 EXAT <exp>. No middleware de verificação, após validar a assinatura e claims padrão, cheque se o JTI está na blocklist. Implemente e verifique que um token revogado é rejeitado imediatamente, mesmo antes do seu exp.
    Critério: após logout, o mesmo access token é rejeitado em 100% das requisições subsequentes; tokens de outros usuários não são afetados; o Redis não acumula JTIs além do TTL máximo dos tokens.
  4. Auditar validação de JWT em uma codebase existente. Em um projeto que usa JWT (seu ou open source), examine cada ponto de verificação e responda: (1) o algoritmo está na allowlist explícita? (2) iss é validado por string exata? (3) aud é validado? (4) exp é validado e clock skew é razoável? (5) claims de negócio necessárias para autorização estão sendo verificadas? (6) a chave de verificação é obtida de forma segura?
    Critério: documento mapeando cada verificação presente e ausente, com o risco correspondente e a fix necessária para cada gap encontrado.
  5. Explorar algorithm confusion attack (RS256→HS256). Configure um resource server que usa RS256 mas aceita múltiplos algoritmos (ou qualquer algoritmo do header). Obtenha a chave pública do JWKS endpoint. Usando PyJWT ou jose, gere um token HS256 usando a chave pública como segredo simétrico — modifique uma claim privilegiada. Verifique que o servidor vulnerável aceita. Implemente a fix (allowlist RS256 apenas) e confirme rejeição. Documente por que a chave pública como segredo HMAC é viável do ponto de vista do atacante.
    Critério: token forjado com HS256 e chave pública como segredo é aceito pelo servidor vulnerável; após a fix de allowlist, o mesmo token é rejeitado com invalid algorithm.

Perguntas de entrevista

Explique a vulnerabilidade alg:none — por que ela existe na especificação e como um sistema correto se defende?

A vulnerabilidade existe porque a RFC 7519 define "none" como um algoritmo válido para JWTs não assinados — casos de uso legítimos em sistemas internos sem requisito de integridade. Bibliotecas vulneráveis liam o campo alg do header do próprio token para decidir como verificar a assinatura. Um atacante podia então: decodificar qualquer JWT legítimo (Base64URL é reversível), modificar claims no payload, alterar o header para "alg": "none", remover a assinatura, e enviar o token forjado — que a biblioteca aceitaria por não ter assinatura para verificar.

A defesa é configurar no servidor uma allowlist explícita de algoritmos aceitos, independente do que o token declara: ValidAlgorithms = ["RS256"] em .NET, algorithms=['RS256'] em PyJWT, WithValidMethods([]string{"RS256"}) em golang-jwt. O token pode declarar qualquer alg — o servidor verifica apenas com os algoritmos da allowlist. Uma variante (RS256→HS256 confusion) usa a chave pública como segredo HMAC; a mesma defesa resolve.

Qual a diferença entre HS256 e RS256 no modelo de confiança em microserviços? Quando cada um é inadequado?

HS256 é simétrico: a mesma chave assina e verifica. Em microserviços, todos os serviços que precisam verificar JWTs precisam ter a chave secreta — cada serviço com acesso à chave pode também assinar tokens, não apenas verificar. Se qualquer serviço for comprometido, o atacante pode emitir tokens válidos para qualquer usuário. HS256 é inadequado quando o modelo de segurança requer separação entre emissão e verificação.

RS256 (e ES256) é assimétrico: chave privada apenas no authorization server, chave pública disponível via JWKS para qualquer serviço verificar. Um serviço comprometido pode verificar tokens existentes mas não pode emitir novos — a chave privada nunca saiu do authorization server. RS256 é inadequado quando a infra de PKI é impraticável ou quando o overhead de operações assimétricas é inaceitável para o volume de tokens (raro — ES256 é muito eficiente).

Por que validar a claim aud é crítico? Descreva um ataque concreto que a ausência desta validação permite.

A claim aud (audience) especifica para qual serviço ou sistema o token foi emitido. Sem validação, qualquer token válido assinado pelo mesmo issuer pode ser usado em qualquer serviço — mesmo que destinado a outro.

Ataque concreto: uma empresa tem dois serviços, api-public.app.com (acesso a dados públicos, autenticação relaxada) e api-admin.app.com (painel administrativo). Um usuário comum obtém um token legítimo para api-public com claims "roles": ["viewer"]. Se api-admin não valida aud, esse token é aceito — o usuário acessa o painel admin com permissões de viewer (ou pior, se o admin não verificar as roles individuais e apenas checar se o token é válido). A assinatura é válida, o issuer é o mesmo, apenas o destino está errado. Sem validação de aud, o vazamento de escopo entre serviços é trivial.

Como implementar revogação de JWT mantendo verificação stateless nos resource servers? Quais as trocas entre as abordagens?

Há três abordagens principais com trocas distintas:

TTL curto (15 min) + refresh token revogável: stateless puro nos resource servers. No logout, invalida o refresh token no authorization server — nenhum novo access token pode ser emitido. Tokens em voo expiram sozinhos em até 15 min. Troca: logout não é imediatamente efetivo para tokens existentes.

Blocklist JTI no Redis: cada JWT tem jti único. No logout, grava o JTI no Redis com TTL = exp do token. Resource server verifica Redis em cada request após validar assinatura. Troca: um round-trip ao Redis por request elimina parte do benefício stateless, mas mantém escala horizontal — Redis é compartilhado, não o authorization server. Tamanho gerenciável: apenas JTIs de tokens vivos ficam na lista.

Token introspection (RFC 7662): resource server chama authorization server em cada request para perguntar se o token é válido. Completamente stateful, revogação imediata garantida. Troca: latência adicionada em cada request, authorization server vira gargalo, elimina o benefício de escala horizontal do JWT.

A escolha depende do SLA de revogação: "15 minutos é OK" → TTL curto; "logout deve ser imediatamente efetivo mas não precisamos de sync síncrono" → JTI blocklist; "requisito regulatório de revogação instantânea verificável" → token introspection.

Como funciona a rotação de chaves JWKS sem downtime? O que pode dar errado e como mitigar?

O protocolo de rotação sem downtime tem 5 fases: (1) gerar novo par RSA/EC; (2) adicionar a nova chave pública ao JWKS endpoint — agora há duas chaves; (3) começar a assinar novos tokens com a nova chave privada (novo kid no header); (4) aguardar a janela de sobreposição = TTL máximo de um access token (ex: 15 min) — durante esse tempo, resource servers podem receber tokens assinados com a chave antiga (kid antigo) e tokens com a chave nova; (5) remover a chave antiga do JWKS apenas após nenhum token por ela assinado poder ainda estar vivo.

O que pode dar errado: resource servers com cache JWKS rígido (sem re-fetch em kid desconhecido) rejeitarão tokens assinados com a nova chave até o cache expirar. Fix: implementar o padrão "re-fetch on unknown kid" — se o kid do token não está no cache, buscar o JWKS novamente antes de rejeitar. Isso absorve rotações transparentemente. Outro risco: remover a chave antiga antes do TTL máximo — tokens legítimos em voo são rejeitados. O campo exp de cada token indica até quando a chave antiga precisa ficar no JWKS.

Referências para aprofundar

  1. docs RFC 7519 — JSON Web Token (JWT) — IETF. tools.ietf.org/html/rfc7519. A especificação completa — claims registradas, regras de validação, e o formato exato de serialização. Leitura de 1 hora.
  2. docs RFC 7517 — JSON Web Key (JWK) — IETF. tools.ietf.org/html/rfc7517. O formato JWKS para publicar chaves públicas. Inclui parâmetros para RSA, EC, e chaves simétricas.
  3. article Critical Vulnerabilities in JSON Web Token Libraries — Tim McLean (2015). O artigo original que expôs alg:none e algorithm confusion em bibliotecas populares. Disponível em auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries. Leitura fundamental para entender o que foi errado na especificação original.
  4. article JWT Security Best Practices — Curity (curity.io, 2023). Guia prático de validação correta de JWTs com exemplos de código em múltiplas linguagens. Cobre alg:none, algorithm confusion, validação de claims, e rotação de chaves.
  5. docs RFC 7662 — OAuth 2.0 Token Introspection — IETF. tools.ietf.org/html/rfc7662. O protocolo para verificação stateful de tokens — alternativa à verificação local para sistemas que precisam de revogação imediata.
  6. article JWT Algorithm Confusion Attacks — PortSwigger Web Security Academy. portswigger.net/web-security/jwt. Laboratórios práticos de exploração de vulnerabilidades JWT — alg:none, algorithm confusion, e outras variantes. Gratuito, baseado em browser, sem instalação.
  7. article JWT: Don't Use It for Sessions — Sven Slootweg (sealedabstract.com, clássico). Artigo frequentemente referenciado que argumenta contra uso de JWTs para sessões de browser — os problemas de revogação, tamanho de token em cookies, e complexidade de implementação vs sessions server-side. Contrabalança o entusiasmo excessivo por JWT.
  8. book API Security in Action — Neil Madden (Manning, 2020). Capítulo 6 cobre JWTs em profundidade — criação, validação, e as armadilhas de implementação. Inclui um capítulo inteiro sobre o que pode dar errado com JWTs em APIs REST.
  9. article JWKS Key Rotation Without Downtime — Auth0 Blog. Guia prático de rotação de chaves JWKS com cache inteligente nos resource servers. Explica a janela de sobreposição de chaves e como implementar o cache com invalidação seletiva.
  10. paper SoK: Authentication in the Wild — IEEE S&P 2022. Survey sistemático de falhas de autenticação em sistemas reais, com seção sobre JWT. Documenta quantitativamente os padrões de erro mais comuns encontrados em auditorias de código.
  11. docs jwt.io — JWT Debugger e Bibliotecas. Ferramenta online para decodificar e verificar JWTs. Lista bibliotecas recomendadas por linguagem com notas sobre quais verificações cada uma faz por padrão. Útil para introspecção rápida de tokens em desenvolvimento.
  12. article Stop Using JWTs as Session Tokens — Scott Arciszewski (paragonie.com, 2018). Análise técnica dos casos de uso legítimos vs ilegítimos de JWT — quando a natureza stateless agrega valor real e quando adiciona complexidade sem benefício. Perspectiva de especialista em criptografia aplicada.