Two-Token Architecture
Modern authentication uses two token types with different properties:
- Access token: Short-lived (15 minutes), self-contained JWT. Resource servers verify it by checking the signature — no database round-trip required. Short expiry limits damage from theft.
- Refresh token: Long-lived (30 days), opaque random string stored in the database. Used only to obtain new access tokens from the token service. Never sent to resource servers.
JWT Structure
A JWT has three base64url-encoded parts separated by dots: header.payload.signature.
Header: { "alg": "RS256", "kid": "key-2024-01", "typ": "JWT" }
Payload: {
"sub": "user:12345",
"iss": "https://auth.example.com",
"aud": ["https://api.example.com"],
"exp": 1705328400,
"iat": 1705327500,
"jti": "a1b2c3d4-uuid",
"roles": ["editor", "viewer"]
}
Signature: RS256(base64url(header) + "." + base64url(payload), private_key)
jti (JWT ID) is a unique identifier per token, used for revocation. kid identifies which public key to use for verification, enabling key rotation.
JWT Verification at Resource Servers
Resource servers verify access tokens without calling the token service:
- Fetch the public key from the JWKS endpoint (
/.well-known/jwks.json), keyed bykid. Cache it. - Verify the RS256 signature using the public key
- Check
exp > now()(not expired) - Check
audincludes this service's identifier - Check
issmatches the trusted issuer - Optionally check
jtiagainst the revocation denylist
Refresh Token Storage
refresh_tokens(
id UUID PRIMARY KEY,
token_hash TEXT UNIQUE, -- SHA-256 of the opaque token
user_id UUID,
family_id UUID, -- all tokens from same login share this
parent_token_id UUID, -- forms a chain
expires_at TIMESTAMPTZ,
used_at TIMESTAMPTZ,
revoked BOOLEAN DEFAULT FALSE
)
The raw token is never stored — only its SHA-256 hash. Lookup: SELECT * FROM refresh_tokens WHERE token_hash = SHA256(:presented_token).
Refresh Token Rotation
On each use of a refresh token:
- Look up the token by hash. Verify it is not expired and not revoked.
- Check
used_at IS NULL. If already used → token reuse detected (see theft detection below). - Mark the old token
used_at = now(). - Issue a new access token + a new refresh token with the same
family_idandparent_token_id = old token. - Return both tokens to the client.
Single-use enforcement: a refresh token can be used exactly once. After rotation, the old token is permanently consumed.
Theft Detection via Family Tracking
If a stolen refresh token is used after the legitimate client has already rotated it, the attacker presents an already-used token. The token service detects used_at IS NOT NULL and responds by revoking the entire token family:
UPDATE refresh_tokens
SET revoked = TRUE
WHERE family_id = :family_id
This forces the legitimate user to re-authenticate. It may cause a false alarm if a legitimate request was retried, so the revocation response should include a clear re-login prompt.
Access Token Revocation
JWTs are stateless — they cannot be individually revoked without a lookup. For logout or compromised token scenarios, add the jti to a Redis denylist:
SET revoked:{jti} 1 EX {seconds_until_token_expires}
Resource servers check the denylist on each request: EXISTS revoked:{jti}. The TTL matches the token's remaining lifetime so the denylist self-cleans. This adds one Redis lookup per request — acceptable for security-sensitive endpoints.
Key Rotation
RS256 private keys are rotated every 90 days:
- Generate new key pair, assign new
kid - Add new public key to the JWKS endpoint (alongside old key)
- Start signing new tokens with the new private key
- Keep old public key in JWKS until all tokens signed with the old key have expired (15 minutes)
- Remove old key from JWKS
Resource servers cache JWKS with a short TTL (5 minutes) and re-fetch on unknown kid. This makes rotation transparent to resource servers.
Minimal Claims Principle
Put only what resource servers need for authorization decisions in the JWT payload. Avoid: email, display name, phone number, or other PII. These are readable by anyone with the base64-decoded token (JWT payload is not encrypted, only signed). Resource servers that need user profile data should query the user service, not read it from the token.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does refresh token rotation prevent token theft?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “On each use, the refresh token is invalidated and a new one is issued atomically; if an attacker reuses a stolen token, the legitimate user's next refresh attempt will find the token already consumed and trigger a 'reuse detected' alarm that revokes the entire token family. This limits the theft window to the interval between the victim's last legitimate use and the attacker's use.”
}
},
{
“@type”: “Question”,
“name”: “How are JWT access tokens revoked before they expire?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Because JWTs are stateless and self-validating, early revocation requires maintaining a denylist (blocklist) of revoked JWT IDs (jti claims) in a fast store like Redis, checked on every token validation. To keep the denylist bounded, entries are stored with a TTL equal to the token's remaining validity, so the list only contains tokens that haven't naturally expired yet.”
}
},
{
“@type”: “Question”,
“name”: “What is token family tracking and how does it detect stolen tokens?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A token family is a chain of refresh tokens derived from the same original authentication event, stored in a table with parent-child relationships and a consumed flag per token. If a refresh request arrives for a token already marked consumed (indicating a second party is using a rotated-away token), the entire family is invalidated and the user is forced to re-authenticate.”
}
},
{
“@type”: “Question”,
“name”: “How does RS256 key rotation work with JWKS?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The token service generates a new RSA key pair on a rotation schedule, adds the new public key to the JWKS endpoint (/.well-known/jwks.json) with a unique kid, and begins signing new tokens with the new private key while keeping the old public key in JWKS until all tokens signed with it have expired. Resource servers cache the JWKS with a short TTL and re-fetch on unknown kid to pick up rotated keys without downtime.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems