What Is a JWT? Structure, Signing, and Debugging
Published 2026-10-31
A JSON Web Token (RFC 7519) is three Base64URL strings joined by dots:
header.payload.signature
That is the whole format. Everything else — expiry, roles, refresh flows — is convention layered on top.
The three segments
// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{ "sub": "user_1024", "role": "admin", "exp": 1767225600, "iat": 1767222000 }
// signature
HMACSHA256(base64url(header) + "." + base64url(payload), secret)
| Segment | Contains | Readable by anyone? |
|---|---|---|
| Header | Algorithm and token type | Yes |
| Payload | Claims: subject, roles, expiry | Yes |
| Signature | Proof the first two were not altered | Not reversible, but not secret either |
The single most important fact: a JWT is signed, not encrypted. Pasting one into any decoder shows the full payload. Never put a password, a national ID number, or an internal API key in the claims.
This is the same confusion as thinking Base64 is encryption — the encoding makes it look opaque, but nothing is hidden.
What the signature proves
Exactly two things:
- The token was produced by someone holding the signing key.
- The header and payload have not changed since signing.
It does not prove the token is still valid (that is exp), nor that the user still has the permissions listed (roles can be revoked server-side after issuance). This gap is why short lifetimes matter: a JWT is a snapshot, and the longer it lives the staler it gets.
Signing algorithms
| Algorithm | Key type | Use when |
|---|---|---|
| HS256 | Shared secret | A single backend signs and verifies |
| RS256 | RSA private/public | Multiple services verify, one signs |
| ES256 | ECDSA | Same as RS256, smaller tokens |
The classic vulnerability is the algorithm confusion attack: if the server accepts whatever alg the token declares, an attacker changes it to none (or switches RS256 to HS256 and signs with the public key), and verification passes.
The fix is one line: the server fixes the algorithm; it never reads it from the token.
Three mistakes that cause most auth bugs
1. Trusting the payload before verifying the signature
If your middleware decodes and reads role before checking the signature, anyone can mint an admin token with a text editor. Decode is not verify.
2. Clock skew breaking exp checks
exp is Unix seconds. A verifier whose clock is 30 seconds ahead rejects every fresh token. Most libraries take a clockTolerance option — set it to 30–60 seconds rather than disabling the check.
Also: exp is in seconds. Producing a token with milliseconds gives an expiry roughly 50,000 years in the future. This is a real bug that ships to production regularly.
3. Huge tokens in every request
A JWT is sent on every request. Put a full permission list in the claims and you get an 8 KB header on every call, which large gateways reject (8 KB is a common limit). Keep claims small; look up the rest server-side.
Storage: where to put the token
| Storage | XSS risk | CSRF risk | Notes |
|---|---|---|---|
localStorage |
High — any script can read it | None | Popular and convenient, and the usual source of token theft |
sessionStorage |
High | None | Same, cleared on tab close |
| HttpOnly cookie | None | Needs SameSite + CSRF token |
The safer default for browser apps |
| Memory only | Low | None | Lost on refresh; needs silent refresh flow |
There is no free option. The pragmatic default for browser apps is an HttpOnly, Secure, SameSite=Lax cookie for the refresh token, and a short-lived access token in memory.
Debugging a token
When auth fails, check four things in order:
- Structure — exactly three segments? A two-segment token is not a JWT.
exp/nbf— expired? Not yet valid?iss/aud— the issuer and audience must match what the verifier expects. Mismatchedaudis a very common cause of "valid token rejected".- Signature — verify with the right key and the right algorithm.
You can decode all of this locally with JWT Decoder — it splits the segments, shows the timestamps as human-readable dates, flags expiry, and can verify an HS256 signature when you supply the secret. Nothing is uploaded, which matters because a production token is a live credential.
Related: Password Hashing explains why the same trick does not apply to storing passwords, and URL Encoding covers why JWTs use the URL-safe Base64 alphabet.