As três partes de um JWT
Um JWT é uma string com três blocos separados por pontos: `header.payload.signature`. Cada bloco é um objeto JSON codificado em base64url, a variante da RFC 4648 §5 que troca `+` por `-`, `/` por `_` e descarta o preenchimento `=`, justamente para caber em URLs e cabeçalhos HTTP sem escape. O header diz qual algoritmo assina o token (`alg`) e o tipo (`typ`). O payload carrega as claims, as afirmações sobre o usuário ou a sessão. A assinatura é o que amarra os dois primeiros a uma chave secreta.
// Um JWT real (exemplo didático público, secret = "your-256-bit-secret")
// A real JWT (public didactic example, secret = "your-256-bit-secret")
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← header
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ ← payload
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← signature
// Header decodificado / decoded:
{ "alg": "HS256", "typ": "JWT" }
// Payload decodificado / decoded:
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }Assinado, não cifrado
O formato que você vê no dia a dia é um JWS, JSON Web Signature, definido pela RFC 7515. "Assinado" significa que o emissor calcula um código de autenticação sobre `header.payload` usando uma chave, e o verificador recalcula para confirmar que nada mudou. Se um atacante alterar um único caractere do payload, a assinatura deixa de bater. Mas atenção: assinatura não é cifra. O conteúdo continua em texto claro, apenas codificado. Existe sim um JWT criptografado, o JWE, da RFC 7516,, mas ele é raro, tem cinco partes em vez de três e é outra história. Quando alguém diz "JWT", quase sempre está falando de um JWS legível.
signature = base64url( HMACSHA256( base64url(header) + "." + base64url(payload), secret ) )- HMACSHA256
- código de autenticação de mensagem com SHA-256 e chave
- secret
- chave simétrica conhecida só pelo emissor e pelo verificador
- base64url
- codificação URL-safe sem preenchimento (RFC 4648 §5)
- JWS (RFC 7515)
- Token assinado. Prova integridade e autoria; o conteúdo é legível. É o JWT do cotidiano.
- JWE (RFC 7516)
- Token criptografado. Aí sim o conteúdo fica ilegível sem a chave. Cinco partes, uso raro.
- JWA (RFC 7518)
- O catálogo de algoritmos (HS256, RS256, ES256, none…) que header pode declarar em `alg`.
As claims registradas
O payload é um objeto JSON livre, mas a RFC 7519 (§4.1) reserva sete nomes de claim com significado padronizado. Nenhuma delas é obrigatória, a própria RFC diz que "nenhuma das claims definidas é obrigatória de usar ou implementar em todos os casos". Isso surpreende quem espera que um JWT sempre tenha expiração. Não tem: um token sem `exp` vale para sempre até a chave mudar. Cabe a você exigir as claims que a sua aplicação precisa e validar cada uma delas.
| Claim | Nome | Significado | Obrigatória? |
|---|---|---|---|
| iss | Issuer | Quem emitiu o token. | Opcional |
| sub | Subject | A quem o token se refere (o usuário). | Opcional |
| aud | Audience | Para quais destinatários o token é válido. | Opcional |
| exp | Expiration Time | Instante a partir do qual o token NÃO deve ser aceito. | Opcional |
| nbf | Not Before | Instante antes do qual o token NÃO deve ser processado. | Opcional |
| iat | Issued At | Quando o token foi emitido. | Opcional |
| jti | JWT ID | Identificador único do token (útil para denylist). | Opcional |
As três claims de tempo, `exp`, `nbf`, `iat`, são segundos desde 1º de janeiro de 1970 (Unix epoch). Um verificador correto rejeita o token se o horário atual for maior ou igual a `exp`, ou menor que `nbf`, tolerando um pequeno desvio de relógio (clock skew) de poucos segundos entre servidores. Validar `aud` e `iss` é igualmente importante: um token emitido para o serviço A não deve ser aceito pelo serviço B só porque a assinatura é válida. Assinatura válida responde "foi este emissor que assinou"; ela não responde "este token era para mim".
Os ataques clássicos
Quase todo ataque a JWT explora a mesma falha de base: a biblioteca confia no header do token, que o atacante controla, para decidir como verificar. A RFC 8725 (JWT Best Current Practices, BCP 225) existe justamente para catalogar essas armadilhas. Três delas aparecem repetidas em auditorias reais.
alg: none, a assinatura que não existe
Mecanismo: a JWA (RFC 7518) define um algoritmo literal `"none"`, que significa "token sem assinatura". Uma biblioteca ingênua lê `{"alg":"none"}` no header e simplesmente pula a verificação, aceitando qualquer payload com o campo de assinatura vazio. O atacante forja `{"alg":"none"}`, escreve `{"sub":"admin"}` no payload, deixa a terceira parte em branco e entra.
Mitigação: rejeite `none` sempre. A RFC 8725 exige que a biblioteca permita ao chamador declarar o conjunto de algoritmos aceitos e NÃO use nenhum outro. Nunca deixe o header do token escolher o algoritmo sozinho.
Confusão RS256 → HS256, a chave pública virada em segredo
Mecanismo: RS256 é assimétrico, o servidor assina com a chave privada RSA e qualquer um verifica com a chave pública (que é, por definição, pública). HS256 é simétrico, a MESMA chave assina e verifica via HMAC. O ataque: o código de verificação recebe a chave pública e deixa o `alg` do token escolher o algoritmo. O atacante troca `RS256` por `HS256` no header, pega a chave pública conhecida e a usa como segredo HMAC para reassinar o token adulterado. O servidor, confiando no `alg`, roda HMAC-SHA256 usando a chave pública como segredo, e a conta bate. Token forjado, aceito.
Mitigação: fixe o algoritmo no lado do verificador. A chave deve estar amarrada ao algoritmo esperado: uma chave RSA só verifica RS256, e ponto. Não deixe o token dizer se é HMAC ou RSA. Ferramentas modernas exigem uma allowlist explícita justamente para fechar essa porta.
Injeção de kid / jku / x5u, apontando para a chave errada
Mecanismo: o header pode trazer `kid` (id da chave), `jku` (URL de um conjunto de chaves) ou `x5u` (URL de um certificado). Se o servidor usa `kid` para montar uma consulta SQL ou um caminho de arquivo sem validar, abre injeção de SQL ou path traversal. Se busca a chave na URL de `jku`/`x5u` sem restrição, o atacante aponta para um servidor que ele controla, entrega a própria chave e faz o token ser aceito, além de expor o servidor a SSRF.
Mitigação: a RFC 8725 recomenda tratar `kid` como dado não confiável (nada de concatenar em query ou caminho) e casar `jku`/`x5u` contra uma allowlist de locais confiáveis. Melhor ainda: nunca busque a chave a partir de um valor do token; use um conjunto de chaves configurado no servidor.
Repare no fio comum: nos três, o servidor delegou ao atacante uma decisão que era dele. O antídoto único e barato é uma allowlist do lado do servidor, do algoritmo e da chave, combinada com a validação das claims de tempo, `aud` e `iss`. O JWT Decoder verifica assinatura HMAC informando o segredo, o que ajuda a reproduzir e entender a confusão RS256/HS256 num ambiente seguro, sem enviar seu token a lugar nenhum.
Revogação: o calcanhar de Aquiles
O apelo do JWT é ser stateless: o servidor não guarda sessão nenhuma, ele só confere a assinatura e confia no que está escrito. Esse é também o seu maior defeito. Um token assinado é válido até o `exp`, e não existe um registro no servidor para "desligar". Se o usuário faz logout, é banido ou troca a senha, o token que já está na mão dele continua funcionando até expirar. Não há um botão que invalide um JWT emitido, a menos que você reintroduza estado.
Denylist (lista de revogados)
- Guarda os `jti` revogados e consulta a cada requisição.
- Revogação imediata, mas reintroduz a busca com estado que o JWT prometia evitar.
Access token curto + refresh
- Access token vive minutos; refresh token, mais longo, fica no servidor e pode ser revogado.
- A janela de abuso encolhe para a duração do access token, sem consulta a cada request.
As duas saídas comuns aparecem acima, e as duas custam algo. A denylist devolve ao servidor o estado que o JWT queria dispensar; os tokens curtos com refresh reduzem o problema em vez de eliminá-lo, ao preço de mais complexidade no fluxo. Não existe revogação instantânea gratuita em um sistema genuinamente stateless, é uma escolha de engenharia, não um detalhe de configuração. Se o segredo HMAC vaza, some com esse dilema: você troca a chave e todos os tokens caem de uma vez. Gere um segredo longo e aleatório para HS256 com o gerador de hash e guarde-o como você guardaria uma senha mestra.
Validação segura e quando NÃO usar JWT
- Valide o `alg` contra uma allowlist do servidor; rejeite `none` e qualquer algoritmo fora da lista.
- Amarre a chave ao algoritmo esperado: uma chave RSA verifica RS256, jamais HS256. Nunca deixe o token escolher o tipo de chave.
- Verifique `exp` e `nbf` (com pequeno clock skew) e cheque `aud` e `iss` contra os valores que a sua aplicação espera.
- Nunca confie em `kid`, `jku` ou `x5u` para buscar a chave; use um conjunto de chaves configurado no servidor.
- Use segredos longos e aleatórios para HMAC; nada de palavra de dicionário ou string curta reutilizada.
- Nunca ponha dado sensível no payload, ele é apenas base64url, legível por qualquer um.
Feita a lição de segurança, vem a pergunta mais honesta e menos feita: você precisa mesmo de JWT? Para a sessão de um usuário em um app web com backend próprio, quase sempre a resposta é não. Um cookie de sessão opaco, um id aleatório que aponta para um registro de sessão no servidor, é mais simples e mais seguro: revogação é apagar a linha, não há superfície de confusão de algoritmo, não há risco de vazar dado no payload e o cookie é minúsculo. O JWT brilha em outro cenário: autorização entre serviços, quando vários backends precisam validar o token sem compartilhar um banco de sessões, em federação de identidade e em asserções assinadas de curta duração. Se o seu caso é "usuário loga e navega no meu site", prefira a sessão opaca.
Perguntas frequentes
JWT é criptografado?
O que é o ataque alg:none?
Posso invalidar um JWT antes de ele expirar?
Quais claims são obrigatórias em um JWT?
JWT ou cookie de sessão: qual usar?
Um JWT é assinado, não cifrado: as três partes são base64url legível, e a assinatura só garante que ninguém adulterou o conteúdo. Valide o `alg` contra uma allowlist, amarre a chave ao algoritmo, cheque `exp`/`nbf`/`aud`/`iss` e nunca deixe o header escolher a chave, é assim que você fecha alg:none, confusão RS256/HS256 e injeção de kid. E lembre que revogar um JWT exige estado: para sessão de app web com backend próprio, o cookie opaco costuma ser a escolha mais simples e segura.