# How to Implement Token Refresh Logic in FastAPI: A Complete JWT Guide

> Implement token refresh logic in FastAPI using short-lived access tokens and long-lived refresh tokens. Secure your JWT authentication with this complete guide.

- Repository: [Benav Labs/fastapi-boilerplate](https://github.com/benavlabs/fastapi-boilerplate)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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

```python
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

```python
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

```python
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

```python
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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/security.py) when setting the JWT `exp` claims.