← Todos os posts
    jwt · jwe · segurança

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

    Rodrigo Vidal
    Rodrigo Vidal5 de maio de 2026 · 6 min de leitura

    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.