Skip to content
ecdsa.com

JWS vs JWE: signed or encrypted JOSE tokens

A JWS proves who created a payload but anyone can read it. A JWE hides the payload from everyone but its recipient but, with public-key algorithms, says nothing about who created it.

JWS vs JWE is the question of what a JOSE token protects. A JSON Web Signature (RFC 7515) adds integrity and authenticity to a payload that stays readable; a JSON Web Encryption (RFC 7516) adds confidentiality. Almost every JWT you meet is a JWS, which surprises people who expect the claims to be secret.

At a glance

JWSJWE
SpecificationRFC 7515RFC 7516
ProtectsIntegrity and originConfidentiality (and integrity via the AEAD)
Compact formheader.payload.signature (3 parts)header.encrypted_key.iv.ciphertext.tag (5 parts)
Header algorithmsalg (for example ES256, RS256, HS256, Ed25519)alg for key management (ECDH-ES, ECDH-ES+A256KW, RSA-OAEP-256, A256KW, dir) plus enc for content (A256GCM, A128CBC-HS256)
Payload visibleYes, base64url onlyNo
Sender authenticatedYes (signature or MAC)Only with symmetric or authenticated key agreement; not with ECDH-ES or RSA-OAEP alone
Size for a 70-byte claim set (measured below)186 characters (ES256)405 characters (ECDH-ES+A256KW with A256GCM)
Typical usesJWT access and ID tokens, DPoP, signed requests, SD-JWTEncrypted ID tokens, tokens stored client-side, some FAPI and EUDI responses

What each one protects

A JWS signs base64url(header) + "." + base64url(payload). Anyone holding the token can decode and read the claims; the JWT debugger does exactly that. Signing proves the token came from the key holder and was not altered, which is what an API needs to trust claims.

A JWE generates a random content encryption key (CEK), encrypts the payload with an AEAD named by enc, and delivers the CEK to the recipient with the key-management algorithm named by alg: wrapped with RSA-OAEP, wrapped with AES key wrap after ECDH-ES key agreement, derived directly from ECDH-ES, or pre-shared (dir). The protected header is authenticated as associated data. Only the recipient can read the payload.

The important asymmetry: with public-key key management, anyone who has the recipient's public key can create a valid JWE. Decrypting successfully proves nothing about the sender. That is why sensitive tokens that must also be trusted are nested: a signed JWT (JWS) placed inside a JWE with cty: JWT, verified after decryption.

Sizes in practice

The script below, run with jose 6.2.13 on Node.js 26.10, protects the same claims both ways.

jose: the same claims as a JWS and as a JWE
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"));
Output
JWS parts: 3 length: 186 payload readable: true
JWE parts: 5 length: 405 decrypts: true

Most of the JWE overhead is the ephemeral public key (epk) in the header, the wrapped key, the 12-byte IV and the 16-byte tag. A nested signed-then-encrypted token grows further, since the whole JWS becomes the JWE plaintext.

Pitfalls

  • Treating JWS claims as secret. Do not put personal data, internal identifiers you would not publish, or secrets in a signed-only token; it will end up in logs, browser storage and support tickets.
  • Trusting an unsigned JWE. Decryption with ECDH-ES or RSA-OAEP proves only that the token was encrypted to you.
  • Weak or legacy JWE algorithms. RSA1_5 is vulnerable to padding-oracle attacks, PBES2 with a huge p2c iteration count can be used for denial of service, and ECDH-ES implementations that accept points off the curve leaked private keys in 2017 (RFC 8725 §2.5). Prefer ECDH-ES+A256KW or RSA-OAEP-256 with A256GCM, and cap PBES2 iterations if you must support it.
  • Algorithm choice by the token. As with JWS, the recipient should accept only the alg and enc values it expects for each key.
  • Compression. JWE's zip: DEF compresses before encrypting, which can leak information about the plaintext through length (RFC 8725 §3.6); avoid it.

When to choose which

  • Access tokens checked by your APIs: JWS; keep the claims minimal, or use opaque tokens with introspection if they must not be readable.
  • ID tokens with personal data stored by a browser or mobile app: JWS is usual; nested JWE if your threat model includes the client environment.
  • Tokens passed through untrusted intermediaries (redirects through third parties, tokens embedded in URLs or QR codes): nested JWS inside JWE.
  • Machine-to-machine messages over mutually authenticated TLS: often neither; TLS already provides confidentiality and integrity in transit, and a JWS adds value only if the message must stay verifiable after it is stored or forwarded.
  • End-to-end encrypted payloads to a known recipient: JWE with ECDH-ES+A256KW and A256GCM, or HPKE-based formats as they are standardized for JOSE.

Migration notes

  1. Classify claims in existing JWS tokens: anything that must not be visible to the holder either leaves the token or moves into a JWE.
  2. Introduce nested tokens gradually: issue cty: JWT JWEs to clients that can decrypt them, and keep the inner JWS unchanged so verification logic is reused.
  3. Distribute encryption keys separately from signing keys: JWKS entries should have use: enc or use: sig and distinct kid values.
  4. Pin algorithms on both sides and reject RSA1_5, unbounded PBES2 and compression.
  5. Re-check logging: once tokens are encrypted, make sure decrypted claims are not logged where the token never was. The JWE inspector shows a JWE's header and structure without decrypting it.

References

All comparisons