By AndyPublished
Is JWT Encrypted? JWS vs JWE vs JWT Explained
Encrypted JWTs do exist. They use JWE (RFC 7516), have five dot-separated parts instead of three, and can only be read by the holder of the decryption key. You can tell which one you have by counting the dots: two dots means signed and readable, four dots means encrypted.
Is a JWT Encoded or Encrypted?
A signed JWT is encoded. Base64url turns bytes into URL-safe text and back again with no key involved. One line of Node.js reads the payload of any signed JWT:
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
console.log(JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()));
// { sub: '1234567890', name: 'John Doe', iat: 1516239022 }No secret was needed. The same is true in a browser, where the jwtdecode.app decoder reads the header and payload locally. This is by design: resource servers, API gateways and clients often need to read claims such as exp or scope without holding a secret. The details of encoding are in JWT header, payload and signature explained.
JWS vs JWE vs JWT: How the Terms Relate
The three acronyms come from the JOSE (JSON Object Signing and Encryption) family of specifications and describe different things:
- ·JWT (RFC 7519) defines a set of claims as a JSON object:
iss,sub,aud,expand so on. It does not define how to protect them. - ·JWS (RFC 7515) is a container that signs any payload. A JWT inside a JWS is a signed JWT.
- ·JWE (RFC 7516) is a container that encrypts any payload. A JWT inside a JWE is an encrypted JWT.
So “JWT vs JWS” is not really a choice: a JWT is always carried as either a JWS or a JWE. When people say “JWT” they almost always mean a JWS-signed JWT. The algorithms both use are registered in RFC 7518 (JWA), and keys can be expressed as JWKs (RFC 7517).
JWS vs JWE: Signed vs Encrypted Tokens
| Aspect | JWS (signed JWT) | JWE (encrypted JWT) |
|---|---|---|
| Specification | RFC 7515 | RFC 7516 |
| Protects | Integrity and authenticity | Confidentiality, plus integrity of the ciphertext |
| Payload readable by holder | Yes, it is only Base64url-encoded | No, only the recipient with the decryption key |
| Compact form | 3 parts: header.payload.signature | 5 parts: header.encryptedKey.iv.ciphertext.tag |
| Header algorithm fields | alg (e.g. RS256, ES256, HS256) | alg (key management) and enc (content encryption) |
| Who holds which key | Issuer signs with private key; anyone verifies with public key | Sender encrypts with recipient’s public key; only recipient decrypts |
| Proves who issued it | Yes (asymmetric algorithms) | Not on its own; sign first, then encrypt (nested JWT) |
| Typical use | Access tokens, ID tokens, almost all JWTs | Tokens carrying personal data through untrusted parties, encrypted session cookies |
Note the key direction flips. With JWS the issuer holds the private key and everyone else verifies with the public key. With JWE the recipient holds the private key and anyone with the public key can encrypt to them, which means encryption alone says nothing about who created the token.
What a JWE Looks Like: the Five Parts
A JWE in compact serialisation has five Base64url segments:
BASE64URL(protected header) . BASE64URL(encrypted key) . BASE64URL(initialisation vector) . BASE64URL(ciphertext) . BASE64URL(authentication tag)
- ·Protected header: readable JSON such as
{"alg":"RSA-OAEP-256","enc":"A256GCM","kid":"enc-2026"}. It is not encrypted, but it is authenticated as additional data, so it cannot be altered. - ·Encrypted key: a random content encryption key (CEK), encrypted for the recipient. Empty when
algisdiror plainECDH-ES. - ·IV: the nonce for the content cipher, random per token.
- ·Ciphertext: the encrypted claims.
- ·Authentication tag: detects any modification of the header, IV or ciphertext.
The only readable part is the header, which usually tells you the algorithms and the kid of the key needed to decrypt.
How to Tell if a JWT Token Is Encrypted
You can decide in seconds without any key:
- ·Count the segments. Three segments (two dots) means JWS; five segments (four dots) means JWE. Nothing else is valid compact serialisation.
- ·Decode the first segment. Both forms start with a readable JSON header, usually beginning
eyJ. A JWS header hasalgvalues such asRS256orES256; a JWE header has bothalg(for exampleRSA-OAEP-256) andenc(for exampleA256GCM). - ·Try to decode the second segment. In a JWS it is JSON claims. In a JWE it is an encrypted key (or empty) and decodes to random bytes.
- ·No dots at all means neither: it is an opaque reference token that only its issuer can interpret.
Identity providers almost always issue signed access and ID tokens. When you do meet a JWE in the wild, it is typically an encrypted ID token requested by a client, a framework’s encrypted session cookie, or a token in a regulated profile such as open banking. How providers use each token type is covered in JWTs in OAuth 2.0 and OpenID Connect.
use: sig and use: enc, each with its own kid. Reusing one key pair for both purposes is a long-standing cryptographic anti-pattern.JWT Encryption Algorithms: alg and enc
JWE uses two algorithms together. alg decides how the content key reaches the recipient; enc decides how the claims themselves are encrypted with that key.
Key management (alg)
| alg | What it does |
|---|---|
| RSA-OAEP-256 | Random content key, encrypted to the recipient’s RSA public key with OAEP and SHA-256. A sound default. |
| RSA-OAEP | As above with SHA-1 in OAEP. Still widely supported; prefer the SHA-256 variant for new systems. |
| RSA1_5 | RSA PKCS#1 v1.5 key encryption. Prone to padding-oracle attacks; avoid (RFC 8725). |
| ECDH-ES / ECDH-ES+A256KW | Ephemeral elliptic-curve key agreement with the recipient’s EC public key; direct or with AES key wrap. |
| A128KW / A256KW | Content key wrapped with a shared AES key. |
| dir | No key management: the shared symmetric key is used directly as the content key. The encrypted-key part is empty. |
Content encryption (enc)
| enc | What it does |
|---|---|
| A256GCM / A128GCM | AES in Galois/Counter Mode. Authenticated, fast, the common choice. |
| A256CBC-HS512 / A128CBC-HS256 | AES-CBC with an HMAC-SHA-2 tag, combined as an authenticated scheme. Used by Auth.js session cookies. |
Sensible defaults: RSA-OAEP-256 or ECDH-ES+A256KW with A256GCM when sender and recipient are different parties, and dir with A256GCM or A256CBC-HS512 when the same service both writes and reads the token, as with an encrypted session cookie. As with signing, the recipient should allow-list the alg and enc values it accepts rather than trusting the header.
JWT Encryption in Practice
Encrypting and decrypting a JWT with the jose library (v6) in Node.js:
import { EncryptJWT, jwtDecrypt, generateKeyPair } from 'jose';
// The recipient's key pair; in production the sender only has the public key.
const { publicKey, privateKey } = await generateKeyPair('RSA-OAEP-256');
const jwe = await new EncryptJWT({ sub: 'user_123', email: 'ada@example.com' })
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
.setIssuer('https://auth.example.com')
.setAudience('https://api.example.com')
.setExpirationTime('10m')
.encrypt(publicKey);
console.log(jwe.split('.').length); // 5
const { payload } = await jwtDecrypt(jwe, privateKey, {
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
console.log(payload.email); // 'ada@example.com'Nested JWTs: Signed and Then Encrypted
A plain JWE hides the claims but, with public-key encryption, does not prove who produced them. When you need both confidentiality and a verifiable issuer, sign the JWT first and then encrypt the whole signed token. RFC 7519 §5.2 calls this a nested JWT and requires the outer header to carry "cty": "JWT".
import { SignJWT, CompactEncrypt, compactDecrypt, jwtVerify } from 'jose';
// Sender: sign with its own private key, then encrypt to the recipient.
const signed = await new SignJWT({ sub: 'user_123' })
.setProtectedHeader({ alg: 'ES256' })
.setExpirationTime('10m')
.sign(senderPrivateKey);
const nested = await new CompactEncrypt(new TextEncoder().encode(signed))
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM', cty: 'JWT' })
.encrypt(recipientPublicKey);
// Recipient: decrypt, then verify the inner signature.
const { plaintext } = await compactDecrypt(nested, recipientPrivateKey);
const { payload } = await jwtVerify(new TextDecoder().decode(plaintext), senderPublicKey);Sign-then-encrypt is the recommended order: the signature covers the real claims, and the outer layer hides both the claims and the signature. OpenID Connect uses exactly this when a client registers for encrypted ID tokens.
Do You Need to Encrypt Your JWTs?
Usually not. TLS already protects tokens in transit, and signing covers integrity. Encryption is worth its extra keys and complexity when:
- ·The token carries personal or regulated data and passes through a party that should not read it, such as a browser or a third-party client.
- ·You store session state in a cookie and do not want users to read or learn from it (Auth.js encrypts its JWT sessions with
dirandA256CBC-HS512by default). - ·A regulation or partner profile requires it, as some open banking and high-assurance OIDC profiles do.
Often the simpler answer is to keep sensitive data out of the token altogether: put a user ID in the JWT and look the rest up server-side, or use an opaque token that only the issuer can resolve. Both options are compared in JWT vs opaque access tokens. To confirm a signed token still verifies after you change its contents, see the verification guide.
Summary
- ·A standard JWT is signed (JWS), not encrypted: its payload is Base64url-encoded and readable by anyone who has it.
- ·JWS (RFC 7515) gives integrity with three parts; JWE (RFC 7516) gives confidentiality with five.
- ·JWE combines a key-management
alg(for exampleRSA-OAEP-256ordir) with a contentenc(for exampleA256GCM). - ·For confidentiality and a verifiable issuer, sign first and then encrypt, producing a nested JWT with
cty: JWT. - ·Most systems do not need JWE; keeping secrets out of the payload is simpler.