How to Use OAuth2PasswordRequestForm for User Login in FastAPI

The OAuth2PasswordRequestForm in the benavlabs/fastapi-boilerplate parses username and password from form-encoded requests, enabling standard OAuth2 password grant authentication with JWT access tokens and HttpOnly refresh cookies.

The benavlabs/fastapi-boilerplate provides a production-ready authentication system built on FastAPI's OAuth2 utilities. This guide explains how the OAuth2PasswordRequestForm class handles user login requests, validates credentials against the database, and issues secure JWT tokens for session management.

Understanding the OAuth2PasswordRequestForm Login Flow

The authentication flow relies on three core components working together to process login requests and manage token lifecycles.

Core Components and File Locations

Step-by-Step Authentication Process

  1. The client sends a POST request to /api/v1/login with Content-Type: application/x-www-form-urlencoded containing username and password fields.
  2. FastAPI injects an OAuth2PasswordRequestForm instance into the endpoint as form_data, automatically parsing the request body.
  3. The endpoint calls authenticate_user(username_or_email=form_data.username, password=form_data.password, db=db) to validate credentials against the users table.
  4. If authentication fails, an UnauthorizedException (HTTP 401) is raised immediately.
  5. Upon successful validation, create_access_token and create_refresh_token generate signed JWTs using the secret key and algorithm defined in src/app/core/config.py.
  6. The refresh token is stored in an HttpOnly, Secure cookie via response.set_cookie, preventing JavaScript access.
  7. The endpoint returns a JSON body matching the Token schema, containing the access_token and token_type: "bearer".

Implementing the Login Endpoint with OAuth2PasswordRequestForm

The login route in src/app/api/v1/login.py uses FastAPI's dependency injection system to handle form parsing automatically.

from fastapi import APIRouter, Depends, Response
from fastapi.security import OAuth2PasswordRequestForm
from typing import Annotated
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.db.database import async_get_db
from app.core.security import authenticate_user, create_access_token, create_refresh_token

router = APIRouter()

@router.post("/login")
async def login(
    response: Response,
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
    db: Annotated[AsyncSession, Depends(async_get_db)]
):
    # Authenticate using the parsed form data

    user = await authenticate_user(
        username_or_email=form_data.username,
        password=form_data.password,
        db=db
    )
    
    # Generate tokens

    access_token = create_access_token(data={"sub": str(user.id)})
    refresh_token = create_refresh_token(data={"sub": str(user.id)})
    
    # Set HttpOnly cookie for refresh token

    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,
        secure=True,
        samesite="strict"
    )
    
    return {
        "access_token": access_token,
        "token_type": "bearer"
    }

The Annotated[OAuth2PasswordRequestForm, Depends()] declaration tells FastAPI to expect application/x-www-form-urlencoded data and automatically convert it into the form object.

Authenticating Users and Generating Tokens

Verifying Credentials with authenticate_user

The authenticate_user function in src/app/core/security.py handles the actual credential verification using bcrypt.

from passlib.context import CryptContext
from sqlalchemy.ext.asyncio import AsyncSession

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

async def authenticate_user(
    username_or_email: str,
    password: str,
    db: AsyncSession
):
    from app.crud.crud_users import crud_users
    
    # Query user by username or email

    user = await crud_users.get_by_username_or_email(db, username_or_email)
    
    if not user:
        return False
    
    # Verify password hash

    if not pwd_context.verify(password, user.hashed_password):
        return False
    
    return user

If the user does not exist or the password verification fails, the function returns False, triggering an HTTP 401 response in the login endpoint.

Creating JWT Access and Refresh Tokens

Token generation uses the create_access_token and create_refresh_token functions from src/app/core/security.py, configured via src/app/core/config.py.

from datetime import datetime, timedelta
from jose import jwt
from app.core.config import settings

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire, "type": "access"})
    return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

def create_refresh_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
    to_encode.update({"exp": expire, "type": "refresh"})
    return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

The settings object loads values from environment variables or .env files, including SECRET_KEY, ALGORITHM (typically HS256), and token expiry durations.

Setting HttpOnly Cookies for Refresh Tokens

The login endpoint stores the refresh token in a secure cookie to prevent XSS attacks while maintaining session continuity.

response.set_cookie(
    key="refresh_token",
    value=refresh_token,
    httponly=True,      # Prevents JavaScript access

    secure=True,        # Requires HTTPS in production

    samesite="strict",  # CSRF protection

    max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
)

This implementation follows the OAuth2 best practice of keeping long-lived refresh tokens in HttpOnly cookies while returning short-lived access tokens in the JSON response body.

Refreshing Access Tokens

The boilerplate includes a dedicated refresh endpoint in src/app/api/v1/login.py that reads the refresh token from the cookie and issues a new access token.

from app.core.security import verify_token
from app.core.schemas import Token

