In the previous lesson you built database sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. dependencies. Now you will use them as building blocks for authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token., the most security-critical dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. in any application. FastAPI's dependency system makes auth elegant: instead of decorators or middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it., you declare current_user = Depends(get_current_user) and the framework handles the restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources.. But elegance can mask subtle security bugs, especially in AI-generated code.
The get_current_user pattern
This is the standard auth dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. that every FastAPI app eventually implements. It follows a clear chain:
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from sqlalchemy.orm import Session
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
):
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=["HS256"])
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise credentials_exception
return userThe execution chain for a protected endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.:
Request arrives
├── oauth2_scheme extracts token from Authorization header
├── get_db creates database session
└── get_current_user runs:
1. Decode JWT token
2. Extract user ID from payload
3. Query database for user
4. Return user or raise 401The endpoint then receives a fully resolved User object, no manual tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. parsing, no raw database queries.
@app.get("/me")
def read_profile(current_user: User = Depends(get_current_user)):
return {"id": current_user.id, "email": current_user.email}OAuth2PasswordBearer explained
OAuth2PasswordBearer is not an OAuth2What is oauth?An authorization protocol that lets users grant a third-party app limited access to their account on another service without sharing their password. implementation, it is a header extractor. It does one thing: pull the tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. from the Authorization: Bearer <token> header. If the header is missing, it returns a 401.
# This line creates a dependency that extracts Bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
# The tokenUrl parameter tells Swagger UI where the login endpoint is
# It does NOT affect how tokens are extracted - it's purely for documentationThe tokenUrl argument confuses many developers (and AI). It only affects the Swagger UI auto-generated docs, it tells the "Authorize" button where to send login credentials. It has zero effect on how tokens are extracted from requests.
APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. key authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token.
Not every API uses JWTWhat is jwt?JSON Web Token - a self-contained, signed token that carries user data (like user ID and role). The server can verify it without a database lookup.. Some use simple API keys passed in headers or query parameters. FastAPI supports this through APIKeyHeader or APIKeyQuery:
from fastapi.security import APIKeyHeader
api_key_header = APIKeyHeader(name="X-API-Key")
def verify_api_key(api_key: str = Depends(api_key_header)):
if api_key not in VALID_API_KEYS:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid API key"
)
return api_key
@app.get("/data")
def get_data(api_key: str = Depends(verify_api_key)):
return {"data": "secret"}API key auth is simpler, no JWT encoding/decoding, no expiration logic. It works well for server-to-server communication. For user-facing APIs, JWT is preferred because tokens carry user identity and can expire.
Composing auth dependencies
Real apps often need more than "is the user logged in?" They need role checks, permission checks, and resource ownership checks. FastAPI's dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. system lets you compose these naturally:
def get_current_active_user(
current_user: User = Depends(get_current_user)
):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def require_admin(
current_user: User = Depends(get_current_active_user)
):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return current_user
# Regular users can access their profile
@app.get("/profile")
def profile(user: User = Depends(get_current_active_user)):
return user
# Only admins can manage users
@app.get("/admin/users")
def admin_users(admin: User = Depends(require_admin)):
return db.query(User).all()The dependency chain builds naturally: require_admin depends on get_current_active_user, which depends on get_current_user, which depends on oauth2_scheme and get_db. Each layer adds one check. If any check fails, the endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. never runs.
The IDORWhat is idor?Insecure Direct Object Reference - a vulnerability where an API lets any logged-in user access any resource by simply changing the ID in the URL. vulnerability AI generates
Here is the most dangerous pattern in AI-generated FastAPI code. It is so common that you will encounter it in nearly every AI-generated CRUDWhat is crud?Create, Read, Update, Delete - the four basic operations almost every application performs on data. application:
# AI generates this - it looks correct but has a critical security flaw
@app.get("/notes/{note_id}")
def get_note(
note_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
note = db.query(Note).filter(Note.id == note_id).first()
if not note:
raise HTTPException(status_code=404, detail="Note not found")
return noteCan you spot the flaw? The endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. checks that the user is authenticated (get_current_user), and it checks that the note exists. But it never checks whether the note belongs to the current user.
User A can request /notes/42 and get User B's note. This is an Insecure Direct Object Reference (IDOR), one of the OWASPWhat is owasp?Open Web Application Security Project - an organization that maintains a widely used list of the most critical web security risks. Top 10 vulnerabilities. The auth dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. verified identity, but the endpoint did not verify authorizationWhat is authorization?Checking what an authenticated user is allowed to do, like whether they can delete records or access admin pages. for the specific resource.
The fix:
@app.get("/notes/{note_id}")
def get_note(
note_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
note = db.query(Note).filter(
Note.id == note_id,
Note.user_id == current_user.id # ownership check
).first()
if not note:
raise HTTPException(status_code=404, detail="Note not found")
return noteNotice the fix returns 404 (not 403) when the note does not belong to the user. This is intentional, returning 403 tells an attacker that the note exists but belongs to someone else. Returning 404 reveals nothing.
Auth as dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. vs middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
FastAPI supports both patterns. When should you use which?
# Approach 1: Auth as a dependency (per-endpoint)
@app.get("/public")
def public_route():
return {"message": "Anyone can see this"}
@app.get("/private")
def private_route(user: User = Depends(get_current_user)):
return {"message": f"Hello {user.name}"}# Approach 2: Auth as middleware (global)
from starlette.middleware.base import BaseHTTPMiddleware
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if request.url.path in ["/public", "/docs", "/openapi.json"]:
return await call_next(request)
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if not token:
return JSONResponse(status_code=401, content={"detail": "Not authenticated"})
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
request.state.user_id = payload.get("sub")
except JWTError:
return JSONResponse(status_code=401, content={"detail": "Invalid token"})
return await call_next(request)| Aspect | Dependency (Depends) | Middleware |
|---|---|---|
| Scope | Per-endpoint | Global (all routes) |
| Return value | Injected into endpoint params | Attached to request.state |
| Error handling | FastAPI exception handlers | Manual JSONResponse |
| Swagger docs | Auto-documented with lock icon | Not reflected in docs |
| Flexibility | Different auth per endpoint | Same auth for all routes |
| When to use | Most apps, mixed public/private routes | APIs where every route needs auth |
The dependency approach is almost always the right choice. It is explicit (you see which endpoints are protected), composable (you can stack auth checks), and self-documenting (Swagger shows which endpoints need auth). Use middleware only when you have a blanket auth requirement across all routes and want to avoid repeating Depends() everywhere.
Extracting auth into a reusable moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions.
In production apps, auth dependencies live in their own module so endpoints stay clean:
# app/dependencies/auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
def get_current_user(token: str = Depends(oauth2_scheme), db = Depends(get_db)):
# ... token decoding and user lookup ...
return user
def get_current_active_user(user = Depends(get_current_user)):
if not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return user
def require_role(required_role: str):
def role_checker(user = Depends(get_current_active_user)):
if user.role != required_role:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return user
return role_checker# app/routes/notes.py
from app.dependencies.auth import get_current_active_user, require_role
@router.get("/notes")
def list_notes(user = Depends(get_current_active_user)):
...
@router.delete("/admin/notes/{id}")
def delete_any_note(admin = Depends(require_role("admin"))):
...The require_role function is a dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. factory, a function that returns a dependency. This pattern lets you parameterize dependencies, which is useful for role-based access controlWhat is rbac?Role-Based Access Control - assigning permissions to roles (like admin or editor), then giving users roles instead of individual permissions. where different endpoints require different roles.