JWT Basics — Structure and Encoding
A JWT is three base64url-encoded segments joined by dots: header.payload.signature. Each segment is a JSON object (the signature is binary). Compact, URL-safe, self-describing — but not encrypted.
The anatomy
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWxpY2UiLCJleHAiOjE3MTYzMjAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└──────── header ──────────┘ └────────────── payload ──────────────┘ └────────── signature ──────────┘
Decode the first two segments at jwt.io — they’re plain JSON. The payload is not secret. Anyone with the token reads everything inside.
Header
{ "alg": "HS256", "typ": "JWT", "kid": "abc123" }
| Field | Means |
|---|---|
alg |
signing algorithm (HS256, RS256, ES256, etc. — see 02_signing_algorithms.md) |
typ |
typically “JWT” |
kid |
key ID — which key in your JWKS signed it (for rotation) |
cty |
content type, rare |
Payload (claims)
{
"sub": "user_42",
"name": "Alice",
"iat": 1716316400,
"exp": 1716320000,
"aud": "myapp",
"iss": "https://auth.example.com"
}
Whatever JSON object you want. Standard claims (registered: sub, iat, exp, aud, iss, nbf, jti) plus your own. See 03_claims.md.
Signature
HMACSHA256(
base64url(header) + "." + base64url(payload),
secret
)
For HS256 (symmetric). For RS256 (asymmetric), it’s RSA-PKCS1-v1_5 with SHA-256 using the private key; verified with the public key.
Critical: the signature signs header + "." + payload. It guarantees:
- Whoever signed had the secret/private key.
- Neither header nor payload was modified after signing.
It does NOT guarantee:
- The content is private (it’s base64-encoded, not encrypted).
- The token hasn’t been stolen.
- The user is still allowed to do whatever (auth state may have changed since issuance).
Base64url — not regular base64
Regular base64: + / =
Base64url: - _ (no padding)
URL-safe variant: + and / (which conflict with URLs) become - and _. No padding = characters (they’re optional). Same data, friendlier encoding.
A complete example in Python
import jwt
import datetime
# Encode
secret = "supersecretkey"
token = jwt.encode(
{
"sub": "user_42",
"name": "Alice",
"iat": datetime.datetime.now(datetime.timezone.utc),
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1),
},
secret,
algorithm="HS256",
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Decode
payload = jwt.decode(token, secret, algorithms=["HS256"])
print(payload)
# {'sub': 'user_42', 'name': 'Alice', 'iat': ..., 'exp': ...}
Always pass algorithms=[...] — never rely on the token’s own alg field. See 02_signing_algorithms.md.
How JWT is used (typical flow)
1. Client logs in: POST /login {user, pass}
2. Server validates: DB lookup, password check
3. Server issues JWT: signs {sub, exp, ...} → returns token
4. Client stores token: cookie / memory / secure storage
5. Client sends token: Authorization: Bearer <token> on every request
6. Server verifies: check signature, check exp, extract sub → know who the user is
7. Server authorizes: decide if sub can do the requested operation
Step 6 is local — no DB lookup needed to validate the signature. That’s the JWT pitch: stateless servers, horizontal scaling.
See 07_jwt_vs_session.md for the comparison.
Where the token lives
| Storage | Pros | Cons |
|---|---|---|
localStorage (browser JS) |
simple | XSS reads it — leaks the session |
sessionStorage |
tab-scoped | same XSS risk |
| HttpOnly cookie | JS can’t access (XSS-safe) | needs CSRF defense |
| In-memory (SPA) | XSS-safe (lost on reload) | re-auth on every reload |
| Mobile secure storage (Keychain/Keystore) | best for native | only mobile |
Modern web app recommendation: refresh token in HttpOnly Secure cookie, access token in memory. See 04_access_refresh_tokens.md.
What JWT does well
- Stateless backends: any replica can verify without a DB lookup.
- Microservices: pass the JWT downstream; each service verifies independently.
- Federated identity: SSO (OIDC ID tokens are JWTs).
- Self-contained claims: roles / tenant / scopes travel with the token.
What JWT doesn’t do well
- Revocation: stateless = can’t easily kick someone out before
exp. Workarounds (blocklist, short tokens) lose statelessness. - Privacy: claims are readable. PII leaks.
- Long-lived sessions: long-expiry JWT = long exposure window after theft.
- Mid-session permission changes: token still has old roles until expiry.
For these cases, server-side sessions (opaque session ID + Redis) often beat JWT. See 07_jwt_vs_session.md.
Common pitfalls (preview)
alg: noneattack — token claims no signature; library accepts.- Algorithm confusion (RS256 ↔ HS256).
- Missing
aud/issvalidation. - PII in payload.
- No expiry (
exp) or excessive lifetime. - Storing in localStorage with cookie auth.
Full coverage: ../../25_security/06_jwt_pitfalls.md.
Common interview confusions
- “JWTs are encrypted.” — signed, not encrypted. Anyone can read the payload. Use JWE if you need encryption. See 08_jose_family.md.
- “JWTs are inherently more secure than sessions.” — they’re stateless, which is a different property. Sessions with Redis can be equally secure and easier to revoke.
- “JWTs solve all auth problems.” — they solve stateless auth specifically. They make revocation, mid-session updates, and privacy harder.
Interview angle
- “What is a JWT?” — JSON Web Token: three base64url segments (header.payload.signature). Header declares the signing algorithm. Payload contains claims (user ID, expiry, etc.). Signature proves the token wasn’t tampered with. Self-contained — verifying servers don’t need to look up state.
- “What does each part contain?” — header:
algandtyp. Payload: claims (registered likesub,exp,iss,aud; custom). Signature: HMAC or RSA/ECDSA over the encoded header + payload. - “Is a JWT encrypted?” — no, it’s signed. Anyone with the token reads the payload. For encryption, use JWE (rarely seen).
- “What’s the typical JWT flow?” — client logs in → server issues signed JWT → client sends
Authorization: Bearer <jwt>on every request → server verifies signature + claims → extracts user identity → authorizes. - “Where should the client store the JWT?” — for web: refresh token in HttpOnly Secure cookie, access token in memory. localStorage is XSS-vulnerable. For mobile: Keychain / Keystore.
- “Why is JWT compact and URL-safe?” — base64url encoding (no
+,/,=); fits in URL params, headers, cookies. Small enough to ride along on every request without bloat.