{ }formatter Nothing leaves your browser

What's inside a JWT

A JSON Web Token is three base64url strings joined by dots. That's the whole format — and once you can read it by eye, most JWT confusion evaporates. This guide walks through each segment, the claims that actually matter, and the security mistakes that keep showing up in production systems.

Three segments, two dots

Every JWT looks like header.payload.signature. Here's a complete, real one signed with HMAC-SHA256 and the secret secret:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMn0.Ykje5O2H3Zn7ASrxlqG3DnVL7cqRElA4DfBlepgKLYo

The first two segments are just base64url-encoded JSON. Decode segment one and you get:

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

Decode segment two and you get the claims:

{"sub":"1234567890","name":"Ada Lovelace","iat":1516239022}

The third segment is the signature — raw bytes, also base64url — computed over the first two.

The payload is readable, not secret

Because the payload is only encoded, anyone holding the token can read every claim inside it. No key required. Paste any token into a decoder and the contents are right there.

What the signature gives you is integrity and authenticity: proof the token was issued by someone holding the key and hasn't been modified since. It gives you nothing resembling confidentiality. So never put anything in a JWT you wouldn't hand to the client directly — no passwords, no internal secrets, no personal data you're not comfortable exposing. (There is a separate standard, JWE, for genuinely encrypted tokens; a plain "JWT" in casual usage means the signed JWS form described here.)

How the signature is computed

For HS256 the recipe is exactly:

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

The signing input is the first two segments as they appear in the token, dot included. That detail matters: a verifier must sign the received bytes, not a re-serialization of the parsed JSON, because key order and whitespace would differ and the signature would never match.

Algorithms fall into two families, and the difference drives most of the security discussion below:

The claims worth knowing

The spec registers a handful of short claim names. Everything else is yours to define.

All three time claims are seconds since the Unix epoch, not milliseconds. Passing a JavaScript Date.now() value straight into exp is a classic bug: it produces a timestamp roughly fifty thousand years out, and the token effectively never expires. If a token's expiry looks absurd, check the units with a timestamp converter.

The security mistakes that keep recurring

1. Trusting the alg header

The header is attacker-controlled — it arrives with the token. Two well-known attacks exploit a verifier that believes it:

The none algorithm. The spec includes an "unsecured JWT" with alg: "none" and an empty signature. A library that honors it will accept a token with any payload and no signature at all. An attacker rewrites the claims, sets alg to none, drops the signature, and walks in as anyone.

Algorithm confusion (RS256 → HS256). If a server verifies with a generic "verify with this key" call, an attacker can take the server's public RSA key — which is public by design — flip alg to HS256, and sign the token using that public key as the HMAC secret. A naive verifier then HMACs with the same public key, the signatures match, and the forgery is accepted.

The fix for both is the same and it's not subtle: decide the acceptable algorithm(s) server-side and pass them explicitly to the verifier. Never let the token's own header choose how it gets checked.

2. Decoding instead of verifying

Most libraries offer both a decode and a verify function. decode just base64url-decodes the payload — it performs no cryptography and trusts nothing. Using it to read sub or a role claim and then acting on that value is equivalent to having no authentication. Debugging tools decode; application code must verify.

3. Weak HMAC secrets

An HS256 token is offline-brute-forceable: an attacker with one token can test candidate secrets locally, as fast as their hardware allows, with no rate limit. Dictionary words and short strings fall in seconds. Use a long, random secret — at least 32 bytes from a CSPRNG — and treat it like the credential it is.

4. Forgetting that JWTs can't be un-issued

This is the architectural trade-off people discover late. A signed token is valid until it expires, because verification is purely local — there's no lookup to consult. Log the user out, change their password, revoke their role: the token in their hand still verifies.

The practical answers are to keep access tokens short-lived (minutes, not weeks) and pair them with a refresh token you can revoke server-side, or to maintain a denylist of jti values — which reintroduces the shared state JWTs were meant to avoid. Choose deliberately: if you need instant revocation and you already have a database on the request path, a plain session may serve you better.

5. Skipping aud and iss

If several services share a signing key, a token minted for the low-privilege service verifies perfectly at the high-privilege one. Checking aud and iss is what stops a valid token being replayed somewhere it was never meant to go.

6. Storing tokens carelessly in browsers

localStorage is readable by any JavaScript on the page, so a single XSS flaw hands over the token. An HttpOnly, Secure, SameSite cookie keeps it out of reach of scripts — at the cost of needing CSRF protection. Neither option is free; the mistake is not making the choice consciously.

A short verification checklist

Inspect a token safely

When you're debugging, decoding a token to see its claims is exactly the right move — just be careful where you paste it, since a live token is a credential. The JWT decoder on this site runs entirely in your browser: it splits the segments, pretty-prints the header and payload, flags exp/nbf status in plain language, and can verify an HS256 signature locally if you supply the secret. Nothing is transmitted anywhere.