How to Implement Token Refresh Logic in FastAPI: A Complete JWT Guide
You can implement token refresh logic in FastAPI by pairing short-lived access tokens with long-lived refresh tokens stored in HTTP-only cookies, then exposing a /refresh endpoint that validates the refresh token and issues a new access token while maintaining a blacklist for revoked tokens.
The benavlabs/fastapi-boilerplate repository provides a production-ready implementation of JWT authentication with built-in token refresh capabilities. This guide explains how to implement token refresh logic using the boilerplate's security utilities, covering token generation, HTTP-only cookie storage, and secure token revocation.
Understanding the Token Refresh Architecture
The token refresh system relies on three primary components working together to manage the lifecycle of JWTs.
Core Security Components
-
src/app/core/security.py: Containscreate_access_token,create_refresh_token,verify_token, andblacklist_tokenfunctions. This module handles JWT signing usingsettings.SECRET_KEY, validatestoken_typeclaims ("access"or"refresh"), and manages revocation viacrud_token_blacklist. -
src/app/api/v1/login.py: Implements thelogin_for_access_tokenendpoint for initial authentication and the/refreshendpoint for token renewal. The refresh logic extracts the token from HTTP-only cookies and returns a new access token without requiring user credentials. -
src/app/api/v1/logout.py: Handles token revocation through theblacklist_tokensfunction, which inserts both access and refresh tokens into thetoken_blacklisttable using theirexpclaims to prevent reuse.
How the Token Refresh Flow Works
The implementation follows a stateless OAuth2-inspired flow with enhanced security through HTTP-only cookies and token blacklisting.
Step 1: Initial Login and Token Issuance
When a user authenticates via the login_for_access_token endpoint in src/app/api/v1/login.py, the system generates two distinct tokens:
- Access token: Short-lived (default 15 minutes based on
ACCESS_TOKEN_EXPIRE_MINUTESinsrc/app/core/config.py) withtoken_typeclaim set to"access" - Refresh token: Long-lived (default 7 days based on
REFRESH_TOKEN_EXPIRE_DAYS) withtoken_typeclaim set to"refresh", stored as an HTTP-only cookie
Both tokens are signed with settings.SECRET_KEY and include standard JWT claims (exp, sub).
Step 2: Refreshing the Access Token
When the access token expires, the client calls POST /api/v1/refresh. The implementation in src/app/api/v1/login.py performs the following:
- Extracts the refresh token from the
refresh_tokenHTTP-only cookie - Calls
verify_token(refresh_token, TokenType.REFRESH, db)fromsrc/app/core/security.pyto validate:- Signature validity using
settings.SECRET_KEY token_typeclaim equals"refresh"- Token is not present in the blacklist (checked via
crud_token_blacklist)
- Signature validity using
- Generates a new access token using
create_access_token - Returns the new access token in the response body
The original refresh token remains valid until it expires or is explicitly revoked.
Step 3: Token Revocation on Logout
When logging out via POST /api/v1/logout, the system revokes both tokens:
- The endpoint receives the access token via the
Authorization: Bearerheader and the refresh token from the cookie - Calls
blacklist_tokensfromsrc/app/core/security.py, which:- Decodes each JWT to extract the
expclaim - Creates a
TokenBlacklistrow in the database viacrud_token_blacklistwith the token's expiry
- Decodes each JWT to extract the
- Subsequent calls to
verify_tokencheck the blacklist and reject revoked tokens
Implementing Token Refresh in Your Application
The following examples demonstrate how to interact with the token refresh endpoints from a Python client using httpx.
Authenticating and Storing Tokens
import httpx
async def login(username: str, password: str):
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
response = await client.post(
"/api/v1/login",
data={"username": username, "password": password},
)
# The response body contains the short-lived access token
tokens = response.json() # {"access_token": "...", "token_type": "bearer"}
# The refresh token is automatically stored as a HttpOnly cookie in the client
return tokens["access_token"]
Refreshing an Expired Access Token
import httpx
async def get_fresh_access_token():
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
# The HttpOnly refresh_token cookie is sent automatically
resp = await client.post("/api/v1/refresh")
resp.raise_for_status()
return resp.json()["access_token"]
Calling Protected Endpoints
async def call_protected_endpoint(access_token: str):
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
headers = {"Authorization": f"Bearer {access_token}"}
resp = await client.get("/api/v1/users/me", headers=headers)
resp.raise_for_status()
return resp.json()
Logging Out and Revoking Tokens
async def logout(access_token: str):
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
headers = {"Authorization": f"Bearer {access_token}"}
await client.post("/api/v1/logout", headers=headers)
# The refresh_token cookie is cleared by the server
Customizing Token Refresh Behavior
You can modify the default token refresh implementation to match your specific security requirements.
Adjusting Token Expiration Times
Modify the configuration values in src/app/core/config.py to change the default lifetimes:
- Access tokens: Change
ACCESS_TOKEN_EXPIRE_MINUTES(default 15 minutes) - Refresh tokens: Change
REFRESH_TOKEN_EXPIRE_DAYS(default 7 days)
These values are consumed by create_access_token and create_refresh_token in src/app/core/security.py when setting the JWT exp claims.
Implementing Refresh Token Rotation
For enhanced security, implement refresh token rotation by modifying the /refresh endpoint in src/app/api/v1/login.py:
- After validating the current refresh token, immediately blacklist it using
blacklist_token - Generate both a new access token and a new refresh token
- Set the new refresh token as an HTTP-only cookie
- Return the new access token to the client
This pattern prevents replay attacks using stolen refresh tokens while maintaining the user's session.
Securing the Refresh Endpoint
The /refresh route in src/app/api/v1/login.py intentionally does not require an Authorization header because the refresh token is transmitted via an HTTP-only cookie. This design:
- Prevents XSS attacks from accessing the refresh token via JavaScript
- Requires
SecureandSameSitecookie attributes in production environments - Validates the
token_typeclaim is"refresh"to prevent access token reuse at the refresh endpoint
Summary
- Token refresh logic in the benavlabs/fastapi-boilerplate uses a dual-token system with short-lived access tokens (15 minutes) and long-lived refresh tokens (7 days) stored in HTTP-only cookies.
- Core implementation resides in
src/app/core/security.pywith token generation functions (create_access_token,create_refresh_token) and verification logic (verify_token) that checks against thetoken_blacklisttable viacrud_token_blacklist. - Refresh endpoint at
POST /api/v1/refreshinsrc/app/api/v1/login.pyextracts the refresh token from cookies, validates it, and returns a new access token without requiring user credentials. - Revocation is handled in
src/app/api/v1/logout.pyusingblacklist_tokensto insert used tokens into the database, preventing reuse after logout.
Frequently Asked Questions
How does the refresh token prevent XSS attacks?
The refresh token is stored in an HTTP-only cookie, which means JavaScript running in the browser cannot access it via document.cookie. This prevents XSS attacks from stealing the long-lived refresh token. The access token, which is short-lived and stored in JavaScript memory, presents a smaller attack window if compromised.
Can I rotate refresh tokens for additional security?
Yes, you can implement refresh token rotation by modifying the /refresh endpoint in src/app/api/v1/login.py. After validating the current refresh token, immediately blacklist it using blacklist_token, then generate both a new access token and a new refresh token. Set the new refresh token as an HTTP-only cookie and return the new access token to the client.
What happens when a refresh token expires?
When a refresh token expires (default 7 days), the verify_token function in src/app/core/security.py will reject it because the exp claim in the JWT will be in the past. The client must then re-authenticate using the /login endpoint with valid user credentials to obtain a new token pair.
How do I change the token expiration times?
Modify the configuration values in src/app/core/config.py. Change ACCESS_TOKEN_EXPIRE_MINUTES to adjust the access token lifetime (default 15 minutes) and REFRESH_TOKEN_EXPIRE_DAYS to adjust the refresh token lifetime (default 7 days). These values are consumed by create_access_token and create_refresh_token in src/app/core/security.py when setting the JWT exp claims.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →