JWT Architecture: Stateless Authentication at Scale


Your API has 50 servers. A user logs in. Their session is stored in a database. Every subsequent request requires a database lookup to validate the session. At 100,000 requests per second, that is 100,000 database lookups per second just for authentication. Your auth database becomes a bottleneck.

JWTs (JSON Web Tokens) eliminate this bottleneck. The token itself contains the user’s identity, signed by the server. Any server can verify the signature without a database lookup. Authentication becomes a local cryptographic operation.

What a JWT is

A JWT is a compact, URL-safe token that contains claims (statements about the user) and a cryptographic signature. It has three parts, separated by dots:

header.payload.signature

Header: Specifies the token type and signing algorithm.

{"alg": "RS256", "typ": "JWT"}

Payload: Contains claims about the user.

{
  "sub": "user:123",
  "email": "alice@example.com",
  "roles": ["user", "admin"],
  "iat": 1705312800,
  "exp": 1705316400
}

Signature: Cryptographic signature over the header and payload.

The entire token is base64url-encoded. It looks like: eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyOjEyMyJ9.signature

How JWT authentication works

graph LR
subgraph login["Login Flow"]
  U1["User"] -->|"credentials"| AUTH["Auth service"]
  AUTH -->|"verify credentials"| DB["User database"]
  DB -->|"user data"| AUTH
  AUTH -->|"sign JWT
with private key"| U1
end

subgraph request["Subsequent Requests"]
  U2["User"] -->|"request + JWT"| API["Any API server"]
  API -->|"verify signature
with public key
(no DB lookup)"| API
  API -->|"response"| U2
end

style AUTH fill:#EEEDFE,stroke:#534AB7,color:#3C3489
style API fill:#E1F5EE,stroke:#0F6E56,color:#085041
style DB fill:#FAEEDA,stroke:#854F0B,color:#633806
  1. User logs in with credentials
  2. Auth service verifies credentials against the database
  3. Auth service creates a JWT with user claims and signs it with a private key
  4. JWT is returned to the client (stored in memory, localStorage, or httpOnly cookie)
  5. Client includes the JWT in subsequent requests (Authorization: Bearer <token>)
  6. Any API server verifies the JWT signature using the public key
  7. If valid and not expired, the request is processed

Signing algorithms

HS256 (HMAC-SHA256): Symmetric algorithm. The same secret key is used to sign and verify. Simple but requires all servers to share the secret. If the secret leaks, all tokens can be forged.

RS256 (RSA-SHA256): Asymmetric algorithm. Private key signs, public key verifies. The private key is kept secret on the auth server. The public key can be distributed to all API servers. More secure for distributed systems.

ES256 (ECDSA-SHA256): Asymmetric algorithm using elliptic curves. Smaller signatures than RS256, similar security. Increasingly preferred.

For distributed systems, use RS256 or ES256. The public key can be published at a JWKS (JSON Web Key Set) endpoint, allowing API servers to automatically fetch and rotate keys.

Where it breaks or gets interesting

The revocation problem

JWTs cannot be revoked before they expire. If a user logs out, their JWT is still valid until expiry. If an account is compromised and you want to invalidate all sessions, you cannot - the JWTs are still valid.

Solutions:

  • Short expiry times - 15-minute access tokens. After 15 minutes, the token is invalid. Users get a new token via a refresh token.
  • Refresh token rotation - Issue a short-lived access token and a long-lived refresh token. The refresh token is stored in the database. To revoke access, delete the refresh token.
  • Token revocation list - Maintain a list of revoked token IDs (jti claim) in Redis. Check the list on every request. This reintroduces a database lookup but only for revoked tokens.
  • Short-lived tokens + logout endpoint - Accept that logout is “soft” (the token is still valid for up to 15 minutes after logout). For most applications, this is acceptable.

The “none” algorithm vulnerability

Early JWT libraries had a vulnerability: if the algorithm was set to “none” in the header, the signature was not verified. An attacker could forge a token by setting alg: none and removing the signature.

Fix: always explicitly specify the allowed algorithms when verifying. Never accept alg: none.

Storing JWTs securely

localStorage: Accessible to JavaScript. Vulnerable to XSS attacks. If an attacker injects JavaScript, they can steal the token.

httpOnly cookie: Not accessible to JavaScript. Protected against XSS. But vulnerable to CSRF attacks (cross-site request forgery). Mitigate with CSRF tokens or SameSite cookie attribute.

In-memory: Most secure against XSS (no persistent storage). But lost on page refresh. Requires a refresh token flow to re-authenticate.

Best practice: store access tokens in memory, store refresh tokens in httpOnly cookies with SameSite=Strict.

JWT size

JWTs can get large if you include many claims. A JWT with 10 claims might be 500-1000 bytes. This is sent with every request. For high-traffic APIs, this adds up.

Keep JWTs small: include only the claims you need. Do not include large objects. Use short claim names (sub, not subject).

graph TB
subgraph refresh["Access Token + Refresh Token Flow"]
  LOGIN["Login"] -->|"credentials"| AUTH2["Auth service"]
  AUTH2 -->|"access token
15min expiry"| CLIENT["Client"]
  AUTH2 -->|"refresh token
30 day expiry
httpOnly cookie"| CLIENT
  CLIENT -->|"API request
