JWT Best Practices: Signing, Expiry, and Key Rotation

JWT

JSON Web Tokens (JWTs) are the most common way to carry authentication state in modern APIs. They're compact, self-contained, and easy to verify — but they're also easy to misuse. This guide covers the practices that keep JWTs secure in production.

Anatomy of a JWT

A JWT has three Base64url-encoded parts separated by dots:

header.payload.signature
  • Header: Algorithm and token type ({"alg": "HS256", "typ": "JWT"})
  • Payload: Claims — your data plus standard fields like exp, iat, sub
  • Signature: Proof that the token hasn't been tampered with

Choosing a Signing Algorithm

The alg field in the header determines how the token is signed.

HMAC (Symmetric)

  • HS256, HS384, HS512 — the same secret key signs and verifies.
  • Simple to set up. Good for single-service architectures where only your backend creates and validates tokens.
  • Risk: Every service that needs to verify the token must have the secret key. If any one is compromised, an attacker can forge tokens.

RSA / ECDSA (Asymmetric)

  • RS256, RS384, RS512 (RSA) or ES256, ES384, ES512 (Elliptic Curve)
  • A private key signs; a public key verifies.
  • Ideal for microservices: only the auth service holds the private key, while downstream services verify with the public key.
  • ES256 is preferred over RS256 — smaller keys, faster verification.

Generate a JWT with the Auth Toolkit API:

curl -X POST https://auth.toolkitapi.io/v1/auth/jwt-generate \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {"sub": "user_42", "role": "admin"},
    "secret": "your-256-bit-secret",
    "algorithm": "HS256",
    "expires_in": 900
  }'

Expiration Best Practices

Always set exp. A token without an expiration is valid forever. If it leaks, there's no way to revoke it (JWTs are stateless).

Use case Recommended expires_in
Access tokens 5–15 minutes
Refresh tokens 7–30 days (store securely)
Email verification links 24 hours
Password reset tokens 15–60 minutes

Short-lived access tokens limit the blast radius of a stolen token. Pair them with longer-lived refresh tokens to avoid forcing users to re-authenticate constantly.

Claims to Include (and Avoid)

Include:

  • sub — subject (user ID)
  • iat — issued-at timestamp
  • exp — expiration
  • iss — issuer (your domain)
  • aud — audience (which service the token is for)

Avoid putting in the payload:

  • Passwords or secrets
  • Full user profiles (name, email, address)
  • Authorization details that change frequently

The payload is encoded, not encrypted. Anyone with the token can decode it and read the claims. Use the Decode JWT endpoint to inspect any token without verifying:

curl -X POST https://auth.toolkitapi.io/v1/auth/jwt-decode \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token": "eyJhbGciOiJIUzI1NiIs..."}'

Key Rotation

Secrets and keys should be rotated regularly. When you rotate:

  1. Generate a new key pair (or secret).
  2. Sign new tokens with the new key. Add a kid (Key ID) to the header.
  3. Keep the old key in your verification key set for the max token lifetime.
  4. After all old tokens expire, remove the old key.

The Auth Toolkit's Generate Key Pair endpoint can create RSA or EC keys on demand:

curl -X POST https://auth.toolkitapi.io/v1/auth/generate-keypair \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"algorithm": "EC", "curve": "P-256"}'

Common Pitfalls

Using alg: none

Some libraries accept "alg": "none", which means no signature at all. Always validate the alg header and reject none.

Confusing encoding with encryption

Base64 is encoding, not encryption. The payload is readable by anyone. If you need to hide claims, encrypt the payload separately or use JWE (JSON Web Encryption).

Not validating expiration on the server

Always verify exp server-side. Don't rely on the client to discard expired tokens.

Storing JWTs in localStorage

Tokens in localStorage are accessible to any JavaScript on the page (XSS target). Prefer httpOnly cookies with Secure and SameSite flags.

Verifying Tokens

Always verify the signature before trusting any claims:

curl -X POST https://auth.toolkitapi.io/v1/auth/jwt-verify \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "secret": "your-256-bit-secret",
    "verify_exp": true
  }'

The response tells you exactly what's wrong — expired, invalid signature, or valid:

{
  "valid": true,
  "payload": {"sub": "user_42", "role": "admin", "exp": 1775268647},
  "header": {"alg": "HS256", "typ": "JWT"},
  "expired": false,
  "error": null
}

Try it out

Browse Tools →

More from the Blog