FastAPI cookbook · Advanced
Use refresh tokens in HttpOnly cookies with FastAPI
Keep users signed in for weeks while access tokens stay short-lived and safe from XSS.
Approach
Issue a short-lived access token (returned in the JSON body, kept in memory by the SPA) and a long-lived, random refresh token stored hashed in the database and sent as an HttpOnly; Secure; SameSite=Strict cookie scoped to the refresh path. Rotate the refresh token on every use and revoke the whole family if an old one is reused.
Solution
import hashlib, secrets
from datetime import timedelta
from fastapi import Cookie, HTTPException, Response
REFRESH_PATH = "/auth/refresh"
def set_refresh_cookie(response: Response, token: str) -> None:
response.set_cookie("refresh_token", token, httponly=True, secure=True, samesite="strict",
path=REFRESH_PATH, max_age=int(timedelta(days=30).total_seconds()))
async def issue_refresh(db, user_id: int, family: str | None = None) -> str:
raw = secrets.token_urlsafe(48)
db.add(RefreshToken(user_id=user_id, family=family or secrets.token_hex(8),
token_hash=hashlib.sha256(raw.encode()).hexdigest(), expires_at=utcnow() + timedelta(days=30)))
return raw
@router.post("/auth/refresh")
async def refresh(response: Response, db: DB, refresh_token: str | None = Cookie(default=None)):
if not refresh_token:
raise HTTPException(401, "No refresh token")
row = await db.scalar(select(RefreshToken).where(RefreshToken.token_hash == hashlib.sha256(refresh_token.encode()).hexdigest()))
if row is None or row.expires_at < utcnow():
raise HTTPException(401, "Invalid refresh token")
if row.used_at is not None: # reuse → likely theft
await db.execute(update(RefreshToken).where(RefreshToken.family == row.family).values(revoked=True))
await db.commit()
raise HTTPException(401, "Refresh token reuse detected")
if row.revoked:
raise HTTPException(401, "Session revoked")
row.used_at = utcnow()
new_raw = await issue_refresh(db, row.user_id, family=row.family)
await db.commit()
set_refresh_cookie(response, new_raw)
return {"access_token": create_access_token(row.user_id), "token_type": "bearer"}
Pitfalls
- Refresh tokens in localStorage.
- Not rotating refresh tokens.
- Cross-site setups where SameSite=Strict blocks the cookie (adjust domain/SameSite and add CSRF protection).
Variations
- A BFF (backend-for-frontend) keeps all tokens server-side and uses a session cookie only.
Further reading: RFC 9700: OAuth 2.0 Security Best Current Practice
Reference: https://datatracker.ietf.org/doc/html/rfc9700
More FastAPI recipes
- Add liveness and readiness endpoints to FastAPI
- Add OpenTelemetry tracing to FastAPI
- Add request IDs and structured logs to FastAPI
- Build a WebSocket chat endpoint in FastAPI
- Cache FastAPI responses in Redis
- Call external APIs with retries and timeouts from FastAPI
- Implement role-based access control in FastAPI
- Isolate tenants in a multi-tenant FastAPI app