When a JWT is the right tool
JWTs are great when you need a stateless, signed assertion that can be carried across services without a database round-trip. They are not a universal session container.
Reach for a JWT when…
· Service-to-service auth where the receiver can't hit your DB.
· Short-lived API access tokens (5–60 min).
· OIDC id_token carrying signed user identity.
· One-time-use tokens (email verify, password reset, magic link).
Use a session instead when…
· You need instant revocation (logout, ban, password change).
· Your session lives for days or weeks.
· You'd be tempted to store a JWT in localStorage for a browser app.
· The data is mutable and shouldn't be cached client-side.
Issuance
- Prefer asymmetric algorithms (
RS256,ES256,EdDSA) overHS256whenever more than one service needs to verify — only the issuer ever needs the private key. - Pin your algorithm. Encode
algin your verifier configuration, not by reading it back from the token header. - Use a
kidin the header so verifiers can pick the right key during rotation. - Keep tokens short-lived (≤ 1 hour for access tokens) and pair with a refresh token if you need longer sessions.
Claim hygiene
- Always include
iss,aud,iat, andexp. Addnbfif your token is post-dated. - Use
jtiif you need replay protection or a revocation list. - Keep payloads small — every request carries the full token. A bloated JWT means bloated headers everywhere.
- Never put PII or secrets in the payload. It's signed, not encrypted. If you need confidentiality, use JWE.
- Use stable, opaque subject identifiers (
sub= user UUID, not email).
Do
sub: stable user id (UUID, ULID).scopeorpermissions: explicit list.expwithin 60 minutes.jtifor one-time tokens.
Don't
- Email or phone as the subject.
- Plaintext passwords or API keys.
- Open-ended
"admin": trueroles. - Tokens that live for days or weeks.
Client storage
- Store access tokens in memory (a JS variable / React state) for browser SPAs — not
localStorage. - Store refresh tokens in
HttpOnly; Secure; SameSite=Strictcookies so JavaScript can't read them. - In native mobile apps, use the OS keychain / keystore — never plaintext shared preferences.
Verification checklist
- Verify the signature against the expected key and algorithm — never derive them from the token.
- Reject
alg: noneoutright. - Check
expandnbfwith a small (≤ 60s) clock skew tolerance. - Assert
issandaudmatch your service's allow-list. - Reject tokens larger than a sensible size limit (e.g. 8 KB).
Key rotation
- Publish keys via a JWKS endpoint; rotate on a scheduled cadence (e.g. every 90 days).
- Keep old keys active for one token TTL after rotation so in-flight tokens remain valid.
- Rotate immediately after any key material exposure or suspicious verification failures.
- Automate rotation — manual steps get skipped under pressure.
Pre-flight checklist
Algorithm is pinned server-side
Never read alg from the token header to decide how to verify.
exp is present and ≤ 1 hour
Long-lived tokens magnify the impact of any compromise.
iss and aud are validated on receipt
Without this, tokens from other issuers may be accepted.
No PII or secrets in the payload
Anyone with the token can decode and read every claim.
Refresh tokens are HttpOnly cookies
Prevents JavaScript from exfiltrating long-lived credentials.
JWKS endpoint is published and cached
Enables zero-downtime key rotation across all verifiers.
jti or short TTL prevents replay
A stolen token should have a limited window of validity.