Understanding JWT: structure, claims and safe verification
A JWT is three base64url-encoded segments joined by dots: header, payload, signature. Nothing in the header or payload is encrypted — it is only encoded, so anyone with the token can read its contents.
The three parts
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MDAwMDAwMDB9.signature- **Header** — the algorithm (`alg`) and token type. `{"alg":"HS256","typ":"JWT"}`.
- **Payload** — the claims: arbitrary key-value pairs plus a handful of registered ones.
- **Signature** — computed over the header and payload, proving they were not modified by anyone who does not hold the signing key.
Registered claims worth knowing
- `sub` — subject, usually a user ID.
- `iat` — issued-at, a Unix timestamp.
- `exp` — expiry, also Unix time. A token past this is invalid regardless of signature validity.
- `nbf` — not-before, the token is invalid until this time.
- `iss` — issuer, who created the token.
- `aud` — audience, who the token is intended for.
None of these are enforced by the format itself — they are conventions the verifying party must check explicitly.
Decoding is not verifying
Pasting a token into a decoder and reading the payload tells you what the token claims, not whether it is genuine. Verification requires:
1. Recomputing the signature with the same algorithm and the correct key. 2. Comparing it to the signature segment using a constant-time comparison. 3. Checking `exp`, `nbf`, and usually `iss`/`aud` against expected values.
Skipping step 1 and only checking the payload's claims is a bug that has bitten real production systems — an attacker can forge any payload they like as long as nobody validates the signature.
HS256 vs RS256
- **HS256** — HMAC-SHA256 with a single shared secret. Whoever can verify a token can also mint one, so the secret must stay server-side only.
- **RS256** — RSA signature with a private/public key pair. The private key signs, the public key verifies. This is the right choice when a third party needs to verify tokens without being trusted to issue them.
A well-known vulnerability class is accepting `alg: none` or letting an attacker switch `RS256` to `HS256` and sign with the (public) verification key as if it were an HMAC secret. Always pin the expected algorithm on the verifying side instead of trusting the `alg` field in the token.
Sizing and content
JWTs travel in headers and sometimes URLs, so keep the payload small — user ID, roles, expiry. Do not put anything you would not want logged in plaintext, since the payload is trivially decodable, and avoid putting large or frequently-changing data in there since the token has to be reissued whenever it changes.
A practical checklist
- Set a short `exp` (minutes to a few hours) and use a refresh mechanism for longer sessions.
- Validate `alg` server-side against an allow-list, never trust the header.
- Store secrets and private keys outside source control.
- Prefer RS256/ES256 when the token crosses a trust boundary you do not fully control.
- Never store JWTs containing sensitive claims in `localStorage` if XSS is a realistic risk — an httpOnly cookie is safer for session tokens.