5.0 KiB
Plan 4 — Security
Items covered: #1 (no authentication), #11 (no rate limiting), #14 (unbounded limit parameter)
Context
You are working on a FastAPI backend for a baby monitor / sleep tracker app. The codebase lives under app/. This plan adds three layers of security enforcement. Do them in order — auth first, then rate limiting, then the parameter bound. After each change, re-read the modified files to verify correctness, and run the tests.
Tasks
1. Add API key authentication to all endpoints (app/)
Step 1 — Read the relevant files first
Read app/config.py, app/main.py, and all router files under app/routers/ before making any changes. Understand how the app is structured and how dependencies are currently shared.
Step 2 — Add API_KEY to settings
In app/config.py, add an API_KEY field to the Settings model:
API_KEY: str = ""
The empty default means auth is effectively disabled unless the env var is set. This allows existing tests to keep working without modification (see Step 5 for the alternative if you prefer strict-by-default).
Step 3 — Create the auth dependency
Create a new file app/dependencies.py:
from fastapi import Security, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.config import settings
bearer = HTTPBearer(auto_error=False)
def verify_api_key(creds: HTTPAuthorizationCredentials = Security(bearer)):
if not settings.API_KEY:
# API_KEY not configured — auth is disabled (development mode)
return
if creds is None or creds.credentials != settings.API_KEY:
raise HTTPException(status_code=401, detail="Unauthorized")
Step 4 — Apply the dependency to all routers
In each file under app/routers/, add verify_api_key as a dependency on the router so every endpoint in that file is protected:
from fastapi import APIRouter, Depends
from app.dependencies import verify_api_key
router = APIRouter(dependencies=[Depends(verify_api_key)])
Apply this to every router file. Do not add it per-endpoint — the router-level dependency is cleaner and ensures nothing is accidentally missed.
Step 5 — Update the tests
Any test that calls a protected endpoint now needs an Authorization: Bearer <key> header — OR the API_KEY setting can be left empty (the default), which disables the check. Verify which approach your test setup uses. If tests use a test client with a settings override, ensure API_KEY is either empty (disables check) or consistently set to a test value and the header is passed.
Run the tests:
pytest
Fix any 401 failures before proceeding.
2. Add rate limiting (app/main.py and app/routers/)
Step 1 — Install slowapi
uv add slowapi
Or add slowapi to pyproject.toml dependencies and run uv sync.
Step 2 — Configure the limiter in main.py
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
Step 3 — Apply limits to endpoints
Add rate limit decorators to the endpoints most at risk:
- All write endpoints (
POST,PUT,PATCH,DELETE):@limiter.limit("10/minute") - The timetable endpoint (expensive):
@limiter.limit("30/minute") - Other read endpoints:
@limiter.limit("60/minute")
Example:
from app.main import limiter
from fastapi import Request
@router.post("/sessions")
@limiter.limit("10/minute")
async def create_session(request: Request, ...):
...
Note: slowapi requires the request: Request parameter to be present in the function signature for the decorator to work. Add it to any endpoint that doesn't already have it. It does not need to be used inside the function body.
Step 4 — Verify
Run the tests:
pytest
Rate limiting may interfere with tests that call endpoints in a tight loop. If so, configure the limiter to be disabled in the test environment (e.g. check for a TESTING=true env var and set enabled=False on the limiter).
3. Bound the limit query parameter (app/routers/sessions.py)
Step 1 — Read the file first
Find the get_sessions endpoint (or equivalent) and locate the limit parameter.
Step 2 — Add the bound
Replace the bare limit: int = 200 with a constrained Query:
from fastapi import Query
async def get_sessions(
limit: int = Query(default=200, ge=1, le=1000),
...
):
This rejects any limit value below 1 or above 1000 with a FastAPI 422 validation error automatically — no extra code needed.
Step 3 — Verify
Run the full test suite one final time:
pytest
All tests must pass before declaring this plan complete. Do not move on to Plan 5 until the suite is green.