Understanding JWT: structure, claims and safe verification

7 min read
jwt
security

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.

Tools from this article

← All articles