@router.post("/refresh", response_model=Token)
async def refresh_token(
    request: Request,
    db: Annotated[AsyncSession, Depends(async_get_db)]
):
    # Extract refresh token from HttpOnly cookie

    refresh_token = request.cookies.get("refresh_token")
    
    if not refresh_token:
        raise UnauthorizedException("Refresh token missing")
    
    # Verify token validity and type

    token_data = verify_token(refresh_token, token_type=TokenType.REFRESH, db=db)
    
    # Generate new access token

    new_access_token = create_access_token(data={"sub": token_data.sub})
    
    return {
        "access_token": new_access_token,
        "token_type": "bearer"
    }

The verify_token function checks the JWT signature, expiration, and token type against the database to ensure the token has not been revoked.

Complete Code Examples

Sending a Login Request with cURL

Test the OAuth2 password flow directly from the command line:

curl -X POST "http://localhost:8000/api/v1/login" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "username=johndoe@example.com" \
     -d "password=secret123"

The response includes the access token in JSON and sets the refresh_token cookie automatically:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}

Automating Login with Python httpx

For integration tests or client applications, use httpx to handle the form encoding and cookie management:

import httpx

def login_user(username: str, password: str) -> dict:
    """
    Authenticate using OAuth2PasswordRequestForm and return tokens.
    """
    url = "http://localhost:8000/api/v1/login"
    data = {"username": username, "password": password}
    
    with httpx.Client() as client:
        response = client.post(url, data=data)
        response.raise_for_status()
        
        # Extract JSON response (access token)

        token_data = response.json()
        access_token = token_data["access_token"]
        
        # Extract HttpOnly cookie (refresh token)

        refresh_token = response.cookies.get("refresh_token")
        
        return {
            "access_token": access_token,
            "refresh_token": refresh_token
        }

# Usage

tokens = login_user("johndoe@example.com", "secret123")
print(f"Access: {tokens['access_token'][:20]}...")

Accessing Protected Endpoints

Use the access token as a Bearer token in the Authorization header:

import httpx

def get_current_user(access_token: str):
    """
    Fetch current user data using the access token.
    """
    headers = {"Authorization": f"Bearer {access_token}"}
    url = "http://localhost:8000/api/v1/users/me"
    
    response = httpx.get(url, headers=headers)
    response.raise_for_status()
    return response.json()

# Usage

user_data = get_current_user(tokens["access_token"])
print(user_data["email"])

Refreshing Expired Access Tokens

When the access token expires, use the refresh token from the cookie to obtain a new one:

import httpx

def refresh_access_token(refresh_token: str) -> str:
    """
    Exchange refresh token for new access token.
    """
    url = "http://localhost:8000/api/v1/refresh"
    
    with httpx.Client() as client:
        # Set the refresh token in the cookie jar

        client.cookies.set("refresh_token", refresh_token)
        
        response = client.post(url)
        response.raise_for_status()
        
        return response.json()["access_token"]

# Usage when access token expires

new_token = refresh_access_token(tokens["refresh_token"])

Summary

  • OAuth2PasswordRequestForm automatically parses username and password from form-encoded POST requests in src/app/api/v1/login.py.
  • The authenticate_user function in src/app/core/security.py validates credentials against bcrypt-hashed passwords stored via src/app/crud/crud_users.py.
  • Access tokens are returned in the JSON response, while refresh tokens are stored in HttpOnly, Secure cookies to prevent XSS attacks.
  • The /api/v1/refresh endpoint reads the cookie and issues new access tokens without requiring re-authentication.
  • Configuration for token expiry and secrets resides in src/app/core/config.py, using environment variables for security.

Frequently Asked Questions

What is OAuth2PasswordRequestForm in FastAPI?

OAuth2PasswordRequestForm is a FastAPI dependency class that parses OAuth2 password grant requests. It expects application/x-www-form-urlencoded data containing username and password fields. In the benavlabs/fastapi-boilerplate, it is declared as form_data: Annotated[OAuth2PasswordRequestForm, Depends()] in the login endpoint, allowing automatic extraction of credentials without manual request body parsing.

How does the refresh token flow work in this boilerplate?

The refresh flow uses a dual-token strategy. After initial login, the client receives a short-lived access token in the JSON response and a long-lived refresh token in an HttpOnly cookie. When the access token expires, the client calls /api/v1/refresh, which extracts the cookie, verifies it via verify_token in src/app/core/security.py, and returns a new access token. This pattern keeps refresh tokens out of JavaScript-accessible storage, mitigating XSS risks.

Where are the JWT tokens configured in the boilerplate?

Token configuration resides in src/app/core/config.py, which loads environment variables for SECRET_KEY, ALGORITHM (typically HS256), ACCESS_TOKEN_EXPIRE_MINUTES, and REFRESH_TOKEN_EXPIRE_DAYS. The create_access_token and create_refresh_token functions in src/app/core/security.py import these settings to set token expiration claims and signing parameters.

How do I protect an endpoint using the access token?

Protected endpoints use the get_current_user dependency (typically defined in src/app/core/security.py or similar) which expects an Authorization: Bearer <token> header. The dependency extracts the token, validates it via verify_token, and returns the user object. To protect a route, inject this dependency into your endpoint function: async def protected_route(current_user: Annotated[User, Depends(get_current_user)]). If the token is missing or invalid, FastAPI returns a 401 Unauthorized response automatically.

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 →