Skip to main content

FastAPI Documentation Security

Introduction

Security is a critical aspect of any web application, and APIs are no exception. FastAPI provides robust tools for implementing various security schemes directly through its documentation system. This integration allows you to simultaneously secure your API endpoints and document the security requirements for client developers.

In this guide, we'll explore how FastAPI approaches security, focusing on how security schemes are documented and implemented. We'll cover authentication methods like API keys, OAuth2 (with Password, Bearer tokens), and more advanced scenarios.

Security Basics in FastAPI

FastAPI's security system is built on top of its dependency injection system and integrates seamlessly with OpenAPI documentation. When you implement security in FastAPI, two important things happen:

  1. Your API endpoints become protected from unauthorized access
  2. The security requirements are automatically documented in your API documentation

Let's start with the simplest form of authentication: API Keys.

API Key Authentication

API key authentication is one of the simplest security methods. It involves passing a key (token) through a header, query parameter, or cookie.

Implementing API Key Security

python
from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security.api_key import APIKeyHeader, APIKey

app = FastAPI()

API_KEY = "your_secret_api_key"
API_KEY_NAME = "X-API-Key"

api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True)

async def get_api_key(api_key_header: str = Security(api_key_header)):
if api_key_header == API_KEY:
return api_key_header
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate API key"
)

@app.get("/protected-route/", summary="Protected API Key Route")
async def protected_route(api_key: APIKey = Depends(get_api_key)):
return {"message": "You have access to the protected route"}

When you run this application and visit the /docs page, you'll see a security button that allows users to input their API key:

API Key Authorization UI

How It Works

  1. We define an APIKeyHeader that specifies which header will contain our API key
  2. We create a dependency function get_api_key that validates the provided key
  3. We protect routes by adding this dependency
  4. FastAPI automatically generates documentation showing the security requirement

OAuth2 with Password and Bearer

OAuth2 is a more sophisticated security protocol. FastAPI provides excellent support for OAuth2 with several flows. Let's implement a password flow with bearer tokens:

python
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel

# Secret key and algorithm for JWT token
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# Mock user database
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}

# Models
class Token(BaseModel):
access_token: str
token_type: str

class TokenData(BaseModel):
username: Optional[str] = None

class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None

class UserInDB(User):
hashed_password: str

# Password context for hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

app = FastAPI()

# Helper functions
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)

def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)

def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

# Token endpoint for authentication
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

# Protected route
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user

How OAuth2 Password Flow Works

  1. Users send their username and password to the /token endpoint
  2. If valid, the endpoint returns an access token
  3. For subsequent requests, users include this token in the Authorization header
  4. FastAPI validates the token and identifies the user

When you visit the /docs page, you'll see an "Authorize" button that allows users to authenticate:

OAuth2 with Password Authorization UI

Scopes and Permissions

For more advanced security needs, FastAPI supports OAuth2 scopes, which allow you to define granular permissions.

python
from fastapi import Depends, FastAPI, HTTPException, Security
from fastapi.security import (
OAuth2PasswordBearer,
OAuth2PasswordRequestForm,
SecurityScopes,
)
from pydantic import BaseModel, ValidationError
from jose import JWTError, jwt
from typing import List, Optional

# ... (assume the same imports and setup from previous example)

# Define our security scopes
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"users:read": "Read information about users",
"users:write": "Create or modify users",
"items:read": "Read items",
"items:write": "Create or modify items",
},
)

# Extended token model with scopes
class TokenData(BaseModel):
username: Optional[str] = None
scopes: List[str] = []

# Function to create token with scopes
def create_access_token(data: dict, scopes: List[str], expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire, "scopes": scopes})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

async def get_current_user(
security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)
):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = "Bearer"

credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)

try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(username=username, scopes=token_scopes)
except (JWTError, ValidationError):
raise credentials_exception

user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception

# Verify required scopes
for scope in security_scopes.scopes:
if scope not in token_data.scopes:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Not enough permissions. Required: {scope}",
headers={"WWW-Authenticate": authenticate_value},
)
return user

