What is actually inside a JWT — and what you should never put in one

A JWT is three Base64URL segments, and the payload is readable by anyone holding the token. Here is what each part contains, what the signature does and does not prove, and the alg:none attack that follows from getting it wrong.

Published · 8 min read

A JSON Web Token looks like an opaque credential — a long random-looking string in an Authorization header. It is not opaque at all. It is three chunks of Base64URL separated by dots, and anyone who has the token can read every claim inside it without any key at all.

That single fact explains most of the mistakes people make with JWTs.

The three parts

header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFkYSIsImV4cCI6MTc2NzIyNTYwMH0
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The first two are Base64URL-encoded JSON. Base64 is an encoding, not encryption — it exists to make binary data survive text channels, and decoding it requires nothing but a decoder. The third is a signature over the first two.

Header

{ "alg": "HS256", "typ": "JWT" }

It declares which algorithm signed the token. Note carefully what that means: the token itself is telling the server how to verify it. That is the source of the most famous JWT vulnerability, below.

Payload

{
  "sub": "1234",
  "name": "Ada",
  "role": "admin",
  "iat": 1767139200,
  "exp": 1767225600
}

The claims. Some names are registered by the specification and have defined meanings — sub for the subject, iss for the issuer, aud for the intended audience, exp for expiry, iat for issued-at, nbf for not-valid-before, jti for a unique token id. Everything else is yours to define.

This part is readable by anyone holding the token: the user, any browser extension with access to it, anything that logged the request. Paste any JWT into a decoder and you will see its full contents. Never put anything confidential in a payload.

Signature

A MAC or digital signature over the encoded header and payload. It proves the token was issued by something holding the key and has not been altered since. It does not encrypt anything and does not hide anything.

The alg:none attack

The specification includes an algorithm called none, meaning unsigned. It exists for cases where integrity is guaranteed by another layer.

The attack follows directly: take a valid token, edit the payload to say you are an administrator, change the header algorithm to none, drop the signature, and send it. A server that reads the algorithm from the token and does what it says will accept it. Many libraries did exactly that, and a wave of authentication bypasses followed.

The related attack is algorithm confusion. A server expecting RS256 — asymmetric, verified with a public key — receives a token declaring HS256, symmetric. A naive implementation then verifies the HMAC using the RSA public key as the shared secret. The public key is public, so the attacker can forge tokens at will.

The fix for both is the same and it is one line: pin the expected algorithm in the verification call rather than reading it from the token. Any modern library supports this and most now require it.

What the signature does not tell you

A valid signature means the token was issued by the holder of the key. It says nothing about whether the token should still be honoured, and every one of these checks has to be made separately.

  • exp — has it expired? Verification libraries usually check this, but not all do by default.
  • nbf — is it valid yet?
  • iss — did it come from the issuer you expect, rather than another tenant or another environment?
  • aud — was it meant for this service? A token issued for your public API should not be accepted by your admin API.
  • Revocation — has it been logged out or invalidated? Nothing in the token can tell you this.

The revocation problem

This is the structural weakness of stateless tokens and it is worth being clear-eyed about. A JWT is valid until it expires, because validity is a property of the token itself rather than of a server-side session.

So when a user logs out, or an administrator revokes access, or a token is stolen, there is nothing to delete. The token keeps working until exp passes. The mitigations all reintroduce state in some form: short expiry with refresh tokens, a denylist of revoked jti values, or a per-user token version checked on each request.

That last point deserves emphasis, because it undercuts the usual argument for JWTs. If you need immediate revocation, you need a lookup on every request — at which point a session identifier in a cookie is simpler, smaller, and revocable by definition.

Where to store a JWT in a browser

There is no option without trade-offs, and anyone who tells you otherwise is selling something.

  • Local storage — readable by any JavaScript on the page. One cross-site scripting flaw, or one compromised dependency, and the token is exfiltrated. Convenient and the most commonly exploited.
  • A httpOnly cookie — unreadable from JavaScript, so XSS cannot steal it directly, but sent automatically with requests, which means CSRF protection is now your responsibility. SameSite=Lax or Strict handles most of it.
  • In memory only — safest, lost on refresh, so it needs a refresh token stored somewhere, which returns you to the question.

The usual recommendation is a httpOnly, Secure, SameSite cookie with CSRF protection. It moves the risk to an attack class that is easier to defend systematically.

When a JWT is the wrong tool

JWTs earn their complexity in one situation: when the party validating the token is not the party that issued it. Cross-service authentication, federated identity, and third-party API access all fit, and being able to verify without a call back to the issuer is genuinely valuable.

For a single application with its own users, a session cookie backed by server-side state is smaller, revocable, and has none of the failure modes above. Reaching for JWTs there is a common case of adopting a distributed-systems solution for a problem that is not distributed.

Frequently asked questions

Is the payload encrypted?
No. It is Base64URL-encoded, which is trivially reversible. Anyone with the token can read every claim. If you need confidentiality, JWE encrypts the payload — but the simpler answer is to keep sensitive data out of the token.
Why does a decoder not ask for my secret?
Because decoding needs no key — only verification does. Reading the claims and checking the signature are separate operations, and a tool that asks you to paste a production signing secret into a web page is asking for something you should never give it.
How long should a token live?
Short, because expiry is your only automatic revocation. Fifteen minutes to an hour for an access token, with a longer-lived refresh token that can be revoked server-side, is the common pattern.
Can I change a claim without the secret?
You can change it, and the signature will then fail verification on any correctly implemented server. The alg:none and algorithm-confusion attacks above are precisely the cases where a server fails to implement it correctly.
What is the difference between JWT, JWS, and JWE?
JWS is a signed token — integrity and authenticity, contents readable. JWE is an encrypted token — contents hidden. JWT is the claims format used inside either. Almost everything called a JWT in practice is a JWS.