Skip to content

Security

FastAPI's security utilities (OAuth2PasswordBearer, HTTPBearer, HTTPBasic) are dependencies that read credentials from the request and answer 401 when they are missing. The binary reproduces those schemes with the behaviour of the FastAPI version it is tested with. The libraries applications use behind them (JWT, password hashing, signed tokens) are native too.

OAuth2 password flow (OAuth2PasswordRequestForm, OAuth2PasswordBearer), dependencies chained on the current user, and HTTP Basic. The token is the user name, as in FastAPI's tutorial; the bookshelf example shows bcrypt hashes and signed JWTs, also native. Like every example on this site, it is compiled and compared with FastAPI in CI (how).

docs_src/tutorial/security.py
import secrets
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials, OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel

router = APIRouter(prefix="/security", tags=["security"])

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="security/token")
basic = HTTPBasic()

# a real application stores password hashes (bcrypt) and signs tokens (PyJWT): see the bookshelf example
USERS = {
    "alice": {"username": "alice", "full_name": "Alice Wonderson", "password": "secret", "disabled": False},
    "bob": {"username": "bob", "full_name": "Bob Builder", "password": "builder", "disabled": True},
}


class User(BaseModel):
    username: str
    full_name: str | None = None
    disabled: bool = False


@router.post("/token")
async def login(form: Annotated[OAuth2PasswordRequestForm, Depends()]):
    user = USERS.get(form.username)
    if user is None or not secrets.compare_digest(form.password, user["password"]):
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    return {"access_token": user["username"], "token_type": "bearer"}


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
    user = USERS.get(token)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return User(**user)


async def get_active_user(user: Annotated[User, Depends(get_current_user)]) -> User:
    if user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return user


@router.get("/me")
async def read_me(user: Annotated[User, Depends(get_active_user)]) -> User:
    return user


@router.get("/basic")
async def read_basic(credentials: Annotated[HTTPBasicCredentials, Depends(basic)]):
    user = USERS.get(credentials.username)
    if user is None or not secrets.compare_digest(credentials.password, user["password"]):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return {"username": credentials.username}

What is native

  • OAuth2PasswordBearer, HTTPBearer (behaviour of FastAPI 0.142: 401 Not authenticated, WWW-Authenticate: Bearer), HTTPBasic (realm= a literal; HTTPBasicCredentials; a payload that is not base64 of ASCII user:password is a 401 even with auto_error=False, as in FastAPI), OAuth2PasswordRequestForm.
  • Behind them, see the rows of Libraries: PyJWT and python-jose (HMAC algorithms), bcrypt and pyotp, itsdangerous, cryptography's Fernet.

How the binary itself behaves in front of untrusted clients (threat model, input limits, the hybrid relay) is described in Security of the generated server. - pwdlib's PasswordHash.recommended() (Argon2id, argon2-cffi's defaults): hash(), verify(), verify_and_update() (a hash of other parameters is rehashed); an unidentified hash is UnknownHashError. Other hashers and hash(salt=...) are refused.

What stays in Python

  • Other schemes (APIKeyHeader...) and a security scheme in dependencies=[...] are refused.