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: Contains create_access_token, create_refresh_token, verify_token, and blacklist_token functions. This module handles JWT signing using settings.SECRET_KEY, validates token_type claims ("access" or "refresh"), and manages revocation via crud_token_blacklist.

  • src/app/api/v1/login.py: Implements the login_for_access_token endpoint for initial authentication and the /refresh endpoint 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 the blacklist_tokens function, which inserts both access and refresh tokens into the token_blacklist table using their exp claims 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_MINUTES in src/app/core/config.py) with token_type claim set to "access"
  • Refresh token: Long-lived (default 7 days based on REFRESH_TOKEN_EXPIRE_DAYS) with token_type claim 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:

  1. Extracts the refresh token from the refresh_token HTTP-only cookie
  2. Calls verify_token(refresh_token, TokenType.REFRESH, db) from src/app/core/security.py to validate:
    • Signature validity using settings.SECRET_KEY
    • token_type claim equals "refresh"
    • Token is not present in the blacklist (checked via crud_token_blacklist)
  3. Generates a new access token using create_access_token
  4. 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:

  1. The endpoint receives the access token via the Authorization: Bearer header and the refresh token from the cookie
  2. Calls blacklist_tokens from src/app/core/security.py, which:
    • Decodes each JWT to extract the exp claim
    • Creates a TokenBlacklist row in the database via crud_token_blacklist with the token's expiry
  3. Subsequent calls to verify_token check 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:

  1. After validating the current refresh token, immediately blacklist it using blacklist_token
  2. Generate both a new access token and a new refresh token
  3. Set the new refresh token as an HTTP-only cookie
  4. 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 Secure and SameSite cookie attributes in production environments
  • Validates the token_type claim 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.py with token generation functions (create_access_token, create_refresh_token) and verification logic (verify_token) that checks against the token_blacklist table via crud_token_blacklist.
  • Refresh endpoint at POST /api/v1/refresh in src/app/api/v1/login.py extracts 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.py using blacklist_tokens to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →