Authentication & Authorization
03 / 04

JWT & Token Security

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 cookie

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free