FastAPI JWT Refresh Token Rotation
Issue short-lived access tokens plus rotating refresh tokens in FastAPI, detecting reuse of revoked tokens on every exchange.
Code
from datetime import datetime, timedelta, timezone
import jwt
from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
SECRET = 'rotate-me-in-production'
REFRESH_TTL = timedelta(days=14)
ACCESS_TTL = timedelta(minutes=15)
app = FastAPI()
oauth2 = OAuth2PasswordBearer(tokenUrl='/token')
revoked: set[str] = set() # 生产环境应持久化并设置过期
class TokenPair(BaseModel):
access: str
refresh: str
def issue_tokens(user_id: str) -> TokenPair:
now = datetime.now(timezone.utc)
return TokenPair(
access=jwt.encode(
{'sub': user_id, 'exp': now + ACCESS_TTL, 'typ': 'access'},
SECRET, algorithm='HS256'),
refresh=jwt.encode(
{'sub': user_id, 'exp': now + REFRESH_TTL, 'typ': 'refresh',
'jti': f'{user_id}-{int(now.timestamp())}'},
SECRET, algorithm='HS256'),
)
@app.post('/token/refresh', response_model=TokenPair)
def refresh(pair: TokenPair):
try:
claims = jwt.decode(pair.refresh, SECRET, algorithms=['HS256'])
except jwt.InvalidTokenError:
raise HTTPException(401, 'invalid refresh token')
if pair.refresh in revoked:
raise HTTPException(401, 'token reuse detected')
revoked.add(pair.refresh)
return issue_tokens(claims['sub'])
@app.get('/me')
def me(token: str = Depends(oauth2)):
claims = jwt.decode(token, SECRET, algorithms=['HS256'])
if claims.get('typ') != 'access':
raise HTTPException(401, 'wrong token type')
return {'user': claims['sub']}Report a problem with this exampleOpens a pre-filled issue on GitHub.