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.
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.
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.
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.
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:
- Atacante decodifica um JWT legítimo (Base64URL é reversível)
- Modifica o payload — troca
"roles": ["editor"]por"roles": ["admin"] - Modifica o header — troca
"alg": "RS256"por"alg": "none" - Remove a assinatura — mantém apenas
header.payload.(com ponto final) - 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:
- Estrutura: três segmentos separados por pontos, cada um Base64URL válido.
- Algoritmo: o
algno header está na allowlist configurada pelo servidor (nunca aceitarnone). - Assinatura: verificar com a chave correspondente ao
kidno header, obtida do JWKS endpoint ou configurada localmente. - Issuer (
iss): deve ser exatamente o authorization server esperado — comparação de string exata. - Audience (
aud): deve conter o identificador do resource server atual. Um token emitido paraapi-a.app.comnão deve ser aceito porapi-b.app.com. - Expiração (
exp):expdeve ser no futuro. Aceitar uma tolerância pequena (30-60 segundos) para clock skew. - Not Before (
nbf): se presente, o token não deve ser aceito antes desse timestamp. - 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.
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:
- TTL curto: access tokens de 15 minutos limitam a janela de abuso de tokens comprometidos sem revogação. É a estratégia padrão e a mais simples.
- Token binding ao refresh token: ao fazer refresh, o access token novo é vinculado ao refresh token que o gerou. Invalidar o refresh token (em logout) não invalida os access tokens existentes, mas impede emissão de novos.
- Blocklist de JTI: cada JWT tem um
jti(JWT ID) único. Uma blocklist em Redis de JTIs inválidos permite revogação explícita. O custo é um round-trip ao Redis por verificação de token — elimina parcialmente o benefício de stateless. Tamanho gerenciável: apenas tokens ainda dentro do TTL precisam estar na blocklist. - Token introspection (RFC 7662): o resource server consulta o authorization server para verificar se o token ainda é válido. Stateful por natureza — abandona completamente o benefício de verificação local, mas permite revogação imediata.
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
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.
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.
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.
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
-
Reproduzir a vulnerabilidade alg:none em ambiente controlado. Em Python, usando PyJWT com configuração incorreta (
algorithms=Noneou sem o parâmetro), decodifique um JWT RS256 válido, modifique uma claim comorolesousub, 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. -
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.jsoncom okid, e emite JWTs comkidno header. Implemente um resource server que verifica tokens buscando a chave pelokidcom 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. -
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 aoexpdo 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 seuexp.
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. -
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. -
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 cominvalid 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
- docs RFC 7519 — JSON Web Token (JWT) — IETF.
- docs RFC 7517 — JSON Web Key (JWK) — IETF.
- article Critical Vulnerabilities in JSON Web Token Libraries — Tim McLean (2015).
- article JWT Security Best Practices — Curity (curity.io, 2023).
- docs RFC 7662 — OAuth 2.0 Token Introspection — IETF.
- article JWT Algorithm Confusion Attacks — PortSwigger Web Security Academy.
- article JWT: Don't Use It for Sessions — Sven Slootweg (sealedabstract.com, clássico).
- book API Security in Action — Neil Madden (Manning, 2020).
- article JWKS Key Rotation Without Downtime — Auth0 Blog.
- paper SoK: Authentication in the Wild — IEEE S&P 2022.
- docs jwt.io — JWT Debugger e Bibliotecas.
- article Stop Using JWTs as Session Tokens — Scott Arciszewski (paragonie.com, 2018).