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

> Learn to implement JWT authentication in FastAPI using access and refresh tokens. Secure your API with short-lived access tokens and http-only cookies for refresh tokens for enhanced security.

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

---

**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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/api/v1/login.py).

### Credential Validation

The `authenticate_user` function in [`src/app/core/security.py`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/security.py) (lines 39-52) validates the supplied username or email and password against the database before issuing any tokens.

```python
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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/security.py)) generates a JWT containing the user identifier in the `sub` claim with a short expiration window.

```python
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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/security.py)) creates a longer-lived token with `"token_type": "refresh"` embedded in the payload.

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

```

### Secure Cookie Storage

The refresh token is set as an **http-only, Secure, SameSite-Lax** cookie in [`src/app/api/v1/login.py`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/api/v1/login.py) (lines 40-44) to prevent client-side JavaScript access and CSRF attacks.

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

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

```

The `verify_token` function in [`src/app/core/security.py`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/api/v1/login.py)).

```python
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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/security.py), lines 111-128).

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

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

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

```python

# 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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/config.py). Both values are consumed by the respective creation functions in [`src/app/core/security.py`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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.