Refresh Tokens and Token Rotation
Design short-lived access tokens with rotating refresh tokens and server-side revocation to mitigate token theft.
Why Two Tokens?
A single long-lived JWT access token is convenient but dangerous: if it leaks, an attacker can use it until it expires. Since JWTs are stateless, you cannot easily revoke one mid-flight.
The standard OAuth2 answer is to split responsibilities into two tokens:
- Access token — short-lived (5-15 min), sent on every request, verified by signature only (no DB hit).
- Refresh token — long-lived (days/weeks), used only to obtain a new access token, and tracked server-side so it can be revoked.
This way a stolen access token is useless within minutes, while users still stay logged in for a long time.
Anatomy of the Token Pair
The login endpoint returns both tokens. The client keeps the access token in memory and the refresh token somewhere more protected (e.g. an HttpOnly cookie).
Notice the very different expiry windows. The access token is intentionally short so a leak has a tiny blast radius.
from datetime import datetime, timedelta, timezone
ACCESS_TOKEN_TTL = timedelta(minutes=15)
REFRESH_TOKEN_TTL = timedelta(days=7)
def expiry(ttl: timedelta) -> str:
return (datetime.now(timezone.utc) + ttl).isoformat()
login_response = {
"access_token": "<jwt>",
"access_expires": expiry(ACCESS_TOKEN_TTL),
"refresh_token": "<opaque-or-jwt>",
"refresh_expires": expiry(REFRESH_TOKEN_TTL),
"token_type": "bearer",
}
for k, v in login_response.items():
print(f"{k}: {v}")All lessons in this course
- OAuth2 Password Flow and Token Issuance
- Signing and Verifying JWTs with python-jose
- Refresh Tokens and Token Rotation
- Scope-Based Authorization and Role Guards