# Token endpoint with scopes
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# Use the requested scopes from the form
access_token = create_access_token(
data={"sub": user.username},
scopes=form_data.scopes,
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

# Protected routes with different scopes
@app.get("/users/me/", response_model=User)
async def read_users_me(
current_user: User = Security(get_current_user, scopes=["users:read"])
):
return current_user

@app.get("/items/")
async def read_items(
current_user: User = Security(get_current_user, scopes=["items:read"])
):
return [{"item_id": "1", "owner": current_user.username}]

@app.post("/items/")
async def create_item(
current_user: User = Security(get_current_user, scopes=["items:write"])
):
return {"result": "Item created", "owner": current_user.username}

How Scopes Work

  1. We define available scopes when creating the OAuth2PasswordBearer
  2. When users authenticate, they request specific scopes
  3. We include these scopes in the JWT token
  4. Each protected endpoint specifies required scopes using Security(dependency, scopes=[...])
  5. FastAPI validates that the user has the necessary permissions

Adding HTTP Basic Authentication

For simpler scenarios or legacy systems, HTTP Basic Authentication might be required:

python
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import secrets

app = FastAPI()

security = HTTPBasic()

# Hard-coded credentials (in a real app, you'd use a database)
USERNAME = "admin"
PASSWORD = "secret"

@app.get("/basic-auth/")
def get_basic_auth_info(credentials: HTTPBasicCredentials = Depends(security)):
correct_username = secrets.compare_digest(credentials.username, USERNAME)
correct_password = secrets.compare_digest(credentials.password, PASSWORD)

if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)

return {"username": credentials.username}

Real-world Application: Multi-layer Security

In real applications, you might need multiple security mechanisms working together. Here's an example of an API with different security approaches:

python
from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import OAuth2PasswordBearer, APIKeyHeader

app = FastAPI()

# OAuth2 setup (assuming we have all the functions from before)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# API Key setup for system-to-system interactions
API_KEY = "system_integration_key"
API_KEY_NAME = "X-System-API-Key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)

# Dependencies
async def get_current_user(token: str = Depends(oauth2_scheme)):
# JWT validation code here (from previous examples)
pass

async def validate_api_key(api_key: str = Security(api_key_header)):
if api_key == API_KEY:
return {"system": "integration"}
return None

# Combined security dependency
async def get_authorization(
user: dict = Depends(get_current_user, use_cache=False),
system: dict = Depends(validate_api_key, use_cache=False),
):
if user is None and system is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
)
if user:
return {"type": "user", "user": user}
return {"type": "system", "system": system}

# Route that can be accessed by either user or system
@app.get("/dual-secured/")
async def get_secured_data(auth: dict = Depends(get_authorization)):
if auth["type"] == "user":
return {"message": f"Hello user {auth['user'].username}"}
return {"message": "Hello system integration"}

This example shows how to create an endpoint that accepts either OAuth2 token authentication (for users) or API key authentication (for system integrations).

Security Best Practices

When implementing security with FastAPI, keep these best practices in mind:

  1. Use HTTPS: Always use HTTPS in production to encrypt data in transit
  2. Store Secrets Securely: Never hardcode secrets in your source code; use environment variables
  3. Proper Password Handling: Always hash passwords using strong algorithms like bcrypt
  4. Token Expiration: Set reasonable expiration times for tokens
  5. Input Validation: Validate all inputs to prevent injection attacks
  6. Rate Limiting: Implement rate limiting to prevent brute force attacks
  7. CORS Configuration: Set appropriate CORS policies

Example for setting up CORS in FastAPI:

python
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["https://trusted-app.com"], # List of allowed origins
allow_credentials=True,
allow_methods=["*"], # Allow all methods
allow_headers=["*"], # Allow all headers
)

Summary

In this comprehensive guide, we've explored how to implement and document security in FastAPI applications:

  1. API Key Authentication: Simple token-based access using headers
  2. OAuth2 with Password Flow: Username/password authentication with JWT tokens
  3. Scopes and Permissions: Granular access control for different operations
  4. HTTP Basic Authentication: Simple username/password authentication
  5. Multi-layer Security: Combining different security mechanisms
  6. Best Practices: Key considerations for secure API development

FastAPI's integrated security system not only protects your endpoints but also automatically generates comprehensive documentation, making it clear to API consumers how to authenticate properly.

Additional Resources

Exercises

  1. Implement an OAuth2 authentication system with refresh tokens
  2. Create an API with role-based access control (admin, regular user, etc.)
  3. Set up an API that uses different security schemes for different endpoints
  4. Implement a rate-limiting system to prevent abuse of your authentication endpoints
  5. Create a multi-tenant API where authentication includes tenant identification


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)