← Todos os posts
jwt

JWT vs JWE: quando assinar não é o suficiente

Todo engenheiro de backend já emitiu um JWT. Bem menos gente já emitiu um JWE. Os dois pertencem à mesma família de specs — JOSE (JSON Object Signing and Encryption) — mas respondem perguntas diferentes, e o hábito de emitir JWT por padrão é silenciosamente errado em algumas superfícies.

Esta é uma nota curta sobre a diferença, a armadilha em que as pessoas caem, e o que o Authaz emite por padrão.

Um JWT assinado não é criptografado

A coisa rotulada como "JWT" em 90% dos tutoriais é, mais precisamente, um JWS (JSON Web Signature) — um payload codificado em base64 com uma assinatura anexada:

JWT assinado (JWS · RS256)
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
  .eyJzdWIiOiJ1c3JfMDFIWlg3IiwiZW1haWwiOiJ2YWxAYWNtZS5jb20i...
  .GdLk2…SignatureBytes…
{
  "sub":   "usr_01HZX7…",
  "email": "val@acme.com",
  "roles": ["admin", "billing"],
  "exp":   1773292800
}
JWT criptografado (JWE · RSA-OAEP-256 + A256GCM)
eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0
  .OKOawDo13gRp2ojaHV7LFpZcgV7T6DV…   CEK criptografada
  .48V1_ALb6US04U3b…                  IV
  .5eym8TW_c8SuK0lt…                  ciphertext opaco
  .XFBoMYUZodetZdvTiFvSkQ             auth tag
[ ilegível até ser decifrado com a chave privada do destinatário ]

Um JWT assinado tem integridade protegida — qualquer um pode ler as claims, mas ninguém forja sem a chave de assinatura. Um JWT criptografado (JWE) tem integridade E confidencialidade protegidas — ninguém lê as claims sem a chave privada do destinatário.

A armadilha: muita gente trata o payload em base64 como se fosse secreto. Não é. Qualquer um com o devtools do navegador pode colar o JWT em jwt.io e ler todas as claims. Se você colocar PII, IDs internos de usuário ou feature flags ali, você publicou tudo isso.

Quando assinado é o suficiente

Para a maior parte da auth de sessão e API, assinado está certo:

  • O token é de curta duração.
  • As claims são coisas que o usuário já sabe sobre si mesmo (ID dele, papéis dele).
  • O destinatário é um backend confiável que só precisa verificar o token, não proteger o conteúdo.

JWTs assinados são baratos de verificar, podem ser cacheados via JWKS, e sobrevivem a todo CDN, proxy e ferramenta de observabilidade pelo qual você vai roteá-los. Esse é o default certo.

Quando JWE compensa o peso

Você quer JWE quando as próprias claims são sensíveis E o token transita por superfícies em que você não confia. Alguns casos reais:

  • Tokens de step-up carregando um ID de prontuário médico de uso único que não devem nunca aparecer num log de CDN.
  • Refresh tokens persistidos no storage de um app mobile que você não quer que um usuário curioso extraia e inspecione.
  • Handoffs de federação para terceiros em que você envia atributos (flags de compliance, nomes internos de tier) que o terceiro não deveria ver caso ele rastreie logs.
  • Tokens de parceiro B2B carregando segredos de tenant — chaves, connection strings fornecidas pelo cliente — que não deveriam sobreviver a um arquivo HAR compartilhado por engano.

O padrão mais forte, quando as claims são ao mesmo tempo sensíveis e precisam ser autenticadas, é JWS aninhado dentro de JWE: assina primeiro (qualquer parte com a chave de verificação detecta adulteração), criptografa depois (só o portador da chave lê o conteúdo). Os dois prováveis modos de falha caem juntos.

O que o Authaz emite

Por padrão, o Authaz emite JWTs assinados (RS256) para access tokens e session tokens. As claims são propositalmente mínimas: sub, aud, iss, exp, org, roles, amr. Nada ali é informação que o usuário não devesse ver sobre si mesmo.

Onde importa, mudamos:

  • Refresh tokens são opacos — estado server-side. Não enviamos claims estruturadas em tokens de longa duração.
  • Step-up assertions para ações sensíveis podem ser emitidas como JWE com a chave pública do resource server. As claims ficam seladas ao longo de CDNs, pipelines de auditoria e do próprio cliente.
  • Tokens de federação cross-tenant para IdPs fornecidos pelo comprador podem ser emitidos como JWS aninhado dentro de JWE — o parceiro verifica, decifra e consome, mas nenhum observador no caminho consegue inspecionar.

A escolha é configurável por recurso, não por tenant. A maioria dos apps precisa de um único verificador de JWT assinado e nunca toca em JWE; as superfícies que precisam, optam por isso sem afetar o resto.

Armadilhas que vale memorizar

  • alg: none não é uma feature. Rejeite no verificador. Várias libs já honraram isso.
  • Sempre fixe o alg. Não deixe o token te dizer qual algoritmo usar; o verificador já sabe.
  • Não confie no kid. Use para selecionar uma chave do seu JWKS — nunca para buscar uma chave remota sob demanda.
  • Rotacione chaves. Uma chave de assinatura que vive para sempre é uma chave que o próximo vazamento leva junto.
  • JWE não substitui HTTPS. Sela o payload, mas o token continua pertencendo ao header Authorization: Bearer, não a uma query string.

TL;DR

JWT assinado responde "quem é essa pessoa e dá pra confiar nas claims?" — perfeito para ~95% das integrações de auth.

JWE responde "quem é essa pessoa, dá pra confiar nas claims, E dá pra manter o conteúdo privado de todo mundo no caminho?" — e você só precisa disso nas superfícies que justificam.

Saber qual pergunta o seu token está respondendo é a diferença entre uma auth que sobrevive a uma revisão de segurança e uma auth que vaza claims silenciosamente por anos. Se você quer conversar com a gente sobre qualquer um dos padrões, estamos aqui.

← Post mais recente · Segurança não é feature premium.Auth como código: um YAML, todo ambiente · Post anterior →