By AndyPublished

Is JWT Encrypted? JWS vs JWE vs JWT Explained

No, a JWT is not encrypted by default. Almost every JWT you meet, including OAuth access tokens and OpenID Connect ID tokens, is a signed token (a JWS, RFC 7515). Its header and payload are Base64url-encoded, which is a reversible text encoding, not encryption, so anyone who holds the token can read every claim. The signature only stops anyone from changing the claims without detection.

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.

⚠
Because the payload is readable, never put passwords, API keys, full card numbers, health data or other secrets in a signed JWT. Assume anything in the payload will end up in a browser, a log file or a support ticket. See JWT security best practices.

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, exp and 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

AspectJWS (signed JWT)JWE (encrypted JWT)
SpecificationRFC 7515RFC 7516
ProtectsIntegrity and authenticityConfidentiality, plus integrity of the ciphertext
Payload readable by holderYes, it is only Base64url-encodedNo, only the recipient with the decryption key
Compact form3 parts: header.payload.signature5 parts: header.encryptedKey.iv.ciphertext.tag
Header algorithm fieldsalg (e.g. RS256, ES256, HS256)alg (key management) and enc (content encryption)
Who holds which keyIssuer signs with private key; anyone verifies with public keySender encrypts with recipient’s public key; only recipient decrypts
Proves who issued itYes (asymmetric algorithms)Not on its own; sign first, then encrypt (nested JWT)
Typical useAccess tokens, ID tokens, almost all JWTsTokens 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 alg is dir or plain ECDH-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 has alg values such as RS256 or ES256; a JWE header has both alg (for example RSA-OAEP-256) and enc (for example A256GCM).
  • ·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 separate keys for signing and encryption, even when both are RSA. A JWKS can publish them side by side, distinguished by 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)

algWhat it does
RSA-OAEP-256Random content key, encrypted to the recipient’s RSA public key with OAEP and SHA-256. A sound default.
RSA-OAEPAs above with SHA-1 in OAEP. Still widely supported; prefer the SHA-256 variant for new systems.
RSA1_5RSA PKCS#1 v1.5 key encryption. Prone to padding-oracle attacks; avoid (RFC 8725).
ECDH-ES / ECDH-ES+A256KWEphemeral elliptic-curve key agreement with the recipient’s EC public key; direct or with AES key wrap.
A128KW / A256KWContent key wrapped with a shared AES key.
dirNo key management: the shared symmetric key is used directly as the content key. The encrypted-key part is empty.

Content encryption (enc)

encWhat it does
A256GCM / A128GCMAES in Galois/Counter Mode. Authenticated, fast, the common choice.
A256CBC-HS512 / A128CBC-HS256AES-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'
ℹ
jwtdecode.app works with signed tokens only. It does not decrypt JWE, and it rejects five-part tokens rather than guessing at their contents. Decrypt a JWE in your own code with the recipient’s key, as above.

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 dir and A256CBC-HS512 by 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 example RSA-OAEP-256 or dir) with a content enc (for example A256GCM).
  • ·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.
Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder