Guide · best practices

Production-grade JWT patterns

JWTs are unforgiving — the wrong default can lock you out of a deploy or leak a session for hours. These are the choices to make on day one, before you ship.

First principles

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) over HS256 whenever more than one service needs to verify — only the issuer ever needs the private key.
  • Pin your algorithm. Encode alg in your verifier configuration, not by reading it back from the token header.
  • Use a kid in 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, and exp. Add nbf if your token is post-dated.
  • Use jti if 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).
  • scope or permissions: explicit list.
  • exp within 60 minutes.
  • jti for one-time tokens.

Don't

  • Email or phone as the subject.
  • Plaintext passwords or API keys.
  • Open-ended "admin": true roles.
  • 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=Strict cookies 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: none outright.
  • Check exp and nbf with a small (≤ 60s) clock skew tolerance.
  • Assert iss and aud match 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

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.