Card 08/ 12
A JWT Is Signed, Not Encrypted
Anyone who holds a JWT can read every claim in it, because base64url is an encoding, not a lock.
A signed JWT protects its claims from being changed, not from being read. Its payload is base64url, which anyone can decode without a key, so every claim is visible to the user, to their browser's extensions, and to any log or proxy the token passes through. Nothing secret belongs in a signed JWT.
The symptom. A security review opens the browser's storage, pastes the token into a decoder, and reads the user's email address, their role and an internal account number.
InputeyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzgzNDEiLCJlbWFpbCI6Im1hcmlhQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzkxMzcwODAwfQ.kH3w9Qe1...
Split on the dots
- eyJhbGciOiJSUzI1NiJ9
- eyJzdWIiOiJ1c2VyXzgzNDEi...payload
- kH3w9Qe1...
Three parts; no key is needed for this step.
Decode the payload from base64url
- sub user_8341
- email [email protected]exposed
- role admin
Every claim is plain text, read by anyone holding the token.
Try to change role and send it back
- role edited to owner
- signature unchanged
- server refusesprotected
The signature no longer matches, so the edit fails. Reading succeeded; changing did not.
ResultThe signature stopped the token being altered, and did nothing to keep its contents private.
Why it happens. The token looks scrambled, and a signature sounds like security, so it is easy to assume both secrecy and integrity.
What it costs. Personal data and internal identifiers exposed wherever the token goes: browser storage, logs, analytics, a support screenshot.
What to do. Put only what the API needs to decide access in the token: a user id, scopes, the standard claims. Look anything else up on the server. If a claim truly must travel secretly, use an encrypted JWT, JWE, defined in RFC 7516, at the cost of every receiver needing the decryption key.
← → move through sections and cards