Skip to content
JWT Authentication and Refresh Rotation: Stateless Authentication That Works 4 min

JWT Authentication and Refresh Rotation: Stateless Authentication That Works

SD
ScaleDojo
May 11, 2026
4 min read1,041 words
JWT Tokens and Refresh Rotation: Stateless Authentication That Works

The Algorithm Confusion Attack That Broke Auth0

In 2022, security researchers discovered that a misconfigured JWT library let attackers change the algorithm in the JWT header from RS256 (asymmetric) to HS256 (symmetric). The server's public key, meant to be shared openly, was suddenly being used as the HMAC secret. Since that public key was, well, public, anyone could forge valid JWTs and access any account they wanted. The bug affected libraries in Node.js, Python, Ruby, and PHP, and it remains a masterclass in why implementation details matter so much for JWT authentication.

JWT Anatomy

JWT Structure (three Base64URL-encoded parts):

  eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyNDIiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MDUzMTI4MDB9.dG9rZW5fc2lnbmF0dXJl
  |___ Header ___|        |__________________ Payload ___________________|        |___ Signature ___|

  Header (decoded):
  {
    "alg": "RS256",        // Signing algorithm
    "typ": "JWT",          // Token type
    "kid": "key-2024-01"   // Which key to use for verification
  }

  Payload (decoded):
  {
    "sub": "user42",       // Subject (who)
    "role": "admin",       // Custom claim (what permissions)
    "iss": "auth.myapp.com",  // Issuer (who created this)
    "aud": "api.myapp.com",   // Audience (who should accept this)
    "iat": 1705309200,     // Issued at (when created)
    "exp": 1705312800      // Expires (when it dies) - 1 hour later
  }

  Signature:
  RS256( base64(header) + '.' + base64(payload), PRIVATE_KEY )

  WARNING: The payload is NOT encrypted!
  base64decode('eyJzdWIiOiJ1c2VyNDIi...') = readable JSON
  NEVER put passwords, SSNs, or secrets in JWT claims.

Stateless vs Stateful Authentication

The difference between session-based and JWT-based authentication comes down to where the source of truth lives, in the database or in the token itself. That single design choice ripples through every request your server handles.

Session-Based (Stateful):

  Client                  Server                  Database
    |--POST /login-------->|                          |
    |                      |--INSERT session--------->|
    |                      |<--session_id='abc123'----||
    |<--Cookie: sid=abc123-|                          |
    |                      |                          |
    |--GET /api (sid=abc)-->|--SELECT * FROM sessions->|
    |                      |  WHERE id='abc123'       |
    |                      |<--user_id=42, role=admin--|
    |<--200 OK-------------|                          |

  Every request = 1 database query. 10K req/s = 10K DB lookups/s.

JWT-Based (Stateless):

  Client                  Server
    |--POST /login-------->|
    |                      |--Sign JWT with private key
    |<--JWT token----------|  (CPU operation, no DB)
    |                      |
    |--GET /api (JWT)----->|
    |                      |--Verify signature (CPU only)
    |                      |  Decode claims: user=42, role=admin
    |<--200 OK-------------|  No database needed!

  Every request = 1 CPU operation. Scales horizontally.
  Any server with the public key can verify any token.

That 10K-lookups-per-second number is exactly the kind of bottleneck that pushes teams toward caching layers for session data, or toward stateless JWT authentication altogether. If you're weighing where to put that state, it's worth reading through caching strategies, invalidation, and placement trade-offs before you commit to an architecture.

The Revocation Problem and Solutions

The Revocation Dilemma:

  User changes password at 10:00 AM
  JWT issued at 9:55 AM, expires at 10:10 AM
  The JWT is STILL VALID for 10 more minutes!
  (Stateless = server cannot 'un-issue' a token)

  Solution: Access + Refresh Token Pattern

  Timeline:
  |----access token (15 min)----|
  |                             |----access token (15 min)----|
  |=============refresh token (7 days)========================|

  Access Token:                    Refresh Token:
  - Short-lived (5-15 min)         - Long-lived (7-30 days)
  - Sent with every API request    - Sent ONLY to /token/refresh
  - Stored in memory (JS variable) - Stored in HTTP-only cookie
  - If stolen: 15 min damage max   - If stolen: detected via rotation
  - Stateless verification         - Checked against database