+ access token"| API2["API server"]
  API2 -->|"verify locally
no DB lookup"| API2
  CLIENT -->|"access token expired"| REFRESH["POST /refresh
+ refresh token cookie"]
  REFRESH -->|"verify refresh token
in database"| DB2["Database"]
  DB2 -->|"valid"| REFRESH
  REFRESH -->|"new access token"| CLIENT
end

style AUTH2 fill:#EEEDFE,stroke:#534AB7,color:#3C3489
style API2 fill:#E1F5EE,stroke:#0F6E56,color:#085041
style DB2 fill:#FAEEDA,stroke:#854F0B,color:#633806

Real-world systems

Auth0 - Identity platform that issues JWTs. Supports RS256 with automatic key rotation. JWKS endpoint for public key distribution.

AWS Cognito - Managed identity service. Issues JWTs (ID tokens, access tokens). Integrates with API Gateway for automatic JWT validation.

Google - Issues JWTs for Google Sign-In. Public keys available at a JWKS endpoint.

Stripe - Uses JWTs for their Connect platform. Restricted keys are JWTs with specific permission claims.

Kubernetes - Service account tokens are JWTs. Pods use them to authenticate with the Kubernetes API server.

How to apply it in practice

JWT claims to include

Standard claims:

  • sub - Subject (user ID)
  • iat - Issued at (Unix timestamp)
  • exp - Expiry (Unix timestamp)
  • jti - JWT ID (unique identifier, for revocation)
  • iss - Issuer (your auth service URL)
  • aud - Audience (which service this token is for)

Custom claims:

  • roles or permissions - User’s roles or permissions
  • email - User’s email (if needed by API servers)
  • tenant_id - For multi-tenant applications

Token expiry strategy

  • Access token: 15 minutes. Short enough to limit damage if stolen.
  • Refresh token: 30 days. Long enough for a good user experience.
  • Refresh token rotation: Issue a new refresh token on every refresh. Invalidate the old one. If a stolen refresh token is used, the legitimate user’s next refresh will fail (their token was invalidated), alerting them to the compromise.

JWKS endpoint

Publish your public keys at /.well-known/jwks.json. API servers fetch the keys and cache them. When you rotate keys, add the new key to the JWKS before using it to sign tokens. Remove old keys after all tokens signed with them have expired.

FAQ

Q: Should you put sensitive data in a JWT?

No. JWTs are base64-encoded, not encrypted. Anyone who has the token can decode the payload and read the claims. Do not include passwords, credit card numbers, or other sensitive data. If you need to include sensitive data, use JWE (JSON Web Encryption) to encrypt the payload.

Q: What is the difference between a JWT and a session token?

A session token is an opaque random string. The server stores the session data in a database. Every request requires a database lookup to validate the session and retrieve the data. A JWT is self-contained: the data is in the token itself. No database lookup needed for validation. JWTs are better for distributed systems (no shared session store). Session tokens are better when you need immediate revocation.

Q: How do you handle JWT key rotation?

Use the kid (key ID) claim in the JWT header to identify which key was used to sign the token. Publish all active public keys in the JWKS endpoint. When rotating: generate a new key pair, add the new public key to JWKS, start signing new tokens with the new private key, wait for all tokens signed with the old key to expire, remove the old public key from JWKS. API servers cache the JWKS and refresh it periodically (or when they encounter an unknown kid).

Interview questions

Q1: Your API has 100 servers. How do you implement authentication without a database lookup on every request?

Strong answer: Use JWTs with asymmetric signing (RS256). The auth service signs tokens with a private key. All 100 API servers verify tokens using the public key - a local cryptographic operation, no database needed. Publish the public key at a JWKS endpoint. API servers fetch and cache the public key. Use short-lived access tokens (15 minutes) to limit the damage if a token is stolen. Use refresh tokens (stored in the database) for re-authentication. This scales to any number of API servers without adding load to the auth database.

Q2: A user’s account is compromised. How do you immediately invalidate all their sessions with JWTs?

Strong answer: JWTs cannot be revoked before expiry. Options: use short-lived tokens (15 minutes) and accept that the attacker has access for up to 15 minutes. Or maintain a token revocation list in Redis: store the jti (JWT ID) of revoked tokens. On every request, check if the token’s jti is in the revocation list. This adds a Redis lookup per request but allows immediate revocation. Or use refresh token rotation: invalidate the user’s refresh token in the database. The attacker’s access token is still valid for 15 minutes, but they cannot get a new one after it expires. For immediate revocation, the Redis revocation list is the most practical approach.

Q3: What are the security risks of storing JWTs in localStorage and how do you mitigate them?

Strong answer: localStorage is accessible to JavaScript. An XSS (cross-site scripting) vulnerability allows an attacker to inject JavaScript that reads the JWT from localStorage and sends it to the attacker’s server. The attacker can then use the JWT to impersonate the user. Mitigation: store access tokens in memory (not localStorage). Store refresh tokens in httpOnly cookies (not accessible to JavaScript). Use Content Security Policy (CSP) headers to prevent XSS. Validate and sanitize all user input. Use short-lived access tokens so stolen tokens expire quickly. For the refresh token cookie, use SameSite=Strict to prevent CSRF attacks. This defense-in-depth approach minimizes the impact of any single vulnerability.