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

The short answer

No. A standard JWT is base64url encoded and signed, not encrypted. Anyone who obtains 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 it wasn’t modified. 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’ll get {"role":"admin"} back. No key, no library, no effort. That’s base64url: a reversible re-spelling of bytes as URL-safe text, designed so tokens survive HTTP headers and query strings intact. 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 declaring the signing algorithm, a payload carrying the claims, and a signature. Decode the first two and you’re reading JSON in the clear. Our JWT decoder does it entirely in your browser, which is itself the demonstration: if a static web page can decode your token without talking to any server, so can anyone who finds that token in a log file.

What the signature actually protects

The signature is real cryptography doing a specific, narrow job. The issuer computes it over the header and payload using a key (a shared secret for HS256, a private key for RS256), and any service holding the corresponding verification key can confirm two properties: this token came from the expected issuer, and nobody altered a byte of it since.

Integrity and authenticity, not confidentiality. The analogy that sticks: 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 is still everything for stateless authentication. Change "role":"user" to "role":"admin" in transit and the signature check fails. The catastrophes happen when verification is skipped or fooled, most famously the alg: none family of attacks, where early libraries let the attacker declare that no signature was needed and then forged whatever they pleased. RFC 8725, the JWT best practices RFC, exists in large part 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 practical 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 from production incidents (use one that runs locally; ours never sends the token anywhere). They ride in URLs during sloppy OAuth flows and end up in browser history and analytics. They rest in localStorage, where any successful XSS reads them in one expression. Browser extensions with broad permissions see them all day.

None of these are exotic. Each one turns whatever you wrote into that payload into data you’ve 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, and they’re 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 direction, pragmatically: the token travels on every request, and a bloated payload taxes every API call you make.

When you actually need encryption

Sometimes the claim itself is sensitive and must 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, at the cost of real key management and far patchier library ergonomics than JWS.

In practice, most teams reach a simpler design first: keep the JWT as a pointer (subject ID, scopes, expiry) and keep 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 the ability to revoke 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. Check yours: paste any token of yours into the decoder and look at the expiry timeline. If the bar stretches past a day, that’s the first thing to fix, and 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 of the standard architecture. The client holds two credentials: a short-lived JWT access token that rides on every API call, and a long-lived refresh token that does exactly one thing, namely obtaining the next access token from the auth server. Users stay signed in for weeks; any individual 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 in mobile apps), never in a JWT payload, a URL or localStorage. Modern guidance adds rotation: each use of a refresh token invalidates it and issues a new one, and if the server ever sees a rotated-out token replayed, it revokes the whole chain because two parties are clearly holding the same credential. OAuth 2.1 folds these practices in by default.

Notice what reappeared: the auth server keeps state about refresh tokens, the revocation that pure stateless JWTs famously lack. That’s the honest tradeoff behind most production systems. Stateless verification where requests are hot and frequent, 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 is only involved in creating and checking the signature. The header and payload are base64url encoded, which is a reversible transformation any browser console performs in one line. Treat every claim in a JWT as public.

Is base64 a form of encryption?

No. Base64 is an encoding: a way to represent bytes as safe text, with no key and no secrecy. Decoding requires nothing but knowing it is base64. Encryption requires a key to reverse. Confusing the two 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 confidentiality is a requirement, JWS does not provide it, period.

Where should I store JWTs in a browser app?

The least bad common answer is an httpOnly, Secure, SameSite cookie, which JavaScript cannot read and XSS cannot exfiltrate directly. localStorage is one injected script away from token theft. Wherever you store them, keep lifetimes short so a stolen token expires before it is worth much.