Security

Are JWTs encrypted? No, and the difference will bite you

On this page
  1. Encoded is not encrypted
  2. What the signature actually protects
  3. Where tokens leak, because they do
  4. What belongs in a payload
  5. When you actually need encryption
  6. How short lifetimes stay usable: the refresh token dance

Someone on your team pastes a production JWT into the group chat and half the room shrugs, because it's encrypted anyway, right? It isn't. A standard JWT is encoded and signed, not encrypted, so anyone holding it can read every claim with one line of code and no key at all. Base64url just makes a token look scrambled; it's an envelope anyone can open. What the signature does is different and genuinely useful: it proves who issued the token and that nobody altered it in transit. We'll get into what belongs in a payload and where tokens leak, plus the cases where you really do need encryption on top of the signature.

The short answer

No. A standard JWT is base64url encoded and signed, not encrypted. Anyone who gets hold of the token can read the header and payload with zero keys and one line of code. The signature proves who issued the token and that nobody modified it. It hides nothing.

2 of 3JWT parts readable by anyone
0keys needed to read a payload
JWEthe variant that actually encrypts
Answer card stating that JWTs are not encrypted and anyone can read them, since the signature proves origin and integrity rather than secrecy.
The one-card version, for the next architecture meeting. PNG

Encoded is not encrypted

Open your browser console and run atob('eyJyb2xlIjoiYWRtaW4ifQ'). You get {"role":"admin"} back. No key, no library. That’s base64url: a reversible re-spelling of bytes as URL-safe text so tokens survive HTTP headers and query strings. Encoding is about transport. Encryption is about secrets, and it’s the thing JWTs famously don’t do.

A JWT is three of these encoded segments joined by dots. A header that declares the signing algorithm, a payload that carries the claims. Then the signature. Decode the first two and you’re reading JSON in the clear. Our JWT decoder runs entirely in your browser, which is the demonstration: a static page can decode your token without talking to any server, and so can anyone who finds it in a log file.

What the signature actually protects

The signature is real cryptography doing one narrow job. The issuer computes it over the header and payload with a key (a shared secret for HS256, a private key for RS256). Any service holding the matching verification key can confirm two things: the token came from the expected issuer, and nobody altered a byte since.

Integrity and authenticity. Not confidentiality. A JWT is a postcard with a tamper-evident seal: the mailman can’t rewrite your message without breaking the seal, but he reads it whenever he likes.

That narrow job still carries all of stateless authentication. Change "role":"user" to "role":"admin" in transit and the signature check fails. The catastrophes happen when verification gets skipped or fooled, most famously the alg: none family of attacks, where early libraries let the attacker declare that no signature was needed, then forge whatever they pleased. RFC 8725, the JWT best practices RFC, exists largely because of that history. Pin your accepted algorithms server-side. Never let the token vote on its own verification.

Where tokens leak, because they do

Reading a payload requires having the token, so the real question is how often tokens escape. Routinely, it turns out. They sit in Authorization headers that land in proxy and gateway logs. They get pasted into online debuggers during production incidents (use one that runs locally; ours never sends the token anywhere). They ride along in URLs during sloppy OAuth flows and end up in browser history and analytics. localStorage hands them to any successful XSS in one expression, and browser extensions with broad permissions see them all day.

None of this is exotic. Whatever you wrote into that payload, treat it as published. Plan on it.

What belongs in a payload

Safe and standard: a user identifier, the issuer, the audience, expiry and issued-at timestamps, roles or scopes when coarse. These are the claims RFC 7519 registered: operational plumbing, not secrets.

Defensible with eyes open: an email address or display name for UI convenience. You’ve accepted that a leaked token leaks them; for many apps that’s a fair trade against a profile lookup per request.

Never, and this list comes from payloads I’ve actually seen decoded in incident reviews: passwords or their hashes, API keys for third-party services, national ID or payment data, internal hostnames and connection strings, medical anything. A JWT payload is a postcard. Write accordingly.

Size pushes the same way: the token rides on every request, so a bloated payload taxes every call.

When you actually need encryption

Sometimes the claim itself is sensitive and has to cross an untrusted hop. The standards answer is JWE, JSON Web Encryption, the JWT sibling that encrypts the payload to a recipient key so only that recipient reads it. It works. It also costs real key management, and library ergonomics are far patchier than JWS.

Most teams we’ve watched reach a simpler design first: keep the JWT as a pointer (a subject ID and its scopes, plus an expiry) and park the sensitive attributes in a server-side store the API consults. The token authenticates the lookup; the data never travels. You give back a little statelessness and get an envelope instead of a postcard, plus revocation by deleting a row.

Short expiries finish the job. A leaked token that died twenty minutes ago is an artifact; one with a 30 day lifetime is an incident. Paste any token of yours into the decoder and look at the expiry timeline. If the bar stretches past a day, fix that first. It costs one config line.

How short lifetimes stay usable: the refresh token dance

Fifteen-minute access tokens sound hostile to users until you add the second piece. The client holds two credentials: a short-lived JWT access token that rides on every API call, and a long-lived refresh token whose only job is getting the next access token from the auth server. Users stay signed in for weeks; any single stolen access token is worthless within minutes.

That design moves the risk into the refresh token, so treat it accordingly. It belongs in the most protected storage you have (an httpOnly cookie scoped to the token endpoint, or the platform keystore on mobile), never in a JWT payload or a URL, and not in localStorage either. Modern guidance adds rotation: each use of a refresh token invalidates it and issues a new one. If the server ever sees a rotated-out token replayed, it revokes the whole chain; two parties are clearly holding the same credential. OAuth 2.1 folds these practices in by default.

Notice what came back: the auth server keeps state about refresh tokens, the revocation that pure stateless JWTs famously lack. That’s the honest tradeoff in most production systems. Stateless verification where requests are hot, stateful control at the slower refresh boundary. The JWT isn’t the security model; it’s the cache layer of one.

Frequently asked questions

Can someone read my JWT without the secret key?

Yes, entirely. The secret key only comes into play when the signature gets created or checked. The header and payload are just base64url encoded, and any browser console reverses that in one line. Treat every claim in a JWT as public, because it is.

Is base64 a form of encryption?

No. Base64 is an encoding: a way to spell bytes as safe text. No key, no secrecy. Decoding it takes nothing but knowing it's base64, while encryption needs a key to reverse. Mixing those two up is the root of most JWT data leaks.

What is the difference between JWS and JWE?

Both are JWT formats. JWS (JSON Web Signature) is the common one: readable payload, signature for integrity. JWE (JSON Web Encryption) actually encrypts the payload, so only the holder of the right key can read it. If you need confidentiality, JWS doesn't give it to you, period.

Where should I store JWTs in a browser app?

The least bad common answer is an httpOnly, Secure, SameSite cookie. JavaScript can't read it, so XSS can't exfiltrate it directly. localStorage is one injected script away from token theft. Wherever they live, keep lifetimes short so a stolen token expires before it's worth much.