JWT Tokens Explained Simply
A JWT is three Base64URL chunks separated by dots. That's it. Here's what each chunk means, how signing keeps them honest, and the mistakes that keep landing in security audits.
The three parts
A JSON Web Token looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIiwiZXhwIjoxNzM1Njg5NjAwfQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three Base64URL-encoded strings separated by dots: header, payload, signature.
Header
Tells the verifier which algorithm to use.
{ "alg": "HS256", "typ": "JWT" }
Payload (claims)
JSON object of claims. Some are standardized:
iss— issuersub— subject (usually the user ID)aud— audience (which service the token is for)exp— expiration time (Unix seconds)nbf— not-before timeiat— issued atjti— unique token ID (for revocation)
Custom claims live alongside these. Keep the payload small — every request carries it.
Signature
The signature is computed over base64url(header) + "." + base64url(payload) using the algorithm declared in the header. With HS256, that's an HMAC-SHA256 with a shared secret. With RS256, it's RSA-SHA256 with a private key.
How verification actually works
- Split the token on
.into three parts. - Base64URL-decode the header and read
alg. - Recompute the signature over
header.payloadusing your key. - Compare against the provided signature in constant time.
- If it matches, validate the claims:
expin the future,nbfin the past,iss/audmatch what you expect.
Expiration: exp, nbf, iat
All three are Unix timestamps in seconds, not milliseconds. A 10-minute token issued now has exp = Math.floor(Date.now()/1000) + 600. Clock skew is the most common source of "token not yet valid" errors — allow a few seconds of leeway on both ends.
Security mistakes that keep happening
1. Accepting alg: none
Some early libraries honored a header of {"alg":"none"} and skipped signature verification entirely. An attacker could forge any payload. Hardcode the expected algorithm on the server — never derive it from the incoming header.
2. HS256 / RS256 confusion
If your server accepts both and uses the RSA public key as the HMAC secret, an attacker can sign tokens with HS256 using that public key. Pin one algorithm per endpoint.
3. Storing tokens in localStorage
Any XSS on your origin reads localStorage instantly. Prefer an httpOnly, Secure, SameSite=Lax cookie. If you must use localStorage (e.g. for a single-page app calling a different origin), keep token lifetimes short.
4. Putting secrets in the payload
The payload is readable by anyone. Don't include PII you wouldn't expose in an API response. Don't put database connection strings or feature flags an attacker shouldn't see.
5. No revocation strategy
JWTs are stateless. Once issued, they're valid until exp. Plan for the day you need to revoke one: short-lived access tokens (5–15 min) plus a refresh token with a server-side allowlist is the common pattern.
When JWTs are the wrong choice
JWTs shine for service-to-service auth, OAuth/OIDC flows, and stateless APIs where the issuer and verifier are different parties. For a classic single-server web app with a session, an opaque session cookie is simpler, smaller, and trivially revocable.
exp vs current time, aud against your service ID, and alg against your expected algorithm.FAQ
Is a JWT encrypted?
Where should I store a JWT in the browser?
httpOnly, Secure, SameSite cookie. localStorage exposes the token to any XSS on your origin.What does exp mean in a JWT?
exp is the expiration time as a Unix timestamp in seconds. Servers reject tokens whose exp is in the past (allowing for a few seconds of clock skew).