Skip to main content

JWT Design Reference

Token Structure

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header (Base64URL)
.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6... ← Payload (Base64URL)
.SflKxwRJSMeKKF2QT4fwpMeJf36POkx... ← Signature

Payload Claims

Rule of thumb: Registered claims first, minimal custom claims. Don't put PII or sensitive data — the payload is Base64-encoded, not encrypted.

Example payload I use

{
"sub": "user_abc123",
"iss": "https://api.myapp.com",
"aud": "myapp-web",
"exp": 1716667200,
"iat": 1716663600,
"role": "admin",
"jti": "tok_xyz789"
}

Algorithm Choice

AlgorithmTypeUse when
HS256Symmetric (shared secret)Single service, internal APIs
RS256Asymmetric (private/public key)Multiple services, public key distribution
ES256Asymmetric (ECDSA, smaller keys)Mobile-constrained environments

My default: RS256 for anything that crosses a service boundary. HS256 for simple single-service setups where secret rotation is manageable.

Use Key ID (kid) in the header when rotating keys — consumers can fetch the right public key by ID.


Access & Refresh Token Lifetimes

TokenLifetimeStorage
Access token15 minIn-memory (JS)
Refresh token7 dayshttpOnly cookie

Short access token lifetimes limit blast radius on theft. Refresh tokens should be rotated on use (issue new one, invalidate old).


Validation Checklist

Every token validation must check:

  • Signature is valid
  • exp > now (not expired)
  • iat ≤ now (not issued in the future)
  • iss matches your app's issuer
  • aud matches your app's audience
  • nbf ≤ now (if present)

Libraries: jose (JS/TS), golang-jwt/jwt (Go).


Common Mistakes

MistakeWhy it's badFix
localStorage for tokensXSS can steal itUse httpOnly cookie or memory
Long-lived access tokensStolen token valid for days15 min max
PII in payloadPayload is readable by anyoneKeep payload minimal
Trusting header algAlgorithm confusion attacksEnforce alg server-side
No key rotationCompromised secret = all tokens compromisedRotate with kid

Reference