Stop Treating Your JWT Like a Database
The payload is base64, not ciphertext — and the fix isn't encryption, it's putting less in the token.
A security reminder is making the rounds again: JWT payloads are base64url-encoded, not encrypted. Anyone holding a token can read every claim inside it. This has been true since RFC 7519 was published in 2015, it's stated plainly in the jwt.io introduction, and yet every year a fresh cohort of developers discovers it the hard way — usually during a pentest, occasionally during an incident.
The fact itself isn't news. The interesting question is why this keeps happening a decade in, and why the standard advice — "don't put secrets in the payload" — keeps failing to stick.
The JWT became a free database, and that's the bug
The signature on a JWS token protects integrity: nobody can tamper with the claims without invalidating it. It does nothing for confidentiality. Decode one yourself:
python3 -c 'import sys,base64,json; p=sys.argv[1].split(".")[1]; \
print(json.dumps(json.loads(base64.urlsafe_b64decode(p + "="*(-len(p)%4))), indent=2))' "$TOKEN"
No key, no secret, one line of Python. Or paste it into any online debugger — which, incidentally, is its own leak vector when the token is from production.
Developers keep tripping over this because the JWT's killer feature is also its trap: it's a signed, stateless key-value store that every service in your architecture can read without a database call. Once that pattern is established, the token becomes the path of least resistance for any data a downstream service might want. Need the user's email for a notification service? Claim. Feature flags? Claim. Internal account tier, tenant ID, the legacy CRM identifier? Claims, claims, claims. Each addition is individually reasonable. Collectively you've built a public dossier that ships with every request.
And "public" is broader than most people model. Tokens don't just sit in an Authorization header between two TLS endpoints. They land in browser localStorage where any XSS payload or over-permissioned extension can read them. They get logged — by API gateways, by overeager debug middleware, by error trackers that capture request headers. They end up in URLs during sloppy OAuth flows, and from there into browser history, Referer headers, and CDN access logs. Every one of those surfaces can now read your claims. If those claims include personal data, your GDPR data-inventory problem just grew to include your logging pipeline.
The audit is thirty minutes; do it this week
The practical move here is boring and fast. Pull a real access token from each of your environments, decode it, and read it as an attacker would. For every claim, ask two questions: would I be comfortable printing this on the user's screen, and does any consumer actually validate or use it? Common offenders worth hunting for: email addresses and names (PII in every log line), internal database IDs that enable enumeration, role structures that map your org chart, and — it happens more than anyone admits — API keys or connection hints some integration stuffed in years ago.
Then grep your log aggregator for eyJ — that's how {" base64-encodes at the start of a JSON header, so it prefixes virtually every JWT. If tokens are landing in logs, everyone with log access holds live credentials plus whatever the payload discloses. That finding alone usually justifies the audit.
Encryption is the wrong fix for most teams
The reflexive answer is JWE — RFC 7516 defines encrypted tokens that actually hide the payload. In practice, reaching for JWE to solve this is usually a mistake. You inherit key distribution across every consuming service, a five-part token format your current libraries may handle badly, and a bigger algorithm-negotiation attack surface — the exact class of vulnerability (remember alg: none?) that made JWT libraries infamous. All of that to keep shipping data to the client that the client shouldn't hold in the first place.
There are two better moves, and they're not exclusive. First, minimize: a token needs a subject, issuer, audience, expiry, and maybe a scope. Almost everything else can be fetched server-side by the services that need it, keyed on sub. Yes, that reintroduces a lookup — which is the honest trade-off JWTs were sold as eliminating. A cached user-context read is cheap; a token you can't redact from the wild is not.
Second, if your services genuinely benefit from rich claims, keep the JWT internal. The phantom token pattern hands clients an opaque reference token — a random string that discloses nothing — and lets your gateway exchange it for a full JWT via introspection before forwarding requests inward. Microservices keep their stateless claim-reading; the public internet sees noise. Most API gateways can do this today with a plugin and an introspection endpoint, and it also solves revocation, the other chronic JWT complaint, since the opaque token can be killed server-side instantly.
Where this actually lands
Here's the uncomfortable read: the recurring "JWTs aren't encrypted" surprise isn't a knowledge gap, it's a design smell. Teams get surprised because their architecture quietly assumed the token was private storage, and no spec ever promised that. The fix isn't a smarter encoding — it's deciding, explicitly, that tokens are credentials, not couriers.
If you run JWTs in production, the near-term work is concrete: decode your tokens, delete every claim nobody validates, cut lifetimes to minutes rather than days, and grep your logs for eyJ. If you're designing something new, put opaque tokens at the edge and confine JWTs to your trust boundary. That architecture was niche five years ago; it's now the default in most serious OAuth deployments, because it makes the base64 question irrelevant. The payload can't leak what it never contained.
Sources & further reading
- Your JWT payload is public. Read it before an attacker does. — dev.to
- JSON Web Tokens Introduction — jwt.io
- RFC 7519 - JSON Web Token (JWT) — datatracker.ietf.org
- The Phantom Token Approach — curity.io
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.