Protecting Endpoints
Generating tokens is half the work; the other half is verifying them on every authenticated request and pulling the right User row out of the database.
FastAPI's OAuth2PasswordBearer plus a small dependency function plus a CurrentUserDep alias lets us add just one extra parameter on a route handler.
OAuth2PasswordBearer: the Security Scheme
OAuth2PasswordBearer is a FastAPI utility that tells the framework "expect a token in the Authorization: Bearer <token> header." It has two side effects:
- Header extraction. FastAPI pulls the token out of the header on protected routes; if there's no token, the request gets rejected with a 401 before the handler runs.
- Docs UI integration. The interactive
/docspage sprouts an "Authorize" button that prompts for an email and password, posts them to the configuredtokenUrl, and stores the returned token for subsequent requests.
from fastapi.security import OAuth2PasswordBearer
security_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
tokenUrl="/auth/token" is the endpoint the docs UI POSTs the credentials to. We'll build that endpoint when we wire up the auth router.
get_current_user: Token to User
Verifying a token means three things in sequence: decode it with the signing key, pull the sub claim out, and look up the user in the database. Any failure along the way collapses to the same 401.
from typing import Annotated
import jwt
from fastapi import Depends, HTTPException, status
from jwt.exceptions import InvalidTokenError
from sqlmodel import Session
from . import models
from .config import get_settings
from .database import get_session
def get_current_user(
token: Annotated[str, Depends(security_scheme)],
session: Annotated[Session, Depends(get_session)],
) -> models.User:
settings = get_settings()
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials.",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token,
settings.jwt_secret_key.get_secret_value(),
algorithms=[JWT_ALGORITHM],
)
user_id_str: str | None = payload.get("sub")
if user_id_str is None:
raise credentials_exception
user_id = int(user_id_str)
except (InvalidTokenError, ValueError) as exc:
raise credentials_exception from exc
user = session.get(models.User, user_id)
if user is None or not user.is_active:
raise credentials_exception
return user
CurrentUserDep
SessionDep and ProjectDep already showed the Annotated shorthand. CurrentUserDep follows the same pattern:
from typing import Annotated
from fastapi import Depends
from .models import User
from .security import get_current_user
CurrentUserDep = Annotated[User, Depends(get_current_user)]
A handler that requires authentication adds CurrentUserDep to its signature:
@router.post(
"/", response_model=ProjectRead, status_code=status.HTTP_201_CREATED
)
def create_project(
payload: ProjectCreate,
session: SessionDep,
current_user: CurrentUserDep,
) -> Any:
return crud.create_project(session, payload)
Now, current_user is a guaranteed-valid User.
If it wasnt't, FastAPI would have returned a 401 before the handler started.
The same CurrentUserDep goes on every route that should require auth: the project create/update/delete handlers, the task create/update/delete handlers, and GET /auth/me.
Info
Reads stay public.
Trying It in /docs
OAuth2PasswordBearer(tokenUrl="/auth/token") makes the docs UI auth-aware. The flow on http://localhost:8000/docs:
- Click the Authorize button at the top of the page.
- Enter the user's email as the username and the user's password as the password. Leave client ID and client secret blank.
- Click Authorize, then Close.
- The docs UI now attaches
Authorization: Bearer <token>to every subsequent request.
Hitting a protected endpoint without authorizing first returns the 401 from get_current_user. Hitting it after authorizing returns the real response.
Security linting
Once auth code is in the project, Ruff's S rule group (the flake8-bandit security checks) starts paying off. It catches hardcoded passwords, weak hash usage, and unsafe deserialization, among others. To run it without scanning tests (which intentionally use insecure values like testpassword):
uv run ruff check --select S --exclude tests/ .
The deployment chapter wires this into the project's regular checks.