🍱 Lunchbox Hands

jwt

How JWTs Actually Work: Header, Payload, Signature & Common Mistakes

A developer-friendly explainer of JSON Web Tokens β€” what the three parts really are, what signing does and does NOT do, how validation works, and the security mistakes that cause real breaches.

JWTs are everywhere in modern auth, and almost everywhere slightly misunderstood. The two biggest myths: that they’re encrypted (they’re not), and that having a valid signature means the token is safe to trust blindly (it isn’t). Let’s open one up and see what’s really going on.

A JWT is three Base64url strings joined by dots

A JSON Web Token looks like one long opaque string, but it’s three parts separated by .:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3...Qssw5c.SflKxwRJ...
└────────── header β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ └────────── payload β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ └─ signature β”€β”˜

Each part is Base64url-encoded (not encrypted). Decode the first two and you get plain JSON.

1. Header β€” how it’s signed

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

alg is the signing algorithm; typ is the token type.

2. Payload β€” the claims

{
  "sub": "1234567890",
  "name": "Ada Lovelace",
  "role": "admin",
  "iat": 1735689600,
  "exp": 1735693200
}

These are claims β€” statements about the user/session. Standard registered claims include:

ClaimMeaning
issIssuer (who made the token)
subSubject (usually the user ID)
audAudience (who it’s intended for)
expExpiration time (Unix seconds)
nbfNot valid before
iatIssued at
jtiUnique token ID

3. Signature β€” the tamper seal

The signature is computed over the encoded header and payload:

HMAC-SHA256( base64url(header) + "." + base64url(payload), secret )

The single most important fact: a JWT is signed, not encrypted

Anyone holding the token can Base64url-decode the payload and read every claim β€” role, email, everything. The signature doesn’t hide the data; it only proves the data hasn’t been changed since the issuer signed it.

So two rules follow immediately:

  • Never put secrets in a JWT payload (passwords, API keys, PII you don’t want exposed). Treat it as public, readable JSON.
  • The value of the signature is integrity, not confidentiality. If you need the contents hidden, you need encryption (JWE) or just don’t put it in the token.

How signing actually works

There are two families of algorithms:

  • HMAC (HS256/384/512) β€” symmetric. The same secret signs and verifies. Simple, fast, but every service that verifies must hold the shared secret.
  • RSA / ECDSA (RS256, ES256) β€” asymmetric. A private key signs; a public key verifies. Your auth server keeps the private key; any number of services can verify with the public key without being able to mint tokens. This is what you want for distributed systems and third-party verification.

Verification recomputes the signature over the received header+payload and checks it matches. If even one byte of the payload was altered, the signatures won’t match and the token is rejected.

Validating a token β€” the full checklist

A correct verifier does all of these, not just the signature:

  1. Signature is valid for the expected algorithm and key.
  2. exp is in the future (the token hasn’t expired).
  3. nbf (if present) is in the past.
  4. iss matches the issuer you trust.
  5. aud matches your service.
  6. The alg is one you explicitly allow.

Skipping any of these is how tokens get misused.

The security mistakes that cause real breaches

1. The alg: none attack. The JWT spec allows an β€œunsigned” token with alg: none. Naive libraries once accepted these β€” an attacker just sets alg to none, strips the signature, and forges any payload. Always pin the expected algorithm on the verification side; never trust the alg value from the incoming header.

2. Algorithm confusion (RS256 β†’ HS256). If your verifier is told β€œthe key is X” but lets the token pick the algorithm, an attacker can take your public RSA key, sign a token with it using HMAC, and your server β€” using that public key as the HMAC secret β€” validates it. Fix: hard-code which algorithm goes with which key type.

3. Not checking expiration. A signed token with no exp check is a permanent credential. Always set exp, and always verify it.

4. Storing JWTs in localStorage. localStorage is readable by any JavaScript on the page, so an XSS bug hands your tokens to the attacker. Prefer HttpOnly, Secure, SameSite cookies for session tokens so scripts can’t read them.

5. Treating JWTs as revocable. A signed token is valid until it expires β€” you can’t β€œlog it out” server-side without extra machinery (a denylist, short lifetimes + refresh tokens, or rotating keys). Keep access-token lifetimes short.

When to actually use one

JWTs shine for stateless, cross-service authorization: an API gateway or microservice can verify a token with a public key and trust the claims without a database round-trip. For a classic single-server web app with a session, a plain server-side session cookie is often simpler and gives you easy revocation. Pick the tool for the topology.

Inspect and experiment

The fastest way to internalize all of this is to take a token apart:

  • Paste any token into the JWT Decoder to see the header, payload, and claims in plain JSON β€” and confirm for yourself that the payload is readable without any secret.
  • Mint test tokens with chosen claims and algorithms in the JWT Generator to see how the signature changes when the payload does.

Both run entirely in your browser β€” no token data leaves your machine.