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​