Segurança

JWT: anatomia, assinatura e segurança

O maior mal-entendido sobre JSON Web Tokens cabe em uma frase: um JWT não esconde nada. Ele é assinado, não cifrado. As três partes que você vê separadas por pontos são apenas texto codificado em base64url, qualquer pessoa com o token lê o conteúdo em segundos, sem chave nenhuma. A assinatura não protege o segredo; ela prova que ninguém adulterou o que está escrito. Entender essa distinção é o que separa quem usa JWT com segurança de quem coloca a senha do usuário no payload achando que está protegida. Este guia destrincha as três partes, percorre as claims registradas da [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519), reconstitui os ataques clássicos e termina com o critério que quase ninguém aplica: quando NÃO usar JWT. Cole um token real no [JWT Decoder](tool:jwt-decoder) enquanto lê, ele decodifica e verifica HMAC localmente, sem enviar nada para um servidor.

J-Kit12 min de leituraAvançado
  • JWT
  • Autenticação
  • Segurança
  • Criptografia

Resumo rápido

  • Um JWT tem três partes em base64url: header, payload e assinatura. As duas primeiras são apenas codificadas, não cifradas, legíveis por qualquer um.
  • A assinatura garante integridade e autoria, não sigilo. Nunca coloque dados sensíveis no payload de um JWT assinado.
  • Os ataques clássicos (alg:none, confusão RS256→HS256, injeção de kid/jku) exploram bibliotecas que confiam no header para escolher o algoritmo ou a chave.
  • JWT é stateless: um token vale até o exp e não pode ser invalidado sem reintroduzir estado no servidor. Para sessões de app web, um cookie opaco costuma ser melhor.

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 }
As três partes e o JSON que cada uma esconde. Nada aqui é secreto: base64url é reversível por qualquer um.
3blocos separados por ponto
base64urlcodificação de cada bloco (RFC 4648 §5)
0chaves necessárias para LER o payload
Cole o token acima: o decoder separa as três partes e mostra o JSON, tudo no navegador, sem rede.Abrir a ferramenta em página inteira

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)
A assinatura HS256: um HMAC-SHA256 sobre as duas primeiras partes já codificadas, com uma chave secreta. Trocar qualquer byte do header ou do payload muda a assinatura por completo.
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.

As sete claims registradas da RFC 7519 §4.1. Todas opcionais no padrão, mas você deve exigir e validar as que importam.
ClaimNomeSignificadoObrigatória?
issIssuerQuem emitiu o token.Opcional
subSubjectA quem o token se refere (o usuário).Opcional
audAudiencePara quais destinatários o token é válido.Opcional
expExpiration TimeInstante a partir do qual o token NÃO deve ser aceito.Opcional
nbfNot BeforeInstante antes do qual o token NÃO deve ser processado.Opcional
iatIssued AtQuando o token foi emitido.Opcional
jtiJWT IDIdentificador ú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?
Não, na forma usual não. O JWT do dia a dia é um JWS (RFC 7515): assinado, não cifrado. Header e payload são apenas base64url, legíveis por qualquer um sem chave. Existe um JWT criptografado, o JWE (RFC 7516), mas é raro e tem cinco partes. Por isso nunca coloque dados sensíveis no payload de um JWT comum.
O que é o ataque alg:none?
É quando o atacante define `{"alg":"none"}` no header, o que significa "token sem assinatura", e uma biblioteca ingênua pula a verificação e aceita qualquer payload. A defesa é rejeitar `none` e exigir que o algoritmo esteja em uma allowlist do servidor, como recomenda a RFC 8725.
Posso invalidar um JWT antes de ele expirar?
Não sem reintroduzir estado. JWT é stateless: um token vale até o `exp` e não há registro no servidor para desligar. As saídas são uma denylist de `jti` consultada a cada requisição (com estado de novo) ou access tokens curtos com refresh tokens revogáveis. Ou, em emergência, trocar o segredo, o que derruba todos os tokens.
Quais claims são obrigatórias em um JWT?
Nenhuma. A RFC 7519 §4.1 define sete claims registradas (iss, sub, aud, exp, nbf, iat, jti) e diz que nenhuma é obrigatória de usar. Na prática, você deve exigir e validar `exp` para limitar a vida do token, e `aud`/`iss` para garantir que o token era para o seu serviço e veio de quem você espera.
JWT ou cookie de sessão: qual usar?
Para a sessão de login de um app web com backend próprio, um cookie de sessão opaco costuma ser melhor: revogação instantânea, sem confusão de algoritmo e sem risco de vazar dado no payload. JWT vale a pena quando vários serviços precisam validar o mesmo token sem compartilhar um banco de sessões, em federação ou em asserções assinadas de curta duração.

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.

Fontes e referências

  1. RFC 7519, JSON Web Token (JWT)
  2. RFC 7515, JSON Web Signature (JWS)
  3. RFC 7516, JSON Web Encryption (JWE)
  4. RFC 7518, JSON Web Algorithms (JWA)
  5. RFC 8725, JSON Web Token Best Current Practices (BCP 225)
  6. RFC 4648 §5, Base 64 Encoding with URL and Filename Safe Alphabet
  7. OWASP, JSON Web Token Cheat Sheet