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:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c3JfMDFIWlg3IiwiZW1haWwiOiJ2YWxAYWNtZS5jb20i...
.GdLk2…SignatureBytes…{
"sub": "usr_01HZX7…",
"email": "val@acme.com",
"roles": ["admin", "billing"],
"exp": 1773292800
}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: nonenã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.