Authentication: JWT & Token Security
JWT Structure
JWT = base64url(header) . base64url(payload) . signature
Header: { "alg": "RS256", "typ": "JWT" }
Payload: { "sub": "user123", "email": "alice@example.com",
"roles": ["admin"], "iat": 1716768000, "exp": 1716854400 }
Signature: RSA_SHA256(secret, header + "." + payload)
Standard claims (IANA registered):
sub — subject (user ID)
iss — issuer (who created the token, e.g. "https://auth.example.com")
aud — audience (intended recipient, e.g. "api.example.com")
exp — expiration timestamp (Unix seconds)
iat — issued at timestamp
jti — JWT ID (unique identifier — use for revocation blocklist)
nbf — not before (token not valid until this time)
WARNING: JWT payload is base64-encoded, NOT encrypted.
Anyone can decode it. Never put passwords, card numbers, or
secrets in the payload. Use JWE (JSON Web Encryption) if needed.Signing Algorithms
HS256 (HMAC-SHA256) — symmetric: same secret signs and verifies
Pros: simple, fast
Cons: every service that verifies must have the secret → secret sprawl
RS256 (RSA-SHA256) — asymmetric: private key signs, public key verifies
Pros: publish public key → any service can verify without knowing the private key
Cons: slower than HS256
ES256 (ECDSA P-256) — asymmetric like RS256 but much smaller keys and faster
Recommended for new systems
EdDSA (Ed25519) — most modern, fastest, smallest signatures
Recommendation:
Internal (single service) → HS256
Distributed / microservices → RS256 or ES256
Never use "none" algorithm — it disables signature verification!Access + Refresh Token Pattern
Access token: Short-lived (15-60 min). Sent with every API request.
Stateless — validated by signature. No DB lookup per request.
Refresh token: Long-lived (7-30 days). Stored in HttpOnly cookie.
Used ONLY to get new access tokens. Stored as hash in DB.
Flow:
POST /auth/login
→ {accessToken, refreshToken (in HttpOnly cookie)}
API calls: Authorization: Bearer <accessToken>
When access token expires (401):
POST /auth/refresh (refresh token sent automatically via cookie)
→ new accessToken + rotated refreshToken (old one invalidated)
POST /auth/logout
→ delete refresh token from DB; client clears access token
Refresh token rotation: issue a new refresh token with every refresh.
If an old refresh token is ever used → revoke the entire family (detect theft).
Revocation: maintain a blocklist in Redis with jti + exp.
Check blocklist on critical operations (password reset, account deletion).Token Storage in Browsers
localStorage / sessionStorage:
❌ XSS vulnerable — any injected script can read window.localStorage
❌ Do not store long-lived tokens here
In-memory (JS variable):
✅ Not accessible to other tabs or XSS from different origins
❌ Lost on page refresh
✅ Good for short-lived access tokens
HttpOnly Cookie (recommended for refresh tokens):
✅ Inaccessible to JavaScript
✅ Sent automatically
❌ CSRF risk → mitigate with SameSite=Strict or CSRF token
Recommended setup:
Access token → in-memory JS variable (short-lived, refreshed silently)
Refresh token → HttpOnly Secure SameSite=Strict cookieKeep your own version of these notes — editable, searchable, and organised by your stack.
Start free