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:

  1. The token was produced by someone holding the signing key.
  2. 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:

  1. Structure — exactly three segments? A two-segment token is not a JWT.
  2. exp / nbf — expired? Not yet valid?
  3. iss / aud — the issuer and audience must match what the verifier expects. Mismatched aud is a very common cause of "valid token rejected".
  4. 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.