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:
- Recomputing the signature with the same algorithm and the correct key.
- Comparing it to the signature segment using a constant-time comparison.
- Checking
exp,nbf, and usuallyiss/audagainst 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
algserver-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
localStorageif XSS is a realistic risk — an httpOnly cookie is safer for session tokens.