Refresh Token Rotation

This is the piece that turns a JWT refresh token from a liability into a theft-detection mechanism. Each refresh token can only be used once, and reuse is treated as a red flag rather than a normal request.

Refresh Token Rotation (Theft Detection):

  Normal flow:
  Client: POST /token/refresh  { refresh_token: 'RT-1' }
  Server: Invalidate RT-1
          Issue new: { access_token: 'AT-2', refresh_token: 'RT-2' }

  Client: POST /token/refresh  { refresh_token: 'RT-2' }
  Server: Invalidate RT-2
          Issue new: { access_token: 'AT-3', refresh_token: 'RT-3' }

  Theft scenario:
  Attacker steals RT-2 and uses it:
  Attacker: POST /token/refresh  { refresh_token: 'RT-2' }
  Server: RT-2 already used! THEFT DETECTED!
          Invalidate ENTIRE token family
          Force user to re-login

  The key insight: each refresh token is single-use.
  If a used token appears again, someone stole it.

HS256 vs RS256: Choosing the Right Algorithm

Once you move past a single monolith and start splitting things into services, especially behind an API gateway or BFF layer, the choice between HS256 and RS256 stops being academic and starts mattering a lot.

Algorithm Selection:

  HS256 (Symmetric - HMAC + SHA-256):
    Same secret key signs AND verifies
    + Faster (pure hash operation)
    - Every service that verifies needs the secret
    - If any service is compromised, attacker can forge tokens
    Best for: single-service architectures

  RS256 (Asymmetric - RSA + SHA-256):
    Private key signs, public key verifies
    + Only auth service has the private key
    + Any service can verify with the public key
    + Compromised service cannot forge new tokens
    - Slower (~10x) than HS256
    Best for: microservices, multi-service architectures

  Rule: If more than one service verifies tokens, use RS256.

JWT Security Checklist

  • Always validate the exp, iss, and aud claims, and reject tokens that are missing them.

  • Use RS256 for multi-service architectures. Never share signing secrets across services.

  • Enforce the algorithm in code. Never trust the alg header from the token itself (this is exactly what prevents algorithm confusion attacks).

  • Store refresh tokens in HTTP-only, Secure, SameSite=Strict cookies, never in localStorage, which is vulnerable to XSS.

  • Keep access tokens short-lived (5-15 minutes) to limit the blast radius of a stolen token.

  • Never put sensitive data in the payload. It's Base64-encoded, not encrypted, so anyone can read it.

  • Implement refresh token rotation so you can detect theft the moment a used token reappears.

Interview Tip

JWT questions are almost guaranteed in system design interviews. Draw the access + refresh token flow and explain the tradeoff: JWTs eliminate the session database bottleneck but sacrifice instant revocation. Mention refresh token rotation as the theft detection mechanism. When asked "how do you handle logout?", explain three options: (1) short-lived access tokens make logout less critical, (2) a token blacklist in Redis for immediate revocation, (3) a per-user token version counter that invalidates all tokens on password change. The strongest answer combines all three.

If you want more practice framing these tradeoffs out loud, the low-level design fundamentals guide and our roundup of system design learning platforms are both good places to keep sharpening this before an interview.

Key Takeaway

JWTs enable stateless authentication at scale but cannot be revoked once issued. Combine short-lived access tokens (5-15 min) with rotating refresh tokens to get both scalability and security. Use RS256 for microservices. And never, ever store sensitive data in the payload, it's readable by anyone who bothers to decode it.

Enjoyed this article?

Share it with your network to help others level up their system design skills.

Discussion0

Join the Discussion

Sign in to leave comments, reply to others, or like insights.

Sign In to ScaleDojo

No comments yet. Be the first to start the thread!

Related Articles

Enjoyed this content?

Your support keeps us creating free resources

We put a lot of hours into researching and writing these guides. If it helped you, consider buying us a coffee. Every bit goes toward keeping ScaleDojo's content free and growing.

$

One-time payment via Stripe. ScaleDojo account required.

ScaleDojo Logo
Initializing ScaleDojo