How to Implement JWT Authentication with Access and Refresh Tokens in FastAPI

Implement JWT authentication in FastAPI by issuing short-lived access tokens for API requests and long-lived refresh tokens stored in http-only cookies, using the create_access_token and create_refresh_token functions from src/app/core/security.py with token blacklisting for logout.

The benavlabs/fastapi-boilerplate repository provides a production-ready implementation of JWT authentication with access and refresh tokens. This architecture separates short-lived access credentials from long-lived refresh credentials to balance security and user experience. Understanding how to implement JWT authentication with access and refresh tokens in this boilerplate ensures your FastAPI applications follow industry best practices for stateless authentication.

Token Architecture and Security Model

The system generates two distinct token types with different lifespans and storage mechanisms. Access tokens grant temporary access to protected endpoints and expire after 30 minutes by default (settings.ACCESS_TOKEN_EXPIRE_MINUTES). Refresh tokens allow obtaining new access tokens without re-authentication and remain valid for 7 days (settings.REFRESH_TOKEN_EXPIRE_DAYS), stored exclusively in http-only cookies to prevent XSS attacks.

Each JWT payload contains three critical claims: sub (the username or email), exp (expiration timestamp), and token_type (either "access" or "refresh"). The Token Pydantic model in src/app/core/schemas.py (lines 62-66) defines the response structure for login endpoints.

Login Flow and Token Issuance

The /api/v1/login endpoint orchestrates token creation through four distinct phases defined in src/app/api/v1/login.py.

Credential Validation

The authenticate_user function in src/app/core/security.py (lines 39-52) validates the supplied username or email and password against the database before issuing any tokens.

user = await authenticate_user(form_data.username, form_data.password, db)

Access Token Generation

The create_access_token function (lines 54-62 in src/app/core/security.py) generates a JWT containing the user identifier in the sub claim with a short expiration window.

access_token = await create_access_token(
    data={"sub": user["username"]},
    expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
)

Refresh Token Generation

The create_refresh_token function (lines 65-73 in src/app/core/security.py) creates a longer-lived token with "token_type": "refresh" embedded in the payload.

refresh_token = await create_refresh_token(data={"sub": user["username"]})

The refresh token is set as an http-only, Secure, SameSite-Lax cookie in src/app/api/v1/login.py (lines 40-44) to prevent client-side JavaScript access and CSRF attacks.

response.set_cookie(
    key="refresh_token",
    value=refresh_token,
    httponly=True,
    secure=True,
    samesite="lax",
    max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60,
)

Only the access token returns in the JSON response body; the refresh token remains invisible to JavaScript clients.

Protecting Routes with Access Tokens

Protected endpoints depend on the get_current_user dependency defined in src/app/api/dependencies.py. This dependency extracts the Bearer token from the Authorization header and validates it using verify_token with TokenType.ACCESS (lines 27-31).

token_data = await verify_token(token, TokenType.ACCESS, db)

The verify_token function in src/app/core/security.py (lines 76-89) checks the token signature, expiration, type, and blacklist status. Invalid or expired tokens raise a 401 Unauthorized error immediately.

Refreshing Expired Access Tokens

When access tokens expire, clients call the /api/v1/refresh endpoint without providing credentials. The endpoint reads the http-only cookie and validates it using verify_token with TokenType.REFRESH (lines 49-58 in src/app/api/v1/login.py).

user_data = await verify_token(refresh_token, TokenType.REFRESH, db)
new_access_token = await create_access_token(data={"sub": user_data.username_or_email})

Only the new access token returns in the response body; the refresh cookie persists unchanged, allowing multiple access token refreshes until the refresh token itself expires.

Logging Out and Token Blacklisting

The logout process in src/app/api/v1/logout.py (lines 22-28) revokes both token types by adding them to a database blacklist via blacklist_tokens (implemented in src/app/core/security.py, lines 111-128).

await blacklist_tokens(access_token=access_token, refresh_token=refresh_token, db=db)
response.delete_cookie(key="refresh_token")

During subsequent verify_token calls, the system queries the blacklist table first; blacklisted tokens return None and trigger authentication failures. This mechanism prevents replay attacks using stolen tokens after logout.

Client Implementation Examples

Authenticating and Storing Tokens

import requests

payload = {
    "username": "alice@example.com",
    "password": "secret123"
}
resp = requests.post("https://api.example.com/api/v1/login", data=payload)
tokens = resp.json()
access_token = tokens["access_token"]

# Refresh token automatically stored as http-only cookie by the requests library

Making Authenticated Requests

headers = {"Authorization": f"Bearer {access_token}"}
resp = requests.get("https://api.example.com/api/v1/posts", headers=headers)
resp.raise_for_status()

Refreshing Expired Sessions


# Automatically sends the refresh_token cookie

resp = requests.post("https://api.example.com/api/v1/refresh")
new_access = resp.json()["access_token"]

Summary

  • Separate token lifespans: Access tokens expire in 30 minutes while refresh tokens last 7 days, minimizing the impact of token theft.
  • Secure cookie storage: Refresh tokens use http-only, Secure, SameSite-Lax cookies to prevent XSS and CSRF attacks.
  • Database-backed revocation: The blacklist mechanism in src/app/core/security.py enables immediate token invalidation during logout.
  • Type-safe verification: The TokenType enumeration ensures access tokens cannot be used as refresh tokens and vice versa.

Frequently Asked Questions

What is the difference between access tokens and refresh tokens in this implementation?

Access tokens are short-lived JWTs (default 30 minutes) sent in the Authorization header for API access. Refresh tokens are long-lived (default 7 days) stored in http-only cookies, used exclusively to obtain new access tokens without re-entering credentials. This separation limits the window of opportunity for attackers if an access token is intercepted.

How does the boilerplate prevent refresh token theft via XSS attacks?

The refresh token is stored in an http-only cookie set in src/app/api/v1/login.py, meaning JavaScript cannot access the token value. The cookie also uses the Secure flag (HTTPS only) and SameSite=Lax to prevent CSRF attacks during cross-site requests.

Can I customize the token expiration times?

Yes. Modify ACCESS_TOKEN_EXPIRE_MINUTES and REFRESH_TOKEN_EXPIRE_DAYS in src/app/core/config.py. Both values are consumed by the respective creation functions in src/app/core/security.py via the global settings object.

How do I protect a custom endpoint with this authentication system?

Import get_current_user from src/app/api/dependencies.py and add it as a FastAPI dependency to your route. This automatically validates the access token and injects the user dictionary into your handler function, rejecting requests with missing or invalid tokens.

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 →