Sessions¶
Sessions are the boilerplate's default authentication mechanism. All built-in API routes use session auth.
Protecting Routes¶
Import the session dependencies and add them to your routes:
from typing import Annotated, Any
from fastapi import APIRouter, Depends
from ...infrastructure.auth.session.dependencies import get_current_user
router = APIRouter()
@router.get("/my-profile")
async def get_profile(
current_user: Annotated[dict[str, Any], Depends(get_current_user)],
) -> dict[str, Any]:
return {"user_id": current_user["id"], "email": current_user["email"]}
If the request doesn't have a valid session, the boilerplate returns 401 Unauthorized.
Available Dependencies¶
All from src/infrastructure/auth/session/dependencies.py.
get_current_user — Returns the authenticated user dict. Raises 401 if not authenticated.
@router.get("/dashboard")
async def dashboard(
current_user: Annotated[dict[str, Any], Depends(get_current_user)],
) -> dict[str, Any]:
return {"welcome": current_user["username"]}
get_current_superuser — Same as get_current_user, plus checks is_superuser=True. Raises 403 if not a superuser.
@router.delete("/users/{user_id}")
async def delete_user(
user_id: int,
current_user: Annotated[dict[str, Any], Depends(get_current_superuser)],
) -> None:
# Only superusers reach this code
...
get_optional_user — Returns the user dict if authenticated, None otherwise. Never raises.
@router.get("/products")
async def list_products(
current_user: Annotated[dict[str, Any] | None, Depends(get_optional_user)],
) -> list[dict[str, Any]]:
if current_user:
# Personalize for logged-in users
...
get_current_session_data — Returns the full SessionData object (id, user_id, ip, device info, timestamps). Useful for endpoints like /check-auth that need to expose session metadata.
Protecting Entire Routers¶
Apply auth to every route in a router:
router = APIRouter(
prefix="/admin",
dependencies=[Depends(get_current_superuser)],
)
@router.get("/stats")
async def stats() -> dict[str, Any]:
# Already authenticated at the router level
...
Note: router-level dependencies don't inject values into handlers. If you need the user object inside the handler, also add Depends(get_current_user) to that specific route.
How Sessions Work¶
When a user hits POST /api/v1/auth/login:
- Login rate limiter checks IP+username (
LOGIN_MAX_ATTEMPTSperLOGIN_WINDOW_MINUTES) authenticate_user(...)validates the credentialsSessionManager.create_session(...)writes a record to the configured backend (Redis by default)- A new CSRF token is generated and bound to the session
- Two cookies are set on the response:
session_id— HTTP-only, the session identifiercsrf_token— readable by JS, mirrors the CSRF token returned in the JSON body
On every subsequent request, the session dependency:
- Reads
session_idfrom cookies - Looks it up in the configured backend; rejects expired or missing sessions
- For mutating requests (POST/PUT/DELETE/PATCH), validates the CSRF token if
CSRF_ENABLED=true - Returns the user record (joined with the
Tierrelationship vialazy="selectin")
Logout (POST /api/v1/auth/logout) terminates the session record and clears the cookies.
CSRF Protection¶
Session auth ships with CSRF protection. For non-GET requests, send the CSRF token via either:
- The
csrf_tokencookie (browsers send it automatically), or - The
X-CSRF-Tokenheader (typical for JS clients)
const csrfToken = getCookie('csrf_token');
await fetch('/api/v1/users/', {
method: 'POST',
credentials: 'include', // include cookies cross-origin
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
Need a fresh token mid-session? Hit POST /api/v1/auth/refresh-csrf — it returns a new token and sets the cookie.
For dev/test environments where CSRF gets in the way, set CSRF_ENABLED=false.
Device Tracking¶
Sessions capture the IP address and parsed User-Agent fields. Inspect via the session dep:
from typing import Annotated, Any
from fastapi import Depends
from src.infrastructure.auth.session.dependencies import get_current_session_data
from src.infrastructure.auth.session.schemas import SessionData
@router.get("/my-current-session")
async def my_session(
session_data: Annotated[SessionData, Depends(get_current_session_data)],
) -> dict[str, Any]:
return {
"ip": session_data.ip_address,
"user_agent": session_data.user_agent,
"device_info": session_data.device_info, # browser, os, is_mobile, etc.
"created_at": session_data.created_at,
"last_activity": session_data.last_activity,
}
This makes it straightforward to build "your active sessions" UIs or detect suspicious activity.
Login Rate Limiting¶
Failed login attempts are tracked per IP+username. After LOGIN_MAX_ATTEMPTS failures within LOGIN_WINDOW_MINUTES, further attempts on /api/v1/auth/login are blocked.
This happens automatically in the login route — you don't need to wire it up. The defaults (5 attempts in 15 minutes) are conservative; tune per your threat model.
Session Limits¶
Per-user concurrent session count is capped by MAX_SESSIONS_PER_USER (default 5). When a user logs in beyond this cap, the oldest session is terminated.
Storage Backends¶
Sessions are stored server-side. Configure via SESSION_BACKEND:
| Value | When to use |
|---|---|
redis (default) |
Production. Supports key expiration, pattern scans for cleanup, persists across restarts |
memcached |
Production alternative — choose based on what your infrastructure already runs |
memory |
Tests only. Cleared on restart, not safe for multi-process deploys |
Storage backends live in src/infrastructure/auth/session/backends/.
Configuration¶
# Backend
SESSION_BACKEND=redis
# Lifetime
SESSION_TIMEOUT_MINUTES=30 # inactive sessions expire
SESSION_CLEANUP_INTERVAL_MINUTES=15 # how often the storage backend sweeps expired entries
SESSION_COOKIE_MAX_AGE=86400 # 1 day — total cookie lifetime
# Per-user cap
MAX_SESSIONS_PER_USER=5
# Cookie security (HTTPS only)
SESSION_SECURE_COOKIES=true
# CSRF
CSRF_ENABLED=true
# Login rate limiting
LOGIN_MAX_ATTEMPTS=5
LOGIN_WINDOW_MINUTES=15
For development you'll typically set SESSION_SECURE_COOKIES=false and CSRF_ENABLED=false so cookies work over plain HTTP and curl/Postman aren't blocked. Re-enable both for staging and production.
Login & Logout Flow¶
Login¶
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=your_admin_password" \
-c cookies.txt
Response:
The HTTP-only session_id cookie is now in cookies.txt. The CSRF token is also set as a cookie and returned in the body so JS clients can store it (browsers can't read HTTP-only cookies).
Authenticated Request¶
For mutating requests, add the CSRF header:
curl -X POST http://localhost:8000/api/v1/users/ \
-b cookies.txt \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: <token-from-login-response>" \
-d '{"name": "...", "username": "...", "email": "...", "password": "..."}'
Refresh CSRF Token¶
Logout¶
Terminates the session and clears the cookies.
Key Files¶
| Component | Location |
|---|---|
| Dependencies | backend/src/infrastructure/auth/session/dependencies.py |
| Session manager | backend/src/infrastructure/auth/session/manager.py |
| Storage backends | backend/src/infrastructure/auth/session/backends/ |
| Schemas | backend/src/infrastructure/auth/session/schemas.py |
| Login/logout routes | backend/src/infrastructure/auth/routes.py |
| Auth settings | backend/src/infrastructure/config/settings.py (AuthSettings) |