JWS frente a JWE es la cuestión de qué protege un token JOSE. Un JSON Web Signature (RFC 7515) añade integridad y autenticidad a una carga útil que sigue siendo legible; un JSON Web Encryption (RFC 7516) añade confidencialidad. Casi todos los JWT que te encuentras son JWS, lo que sorprende a quien espera que los claims sean secretos.
De un vistazo
| JWS | JWE | |
|---|---|---|
| Especificación | RFC 7515 | RFC 7516 |
| Protege | Integridad y origen | Confidencialidad (e integridad mediante el AEAD) |
| Forma compacta | header.payload.signature (3 partes) | header.encrypted_key.iv.ciphertext.tag (5 partes) |
| Algoritmos del encabezado | alg (por ejemplo, ES256, RS256, HS256, Ed25519) | alg para la gestión de claves (ECDH-ES, ECDH-ES+A256KW, RSA-OAEP-256, A256KW, dir) más enc para el contenido (A256GCM, A128CBC-HS256) |
| Carga útil visible | Sí, solo en base64url | No |
| Remitente autenticado | Sí (firma o MAC) | Solo con gestión de claves simétrica o con acuerdo de claves autenticado; no con ECDH-ES ni con RSA-OAEP por sí solos |
| Tamaño para un conjunto de claims de 70 bytes (medido más abajo) | 186 caracteres (ES256) | 405 caracteres (ECDH-ES+A256KW con A256GCM) |
| Usos típicos | Tokens JWT de acceso y de ID, DPoP, solicitudes firmadas, SD-JWT | Tokens de ID cifrados, tokens almacenados en el cliente, algunas respuestas de FAPI y EUDI |
Qué protege cada uno
Un JWS firma base64url(header) + "." + base64url(payload). Cualquiera que tenga el token puede decodificarlo y leer los claims; el decodificador de JWT hace exactamente eso. La firma demuestra que el token procede del titular de la clave y que nadie lo ha modificado, que es lo que necesita una API para confiar en los claims.
Un JWE genera una clave de cifrado de contenido (CEK) aleatoria, cifra la carga útil con el AEAD que indica enc y entrega la CEK al destinatario con el algoritmo de gestión de claves que indica alg: envuelta con RSA-OAEP, envuelta con AES Key Wrap tras un acuerdo de claves ECDH-ES, derivada directamente de ECDH-ES o compartida de antemano (dir). El encabezado protegido se autentica como datos asociados. Solo el destinatario puede leer la carga útil.
La asimetría importante: si la gestión de claves usa criptografía de clave pública, cualquiera que tenga la clave pública del destinatario puede crear un JWE válido. Que el descifrado funcione no demuestra nada sobre el remitente. Por eso los tokens sensibles en los que además hay que confiar se anidan: un JWT firmado (JWS) dentro de un JWE con cty: JWT, que se verifica después del descifrado.
Tamaños en la práctica
El script siguiente, ejecutado con jose 6.2.13 en Node.js 26.10, protege los mismos claims de las dos formas.
import { CompactEncrypt, CompactSign, compactDecrypt, compactVerify, generateKeyPair } from "jose";
const claims = new TextEncoder().encode(JSON.stringify({ sub: "user-42", email: "alice@example.com", plan: "pro" }));
// JWS: integrity and origin; the payload is only base64url-encoded.
const signer = await generateKeyPair("ES256");
const jws = await new CompactSign(claims).setProtectedHeader({ alg: "ES256" }).sign(signer.privateKey);
await compactVerify(jws, signer.publicKey);
// JWE: confidentiality for the holder of the recipient's private key.
const recipient = await generateKeyPair("ECDH-ES+A256KW");
const jwe = await new CompactEncrypt(claims).setProtectedHeader({ alg: "ECDH-ES+A256KW", enc: "A256GCM" }).encrypt(recipient.publicKey);
const { plaintext } = await compactDecrypt(jwe, recipient.privateKey);
console.log("JWS parts:", jws.split(".").length, "length:", jws.length, "payload readable:", Buffer.from(jws.split(".")[1], "base64url").toString().includes("alice"));
console.log("JWE parts:", jwe.split(".").length, "length:", jwe.length, "decrypts:", new TextDecoder().decode(plaintext).includes("alice"));
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true
La mayor parte de la sobrecarga del JWE se debe a la clave pública efímera (epk) del encabezado, la clave envuelta, el IV de 12 bytes y la etiqueta de 16 bytes. Un token anidado, firmado y después cifrado, crece todavía más, porque el JWS entero pasa a ser el texto en claro del JWE.
Trampas habituales
- Tratar como secretos los claims de un JWS. No pongas datos personales, identificadores internos que no publicarías ni secretos en un token que solo está firmado: acabará en logs, en el almacenamiento del navegador y en tickets de soporte.
- Confiar en un JWE sin firmar. El descifrado con ECDH-ES o RSA-OAEP solo demuestra que el token se cifró para ti.
- Algoritmos JWE débiles u obsoletos.
RSA1_5es vulnerable a ataques de oráculo de relleno (padding oracle),PBES2con un número de iteracionesp2cenorme puede usarse para una denegación de servicio, y las implementaciones deECDH-ESque aceptaban puntos fuera de la curva filtraron claves privadas en 2017 (RFC 8725 §2.5). PrefiereECDH-ES+A256KWoRSA-OAEP-256conA256GCM, y limita las iteraciones de PBES2 si tienes que admitirlo. - Algoritmo elegido por el token. Igual que con JWS, el destinatario debería aceptar solo los valores de
algyencque espera para cada clave. - Compresión. El
zip: DEFde JWE comprime antes de cifrar, lo que puede filtrar información sobre el texto en claro a través de la longitud (RFC 8725 §3.6); evítalo.
Cuándo elegir cada uno
- Tokens de acceso que comprueban tus API: JWS; reduce los claims al mínimo o usa tokens opacos con introspección si no deben ser legibles.
- Tokens de ID con datos personales, almacenados por un navegador o una aplicación móvil: lo habitual es JWS; un JWE anidado si tu modelo de amenazas incluye el entorno del cliente.
- Tokens que pasan por intermediarios que no son de confianza (redirecciones a través de terceros, tokens incrustados en URL o en códigos QR): JWS anidado dentro de un JWE.
- Mensajes entre máquinas sobre TLS con autenticación mutua: a menudo ninguno de los dos; TLS ya aporta confidencialidad e integridad en tránsito, y un JWS solo aporta valor si el mensaje debe seguir siendo verificable después de almacenarse o reenviarse.
- Cargas útiles cifradas de extremo a extremo para un destinatario conocido: JWE con
ECDH-ES+A256KWyA256GCM, o formatos basados en HPKE a medida que se estandaricen para JOSE.
Notas de migración
- Clasifica los claims de los tokens JWS existentes: todo lo que no deba ser visible para el titular sale del token o pasa a un JWE.
- Introduce los tokens anidados de forma gradual: emite JWE con
cty: JWTpara los clientes que puedan descifrarlos y deja sin cambios el JWS interior para reutilizar la lógica de verificación. - Distribuye las claves de cifrado por separado de las claves de firma: las entradas del JWKS deberían tener
use: encouse: sigy valores dekiddistintos. - Fija los algoritmos en ambos extremos y rechaza
RSA1_5,PBES2sin límite de iteraciones y la compresión. - Vuelve a revisar los logs: una vez cifrados los tokens, asegúrate de que los claims descifrados no se registran en lugares donde el token nunca estuvo. El inspector de JWE muestra el encabezado y la estructura de un JWE sin descifrarlo.