Signed, encrypted, or both

JWT Authentication Course · lesson 13 of 13 · 5 min read

A JWS proves who wrote it; anyone can read it.

Open this lesson in the learning hub

Key points

  • A normal JWT is a JWS - signed, not encrypted. The payload is base64url, which is encoding rather than protection: anyone holding the token can read every claim.
  • That is the single most common misconception about JWTs, and it has real consequences - putting an email, a role list or an internal id in a token publishes it to the client and to anything that logs the header.
  • A JWE encrypts the payload so only the intended recipient can read it. It costs key management on both sides and makes debugging considerably harder.
  • Nested JWT - sign then encrypt - gives both authenticity and confidentiality. It is used where claims are genuinely sensitive and the token crosses untrusted intermediaries.
  • The usual answer is neither: keep sensitive data out of the token. Put an opaque subject in it and look the rest up server-side.
  • Remember tokens end up in access logs, browser history via query strings, and error reports. Assume anything in a JWT will eventually be read by someone you did not intend.

Example

# A JWS is READABLE by anyone. Decode it yourself:
$ echo 'eyJzdWIiOiIxMjMiLCJlbWFpbCI6ImFAYi5jb20ifQ' | base64 -d
{"sub":"123","email":"a@b.com"}
#   No key needed. The signature stops MODIFICATION, not READING.

# STRUCTURE:
#   JWS  header.payload.signature                    3 parts, readable
#   JWE  header.key.iv.ciphertext.tag                5 parts, opaque

---
# WHAT NOT TO PUT IN A TOKEN:
#
#   email, phone, name          PII, and it leaks into every log
#   internal database ids       tells an attacker your schema
#   permission lists            large, and stale the moment they change
#   anything you would not      because you are effectively publishing it
#     paste into a public gist

# WHAT A GOOD TOKEN LOOKS LIKE:
{
  "iss": "https://auth.example.com",
  "sub": "a1b2c3d4-opaque-user-id",
  "aud": "orders-api",
  "exp": 1754251200,
  "iat": 1754250300,
  "jti": "unique-token-id",
  "scope": "orders.read orders.write"
}
#   Small, opaque, and nothing here is a secret.

---
# WHEN A JWE IS ACTUALLY WARRANTED:
#   - the token crosses an intermediary you do not control
#   - a regulation requires the claims be unreadable in transit AND at rest
#   - the claims themselves are the sensitive data and cannot be looked up
#
# Otherwise: shrink the token, look the rest up, and keep debugging easy.

A signed JWT is readable by anyone holding it - the fix is almost never encryption, it is not putting secrets in the token.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JWT Authentication Course course, and every lesson in it is listed on the JWT Authentication Course contents page.