O que tem dentro de um JWT — e o que você nunca deve colocar lá
Pegue qualquer JWT que a sua aplicação emite e cole num decodificador. O payload aparece na hora. Sem senha, sem chave, sem requisição ao seu servidor.
Isso não é fraqueza do decodificador. É o formato funcionando como projetado — e quase todo incidente com JWT nasce de alguém supor o contrário.
Três partes, dois pontos
Um JWT é uma única string com dois pontos no meio:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Separe nos pontos e você tem o cabeçalho, o payload e a assinatura. Os dois primeiros são JSON codificado em Base64url — reversível por qualquer um, sem chave nenhuma. Decodifique o primeiro:
{ "alg": "HS256", "typ": "JWT" }
E o segundo:
{ "sub": "1234", "role": "admin" }
A terceira parte é a assinatura: as duas primeiras partes, unidas por um ponto, passadas pelo algoritmo declarado no cabeçalho usando um segredo que só o servidor tem.
Ou seja: a garantia que um JWT oferece é integridade e autenticidade, não confidencialidade. Ele prova que o payload não foi alterado desde que o servidor assinou. Não faz absolutamente nada para esconder o payload.
Base64url, não Base64
A codificação é a variante segura para URL: + vira -, / vira _, e o padding = do fim é removido. Isso importa quando você está depurando à mão, porque colar um segmento de JWT num decodificador Base64 padrão pode falhar até você recolocar o padding.
A regra: acrescente = até o tamanho ser múltiplo de quatro. Um segmento de 22 caracteres precisa de dois, de 23 precisa de um, de 24 não precisa.
O que nunca pertence ao payload
Como qualquer um com o token consegue ler todas as claims, a lista é curta e absoluta.
Qualquer coisa que você não imprimiria num log. Senhas, obviamente. Mas também chaves de API, IDs internos de banco que você prefere não expor, números de licença e documentos. Se seria um problema num print de ticket de suporte, é um problema aqui.
Dados pessoais que você não precisa a cada requisição. E-mail e nome completo são os infratores de rotina. Eles acabam no storage do navegador, no log de acesso do servidor, no payload de relatórios de erro e em qualquer proxy que logue cabeçalhos. Sob a LGPD e o GDPR isso é tratamento que você agora precisa justificar, por uma conveniência de que provavelmente não precisava.
Qualquer coisa que muda mais rápido que o token. Um JWT é uma fotografia tirada no login. Se você põe um conjunto de permissões nele e o usuário é rebaixado, o token continua afirmando a permissão antiga até expirar. Essa é a troca fundamental dos tokens sem estado: você evita uma consulta ao banco por requisição e aceita uma janela de desatualização. Projete a expiração em torno de quanto essa janela pode durar.
As quatro claims que valem conhecer
A especificação registra várias claims. Estas são as que carregam peso:
exp— expiração. Segundos desde a epoch Unix.iat— emitido em. Mesma unidade.nbf— não antes de. O token é inválido até esse instante.sub— o sujeito, ou seja, sobre quem é o token.
Essas unidades são a origem de um bug que se esconde por completo em teste. O Date.now() do JavaScript devolve milissegundos. As claims estão em segundos. Compare direto e todo token parece válido até por volta do ano 55000:
// Errado — compara milissegundos com segundos.
if (payload.exp > Date.now()) { /* sempre verdadeiro */ }
// Certo.
if (payload.exp > Math.floor(Date.now() / 1000)) { /* ... */ }
Nada falha em desenvolvimento, porque os tokens ainda não expiraram. Aparece em produção na primeira vez que um token expirado é aceito. Se você está olhando um timestamp e quer saber o que ele significa, um conversor de timestamp é mais rápido que fazer a conta.
O problema do alg: none
O cabeçalho declara qual algoritmo assinou o token. Um verificador que lê esse campo para decidir como conferir a assinatura está confiando na entrada do atacante para saber como validar a entrada do atacante.
O exploit clássico coloca "alg": "none", remove a assinatura inteira e manda um payload afirmando o que quiser. Uma biblioteca que respeite o cabeçalho vê "nenhum algoritmo", não faz verificação nenhuma e aceita.
Uma variante troca RS256 por HS256. No RSA o servidor verifica com uma chave pública; no HMAC ele verifica com um segredo compartilhado. Se o verificador troca conforme o cabeçalho, um atacante pode assinar um token usando a chave pública como segredo do HMAC — pública, então ele tem — e o servidor valida.
Os dois se corrigem do mesmo jeito: fixe o algoritmo esperado no servidor e ignore o que o token afirma. Toda biblioteca madura suporta isso; normalmente é um argumento que você precisa passar, não um padrão que você ganha.
Decodificar não é verificar
Isso merece título próprio porque é o erro que transforma um mal-entendido em vulnerabilidade.
Decodificar mostra o conteúdo. Não diz nada sobre a assinatura ser válida. Um token adulterado decodifica com exatamente a mesma limpeza de um legítimo — troque "role": "user" por "role": "admin", recodifique, e o decodificador vai te mostrar um token de admin.
Só quem tem a chave de assinatura consegue distinguir. O que significa:
- Verificação pertence ao servidor, sempre.
- Um cliente pode decodificar um token para ler um nome de exibição ou uma expiração para fins de interface. Nunca para tomar decisão de autorização.
- Qualquer endpoint que confie numa claim sem verificar a assinatura está sem autenticação, por mais que a requisição pareça autenticada.
Um checklist curto de projeto
- Coloque em
suba menor coisa que identifique o sujeito, e nada que você se incomodaria de vazar. - Use um
expcurto. Minutos para tokens de acesso, e um mecanismo separado de refresh para longevidade. - Fixe o algoritmo no verificador. Rejeite
noneexplicitamente. - Compare
expem segundos. - Se você realmente precisa do payload escondido, o que você quer é JWE — tokens criptografados — não um token assinado com as partes sensíveis ofuscadas. Base64 não é ofuscação, e nunca foi.
Um JWT é um cartão-postal assinado. Perfeitamente bom para o que faz; só não escreva nele nada que você não queira que o carteiro leia.