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
OAuth2PasswordRequestForm– Parses form-encoded credentials insrc/app/api/v1/login.pyauthenticate_user– Verifies bcrypt-hashed passwords insrc/app/core/security.pycreate_access_tokenandcreate_refresh_token– Generate signed JWTs insrc/app/core/security.pyTokenschema – Defines the response model insrc/app/core/schemas.pycrud_users.py– Database queries for user lookup insrc/app/crud/crud_users.py
Step-by-Step Authentication Process
- The client sends a POST request to
/api/v1/loginwithContent-Type: application/x-www-form-urlencodedcontainingusernameandpasswordfields. - FastAPI injects an
OAuth2PasswordRequestForminstance into the endpoint asform_data, automatically parsing the request body. - The endpoint calls
authenticate_user(username_or_email=form_data.username, password=form_data.password, db=db)to validate credentials against theuserstable. - If authentication fails, an
UnauthorizedException(HTTP 401) is raised immediately. - Upon successful validation,
create_access_tokenandcreate_refresh_tokengenerate signed JWTs using the secret key and algorithm defined insrc/app/core/config.py. - The refresh token is stored in an HttpOnly, Secure cookie via
response.set_cookie, preventing JavaScript access. - The endpoint returns a JSON body matching the
Tokenschema, containing theaccess_tokenandtoken_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
OAuth2PasswordRequestFormautomatically parsesusernameandpasswordfrom form-encoded POST requests insrc/app/api/v1/login.py.- The
authenticate_userfunction insrc/app/core/security.pyvalidates credentials against bcrypt-hashed passwords stored viasrc/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/refreshendpoint 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →