Skip to content

Authentication Middleware for FastAPI — Dependency-Based Auth for Python APIs

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Authentication Middleware for FastAPI. We cover key concepts, practical examples, and best practices to help you master this topic.

FastAPI authentication uses Dependency Injection to create reusable auth components, where OAuth2PasswordBearer extracts tokens from requests and custom dependencies validate them and return the authenticated user.

What You'll Learn

FastAPI OAuth2PasswordBearer, JWT verification dependencies, role and scope checking dependencies, composing auth dependencies, and testing authenticated endpoints.

Why It Matters

FastAPI's dependency injection system makes authentication composable and testable. Auth logic is defined once as a dependency and reused across all protected endpoints with automatic OpenAPI documentation.

Real-World Use

FastAPI is used by Netflix, Uber, and Microsoft for high-performance APIs. Durga Antivirus Pro uses FastAPI for its threat intelligence API, with dependency-based JWT authentication and role checking.

Code Example: JWT Authentication Dependency

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import BaseModel
import os

app = FastAPI()

SECRET = os.environ.get("JWT_SECRET", "dev-secret")
ALGORITHM = "HS256"

oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="/api/auth/login",
    scheme_name="JWT Bearer Token"
)

class User(BaseModel):
    id: str
    email: str
    roles: list[str] = []
    scopes: list[str] = []

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """Verify JWT and return the current user."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"}
    )

    try:
        payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
        user_id: str = payload.get("sub")
        if user_id is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    return User(
        id=user_id,
        email=payload.get("email", ""),
        roles=payload.get("roles", []),
        scopes=payload.get("scope", "").split()
    )

@app.get("/api/v1/profile")
async def read_profile(current_user: User = Depends(get_current_user)):
    """Protected endpoint — requires valid JWT."""
    return {
        "user_id": current_user.id,
        "email": current_user.email,
        "roles": current_user.roles
    }

Code Example: Role and Scope Dependencies

from functools import wraps

# Role-checking dependency
class RoleChecker:
    def __init__(self, allowed_roles: list[str]):
        self.allowed_roles = allowed_roles

    async def __call__(self, current_user: User = Depends(get_current_user)):
        if not any(role in current_user.roles for role in self.allowed_roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Requires one of roles: {self.allowed_roles}"
            )
        return current_user

# Scope-checking dependency
class ScopeChecker:
    def __init__(self, required_scope: str):
        self.required_scope = required_scope

    async def __call__(self, current_user: User = Depends(get_current_user)):
        if self.required_scope not in current_user.scopes:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Requires scope: {self.required_scope}"
            )
        return current_user

# Pre-built checkers
require_admin = RoleChecker(["admin"])
require_analyst = RoleChecker(["analyst", "admin"])
require_threat_read = ScopeChecker("threat:read")
require_threat_write = ScopeChecker("threat:write")

# Route usage
@app.get("/api/v1/threats")
async def list_threats(
    user: User = Depends(get_current_user),
    _: User = Depends(require_analyst),
    __: User = Depends(require_threat_read)
):
    return {"threats": [], "user": user.id}

@app.post("/api/v1/threats")
async def create_threat(
    user: User = Depends(get_current_user),
    _: User = Depends(require_admin),
    __: User = Depends(require_threat_write)
):
    return {"status": "created", "by": user.id}

Code Example: Multiple Auth Strategies with FastAPI

from fastapi.security import APIKeyHeader

# API Key authentication
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)

async def get_current_user_optional(
    token: str = Depends(oauth2_scheme),
    api_key: str = Depends(api_key_header)
) -> User | None:
    """Try multiple auth methods, return None if none work."""
    # Try JWT first
    if token:
        try:
            payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
            return User(
                id=payload.get("sub"),
                roles=payload.get("roles", []),
                scopes=payload.get("scope", "").split()
            )
        except JWTError:
            pass

    # Try API key
    if api_key:
        key_data = await validate_api_key(api_key)
        if key_data:
            return User(
                id=key_data["service"],
                roles=["service"],
                scopes=key_data["scopes"]
            )

    return None

async def get_current_user_required(
    user: User | None = Depends(get_current_user_optional)
) -> User:
    """Require authentication — raise 401 if not authenticated."""
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authentication required"
        )
    return user

# Routes
@app.get("/api/v1/public")
async def public_endpoint():
    return {"message": "Public — no auth required"}

@app.get("/api/v1/protected")
async def protected_endpoint(user: User = Depends(get_current_user_required)):
    return {"message": "Authenticated", "user": user.id}

@app.get("/api/v1/optional")
async def optional_endpoint(user: User | None = Depends(get_current_user_optional)):
    if user:
        return {"message": "Authenticated", "user": user.id}
    return {"message": "Not authenticated"}

Common Mistakes

1. Not Using auto_error=False for Optional Auth

If auto_error is True (default), missing tokens cause a 403 before your code runs. Use auto_error=False for optional auth and handle the None case.

2. Raising 403 Instead of 401

A missing or invalid token is a 401 (Unauthenticated). Lack of permission after authentication is a 403 (Forbidden). FastAPI defaults to 403 for OAuth2; override to 401.

3. JWT Payload Without Type Hints

FastAPI's dependency injection works best with Pydantic models. Return a User model from auth dependencies instead of raw dictionaries.

4. Not Handling JWTError Broadly

Catch JWTError instead of individual exceptions (ExpiredSignatureError, InvalidTokenError). JWTError covers all Jose library exceptions.

5. Missing WWW-Authenticate Headers

When returning 401, FastAPI's OAuth2PasswordBearer automatically adds the WWW-Authenticate header. Ensure custom 401 responses also include it.

Practice Questions

  1. How does FastAPI's OAuth2PasswordBearer extract tokens?
  2. Why are dependencies better than middleware in FastAPI?
  3. How do you create a role-checking dependency?
  4. What is the difference between auto_error=True and auto_error=False?
  5. How does FastAPI document auth requirements in OpenAPI?

Answers:

  1. OAuth2PasswordBearer reads the Authorization header, extracts the Bearer token, and passes it to the dependency function. It also adds the security scheme to OpenAPI.
  2. Dependencies are type-checked, testable in isolation, and can be composed. Middleware runs globally and is harder to test. Dependencies integrate with FastAPI's OpenAPI generation.
  3. Create a class with call that takes current_user as a dependency. Check roles inside call and raise HTTPException(403) if the check fails.
  4. auto_error=True raises a 403 on missing token. auto_error=False returns None, allowing your code to handle the unauthenticated case with custom logic.
  5. OAuth2PasswordBearer adds the security scheme to the OpenAPI spec. Each route with Depends(oauth2_scheme) automatically shows a lock icon in Swagger UI.

Challenge: Build a FastAPI application with dependency-based JWT authentication, role checking, scope checking, API key support as a fallback, and automatic OpenAPI documentation of security schemes.

FAQ

Does FastAPI support both JWT and session auth?

Yes. Use OAuth2PasswordBearer for JWT and create a custom dependency that reads session cookies and validates the session.

How do I test FastAPI auth dependencies?

Use TestClient with headers set: client.get('/protected', headers={'Authorization': 'Bearer test-token'}). Override the dependency with app.dependency_overrides.

Can dependencies be async?

Yes. FastAPI supports async dependencies. Use async def and await for database lookups or external API calls during authentication.

How do I debug auth issues in FastAPI?

Check the 401/403 response body for detail messages. Use middleware to log auth attempts. FastAPI's debug mode shows traceback for 500 errors.

Should I use middleware or dependencies for auth?

Use dependencies. They integrate with FastAPI's OpenAPI, are testable, composable, and type-safe. Middleware is better for cross-cutting concerns like CORS or request logging.

Mini Project

Build a FastAPI application with JWT authentication dependency, role and scope checkers, optional auth for public endpoints, API key fallback, and a complete test suite using TestClient with dependency overrides.

What's Next

Now learn about Authentication Logging and Audit for tracking authentication events for security and Compliance.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro