This document explains the security model used by FastAPI Production Template β including authentication flow, CSRF protection, session handling, and best practices for secure deployments.
It builds on the README's Authentication section and the Configuration Guide.
The template implements a Backend-for-Frontend (BFF) architecture using OpenID Connect (OIDC) with the Authorization Code + PKCE flow.
This ensures your frontend never directly handles tokens, and your backend maintains complete control over authentication and sessions.
| Concept | Description |
|---|---|
| OIDC Provider | External identity service (e.g., Keycloak, Google, Microsoft Azure AD). |
| PKCE | Proof Key for Code Exchange β prevents interception of authorization codes. |
| Nonce | Random value to bind ID tokens to the original request and prevent replay attacks. |
| State | Random CSRF token ensuring callback integrity. |
| Session Cookie | HttpOnly secure cookie identifying authenticated users. |
-
Login Request
The client (usually a browser) calls/auth/web/login, optionally specifying a provider.
The server:- Generates
state,nonce, and acode_verifier(PKCE). - Stores them in Redis or in-memory storage (ephemeral auth session with 10-minute TTL).
- Creates an
auth_session_idcookie for the client. - Redirects the user to the provider's authorization endpoint with:
response_type=code client_id=... redirect_uri=http://localhost:8000/auth/web/callback state=<random> nonce=<random> code_challenge=<PKCE-hash> code_challenge_method=S256
- Generates
-
User Authenticates
The user logs into the OIDC provider (e.g., Keycloak, Google). -
Callback Exchange
The provider redirects the user back to/auth/web/callbackwith a short-lived authorization code.The backend:
- Validates the returned
stateagainst the stored auth session. - Validates the client fingerprint to prevent session hijacking.
- Marks the auth session as "used" (single-use enforcement).
- Exchanges the
codefor tokens using the storedcode_verifier(PKCE). - Verifies the ID token signature, issuer, audience, and nonce.
- Fetches or parses user claims from the ID token or userinfo endpoint.
- Provisions or updates the user in the database (JIT provisioning).
- Issues a secure session cookie (
user_session_id) with HttpOnly and Secure flags. - Deletes the ephemeral auth session.
- Validates the returned
-
Authenticated Session
The browser now carries a secure, HttpOnly session cookie on subsequent requests. Sessions are stored server-side with client fingerprinting for additional security. -
Logout / Refresh
/auth/web/logout(POST): Invalidates the session and clears cookies. Requires CSRF token and Origin validation./auth/web/refresh(POST): Rotates the session ID and CSRF token. Requires CSRF token and Origin validation.
Sessions are server-managed using secure cookies and Redis (or in-memory fallback) for storage.
1. Auth Session (Temporary, during OIDC flow):
- Duration: 10 minutes (600 seconds)
- Cookie:
auth_session_id - Purpose: Store PKCE verifier, state, nonce during authorization
- Single-use: Marked as "used" after token exchange
2. User Session (Persistent, after authentication):
- Duration: Configurable via
app.session_max_age(default: 3600 seconds / 1 hour) - Cookie:
user_session_id - Purpose: Maintain authenticated user state
- Features: Client fingerprinting, token storage, session rotation
| Property | Value | Description |
|---|---|---|
| HttpOnly | true |
Prevents JavaScript access to cookies (XSS protection). |
| Secure | true in production |
Enforces HTTPS (disabled for localhost in development). |
| SameSite | Lax |
Allows top-level navigation (OAuth callbacks) while blocking CSRF. |
| Path | / |
Cookie available across entire domain. |
| Max-Age | Configurable | User session: app.session_max_age, Auth session: 600 seconds. |
Note: SameSite=Lax is optimal for BFF pattern OAuth flows. Use SameSite=None only if your frontend is on a different domain (requires Secure=true).
Sessions are stored in Redis (with automatic fallback to in-memory storage if Redis is unavailable):
- Auth sessions: Prefix
auth:, 10-minute TTL - User sessions: Prefix
user:, configurable TTL (default 1 hour) - Automatic cleanup: Redis handles TTL expiration automatically
- Session data: Pydantic models serialized as JSON
Storage Features:
- Automatic Redis connection health checking
- Graceful fallback to in-memory storage
- TTL-based expiration (no manual cleanup needed)
- Session listing and pattern matching capabilities
Cross-Site Request Forgery (CSRF) protection is implemented using HMAC-based tokens bound to sessions and Origin header validation.
| Mechanism | Description |
|---|---|
| CSRF Token | HMAC-based token generated using csrf_signing_secret and session ID. |
| Time-based expiration | Tokens expire after 12 hours (default) to limit replay window. |
| Session binding | CSRF tokens are cryptographically bound to specific session IDs. |
| Origin validation | Validates Origin or Referer headers against allowed origins list. |
| Header requirement | Frontend must include token in X-CSRF-Token header. |
CSRF tokens are generated using HMAC-SHA256:
token = timestamp:hmac(session_id + ":" + timestamp, csrf_signing_secret)
- Tokens are hour-based (timestamp truncated to hours)
- Maximum age: 12 hours (configurable)
- Constant-time comparison prevents timing attacks
- Get CSRF token: Call
/auth/web/meto receive auth state includingcsrf_token - Include in requests: Add
X-CSRF-Tokenheader to state-changing requests (POST, PUT, PATCH, DELETE) - Origin validation: Ensure correct
Originheader is sent by browser
# Get auth state and CSRF token
CSRF=$(curl -s -b cookies.txt http://localhost:8000/auth/web/me | jq -r '.csrf_token')
# Use token in state-changing request
curl -X POST -b cookies.txt \
-H "Origin: http://localhost:8000" \
-H "X-CSRF-Token: $CSRF" \
-H "Content-Type: application/json" \
-d '{"data":"value"}' \
http://localhost:8000/api/v1/resourceThe following endpoints require CSRF tokens:
POST /auth/web/logout- Logout userPOST /auth/web/refresh- Refresh session
Custom endpoints can add CSRF protection using:
from src.app.api.http.deps import enforce_origin, require_csrf
@router.post("/protected", dependencies=[Depends(enforce_origin), Depends(require_csrf)])
async def protected_endpoint():
return {"message": "CSRF protected"}- Missing CSRF token β
403 Forbidden: Missing CSRF token header - Invalid/expired CSRF token β
403 Forbidden: Invalid CSRF token - Missing or invalid
Originheader β403 Forbidden: Origin not allowed - Missing session cookie β
401 Unauthorized: No session found
CSRF validation is disabled in development and test environments to simplify testing. Always test with APP_ENVIRONMENT=production before deployment.
Each session is bound to a lightweight client fingerprint to prevent session hijacking and cookie theft attacks.
The fingerprint is derived from:
- User-Agent: Browser and OS identification string
- Client IP: First IP from
X-Forwarded-For,X-Real-IP,CF-Connecting-IP, or direct client IP - Hashing: SHA256 hash of combined components
- Session Creation: Fingerprint captured during login callback
- Storage: Stored in
UserSessionmodel (client_fingerprintfield) - Validation: Checked on every authenticated request
- Comparison: Constant-time comparison to prevent timing attacks
- Stolen cookie protection: Cookies used from different devices/locations are rejected
- Session binding: Ties sessions to specific client contexts
- Defense in depth: Additional layer beyond HttpOnly and Secure cookies
# Fingerprint extraction (from src/app/core/security.py)
def extract_client_fingerprint(request: Request) -> str:
"""Extract and hash client fingerprint from request."""
user_agent = request.headers.get("user-agent")
# Try forwarded headers (proxy-aware)
forwarded_headers = ["x-forwarded-for", "x-real-ip", "cf-connecting-ip"]
client_ip = None
for header in forwarded_headers:
value = request.headers.get(header)
if value:
client_ip = value.split(",")[0].strip()
break
# Fallback to direct client IP
if not client_ip and request.client:
client_ip = request.client.host
return hash_client_fingerprint(user_agent, client_ip)- NAT/Proxy networks: Users behind shared IPs may have issues if IP changes
- Mobile networks: Cellular IPs can change frequently
- User-Agent changes: Browser updates invalidate fingerprint
Mitigation: The implementation uses strict matching by default but can be configured for fuzzy matching if needed.
The template supports both session-based (BFF) and JWT-based authentication for different client types.
When a JWT bearer token is provided in the Authorization header:
| Validation Step | Description |
|---|---|
| Signature Verification | Verifies JWT signature using JWKS from provider. |
| Algorithm Check | Ensures algorithm is in allowed list (RS256, RS512, ES256, ES384, HS256). |
| Issuer Validation | Confirms token issuer matches expected provider. |
| Audience Check | Verifies token audience matches configured audiences. |
| Expiration Check | Rejects expired tokens (with clock skew tolerance of 60s). |
| Not-Before Check | Ensures token is not used before valid time. |
| Nonce Validation | Validates nonce in ID tokens (during callback only). |
During the /auth/web/callback flow, ID tokens undergo enhanced validation:
# From auth_bff_enhanced.py callback handler
await jwt_verify_service.verify_jwt(
token=tokens.id_token,
expected_nonce=auth_session.nonce, # Single-use nonce
expected_issuer=provider_cfg.issuer,
expected_audience=provider_cfg.client_id,
)Security guarantees:
- Nonce binding: Prevents ID token replay attacks
- Single-use enforcement: Nonce is deleted after first use
- Signature verification: Ensures token issued by trusted provider
- Audience validation: Confirms token intended for this application
Important: The backend never stores access tokens or ID tokens long-term.
- Auth flow: Tokens are used once during callback then discarded
- User sessions: Only session metadata (and optional refresh tokens) are stored
- Refresh tokens: Disabled by default. When enabled, they are persisted only if
oidc.refresh_tokens.persist_in_session_storeis true and are subject to a configurable lifetime cap. - JWKS caching: Public keys cached with configurable TTL to reduce provider requests
# Automatic JWKS fetching and caching
jwks_cache = JWKSCacheInMemory(
max_entries=config.jwt.jwks_cache_max_entries,
ttl_seconds=config.jwt.jwks_cache_ttl_seconds,
)Features:
- Automatic key rotation detection with forced refresh on
kidmisses - Configurable TTL + cache size per environment
- Provider-specific key caching
- Thread-safe operations
Standard OIDC claims are mapped to internal user model:
jwt:
claims:
user_id: "sub" # Subject (unique user ID)
email: "email" # Email address
roles: "roles" # User roles/permissions
groups: "groups" # User groups
scope: "scope" # OAuth scopes
name: "name" # Full name
preferred_username: "preferred_username"When a valid JWT is received from a new user:
- Identity lookup: Check if user identity exists (by
uidorissuer:subject) - Create user: If new, create user record with claims data
- Create identity: Link identity to user with issuer/subject
- Return user: Authenticate request with provisioned user
This enables zero-touch onboarding for federated authentication.
Rate limiting is implemented via Redis (with in-memory fallback) using sliding window algorithm.
Configure rate limits in config.yaml:
config:
rate_limiter:
requests: 10 # Max requests per window
window_ms: 5000 # Window size in milliseconds (5 seconds)
enabled: true # Enable/disable rate limiting
per_endpoint: true # Separate limits per endpoint
per_method: true # Separate limits per HTTP method| Setting | Description | Default |
|---|---|---|
requests |
Maximum requests allowed in window | 10 |
window_ms |
Time window in milliseconds | 5000 (5 seconds) |
enabled |
Enable rate limiting | true |
per_endpoint |
Apply limits per route path | true |
per_method |
Separate GET/POST/etc limits | true |
Rate limiting uses fastapi-limiter with Redis backend:
from src.app.api.http.middleware.limiter import get_rate_limiter
@router.post("/api/resource")
async def create_resource(
rate_limit: None = Depends(get_rate_limiter()) # Uses config defaults
):
return {"message": "created"}
# Custom rate limit for specific endpoint
@router.post("/api/expensive-operation")
async def expensive_op(
rate_limit: None = Depends(get_rate_limiter(requests=2, window_ms=60000)) # 2 req/min
):
return {"message": "processed"}When rate limit is exceeded:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"detail": "Rate limit exceeded"
}Headers (may be included depending on configuration):
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Remaining requests in current windowX-RateLimit-Reset: Timestamp when limit resets
Rate limit keys are generated based on:
- Client IP: From
X-Forwarded-For,X-Real-IP, or direct connection - Endpoint: Route path (if
per_endpoint=true) - Method: HTTP verb (if
per_method=true)
Example key: ratelimit:127.0.0.1:/api/resource:POST
- Primary: Redis (shared across multiple instances)
- Fallback: In-memory (single instance only, loses state on restart)
- Automatic detection: Falls back gracefully if Redis unavailable
Production Settings:
# Recommended production rate limits
rate_limiter:
requests: 100 # 100 requests
window_ms: 60000 # per minute
per_endpoint: true
per_method: trueAuthentication Endpoints (stricter limits):
# Login/callback endpoints
requests: 10
window_ms: 60000 # 10 attempts per minutePublic API Endpoints (more lenient):
# Read-only public data
requests: 1000
window_ms: 60000 # 1000 reads per minute- Use local Keycloak only for testing - never use development credentials in production
- Test with HTTPS using self-signed certificates if testing cross-origin flows
- Set environment variables:
APP_ENVIRONMENT=development SESSION_SIGNING_SECRET=dev-secret-change-me CSRF_SIGNING_SECRET=dev-csrf-secret-change-me
- Development mode relaxations:
- CSRF validation disabled
- Origin validation disabled
- Secure cookie flag disabled (allows HTTP)
- Detailed error messages exposed
- Set environment:
APP_ENVIRONMENT=test
- Use production-like settings but with relaxed validation for automated tests
- Seed test data with known credentials
- Mock OIDC providers to avoid external dependencies
Critical Security Checklist:
-
Environment Configuration:
APP_ENVIRONMENT=production
-
Generate Strong Secrets:
# Use infra/secrets/generate_secrets.sh ./infra/secrets/generate_secrets.sh --generate-pki # Verify secrets ./infra/secrets/generate_secrets.sh --verify
-
Use Managed Identity Provider:
- β Azure AD / Entra ID
- β Google Workspace
- β Auth0 / Okta
- β AWS Cognito
- β Development Keycloak instance
-
HTTPS Enforcement:
- Enable HTTPS on all endpoints
- Set
Securecookie flag (automatic in production) - Use TLS 1.2+ with strong cipher suites
- Implement HSTS headers
-
Secret Management:
# Use environment variables or secret management session_signing_secret: "${SESSION_SIGNING_SECRET}" # From secret manager csrf_signing_secret: "${CSRF_SIGNING_SECRET}" # From secret manager
- Rotate secrets every 90 days
- Never commit secrets to version control
- Use
.envfiles (gitignored) or external secret managers - Implement secret rotation procedures
-
CORS Configuration:
app: cors: origins: - "https://app.yourdomain.com" # Explicit origins only allow_credentials: true
- Never use wildcards (
*) with credentials - List explicit allowed origins
- Use
https://URLs only in production
- Never use wildcards (
-
Database Security:
- Use connection pooling with reasonable limits
- Enable SSL/TLS for database connections
- Use separate database users with minimal privileges
- Regular backup and recovery testing
-
Redis Security:
- Enable password authentication
- Use TLS for Redis connections in production
- Set appropriate
maxmemorylimits - Monitor Redis memory usage
-
Logging & Monitoring:
logging: level: "INFO" # Not DEBUG in production format: "json"
- Never log sensitive data (tokens, passwords, PII)
- Use structured JSON logging
- Set up alerting for authentication failures
- Monitor rate limit violations
-
Session Configuration:
app: session_max_age: 3600 # 1 hour, adjust based on security requirements
- Balance security vs user experience
- Shorter sessions for sensitive operations
- Implement session refresh for long-lived applications
Regular Tasks:
-
Weekly:
- Review authentication logs for anomalies
- Check rate limit violations
- Monitor failed login attempts
-
Monthly:
- Rotate CSRF and session signing secrets
- Review and update CORS origins
- Audit active user sessions
-
Quarterly:
- Update dependencies (
uv lock --upgrade) - Review security advisories
- Conduct security testing
- Rotate database passwords
- Update dependencies (
-
Annually:
- Renew TLS certificates
- Comprehensive security audit
- Penetration testing
- Update security documentation
If secrets are compromised:
- Immediate: Rotate all affected secrets
- Invalidate: Clear all active sessions
- Notify: Inform affected users
- Investigate: Audit logs for unauthorized access
- Document: Record incident and response
If session hijacking detected:
- Terminate: Delete affected session
- Force re-authentication: Require user to log in again
- Review: Check fingerprint validation logs
- Enhance: Consider adding additional security layers
| Layer | Responsibility |
|---|---|
| Template | Provides secure defaults, validated auth/session flows, CSRF protection, and client fingerprinting. |
| You (Developer) | Configure secrets properly, set CORS origins, manage OIDC provider credentials, implement business logic security. |
| Ops / Infra | Enforce HTTPS, handle TLS certificate management, secure Redis/PostgreSQL, implement network security, monitor and respond to incidents. |
Provided out-of-the-box:
- β OIDC Authorization Code + PKCE flow
- β Nonce validation and single-use enforcement
- β Client fingerprinting for session binding
- β HMAC-based CSRF token generation
- β Origin/Referer validation
- β Secure cookie configuration (HttpOnly, Secure, SameSite)
- β JWT signature verification with JWKS
- β Rate limiting with Redis backend
- β Session rotation on refresh
- β JIT user provisioning
- β Automatic Redis/in-memory fallback
Requires configuration:
β οΈ OIDC provider credentials (client_id,client_secret)β οΈ Session signing secret (SESSION_SIGNING_SECRET)β οΈ CSRF signing secret (CSRF_SIGNING_SECRET)β οΈ CORS allowed originsβ οΈ Rate limit thresholdsβ οΈ Session expiration times
You must:
- Generate and rotate secrets regularly
- Configure production OIDC providers (not dev Keycloak)
- Set appropriate CORS origins (no wildcards in production)
- Implement authorization logic (roles, permissions)
- Handle sensitive data appropriately
- Write secure business logic
- Test security configurations
- Review and update dependencies
You should not:
- Store tokens in localStorage (use HttpOnly cookies)
- Expose sensitive data in logs
- Use development credentials in production
- Bypass CSRF protection
- Disable security features without understanding implications
Infrastructure team must:
- Deploy with HTTPS/TLS
- Manage certificate lifecycle
- Secure Redis with password and TLS
- Secure PostgreSQL with SSL and network isolation
- Implement firewall rules
- Set up monitoring and alerting
- Perform regular security audits
- Implement backup and disaster recovery
- Monitor for security incidents
- Rotate infrastructure secrets
Authentication Flow:
- Login redirects to OIDC provider correctly
- Callback validates state parameter
- Invalid state is rejected with 400
- Client fingerprint is validated on callback
- Session cookie is set with correct attributes
- Logout clears session and cookies
- Session refresh rotates session ID
CSRF Protection:
- GET requests work without CSRF token
- POST without CSRF token returns 403
- POST with invalid CSRF token returns 403
- POST with valid CSRF token succeeds
- CSRF token expires after 12 hours
- Origin validation rejects invalid origins
Session Security:
- HttpOnly flag prevents JavaScript access
- Secure flag enforced in production
- Sessions expire after configured time
- Changed fingerprint invalidates session
- Session rotation works on refresh
Rate Limiting:
- Exceeding limit returns 429
- Rate limits reset after window
- Per-endpoint limits work independently
- Per-method limits work independently
Run security-focused tests:
# Unit tests for security functions
uv run pytest tests/unit/app/core/test_security.py -v
# Integration tests for auth flows
uv run pytest tests/integration/test_oidc_keycloak.py -v
# CSRF and Origin validation tests
uv run pytest tests/unit/app/api/test_dependencies.py -v
# Rate limiting tests
uv run pytest tests/unit/app/api/test_ratelimit.py -vDependencies:
# Check for known vulnerabilities
uv run pip-audit
# Update dependencies
uv lock --upgradeStatic Analysis:
# Type checking
uv run mypy src/
# Security linting
uv run bandit -r src/SAST (Static Application Security Testing):
- Consider integrating tools like Snyk, Semgrep, or SonarQube
- Run scans in CI/CD pipeline
- Review and remediate findings regularly
- Configuration Guide - Complete configuration reference
- OIDC Documentation - OIDC setup and provider configuration
- Secrets Management - Generate and manage secrets
- README β Authentication API - API endpoint reference
- JavaScript Client Guide - Frontend integration examples
- Python Client Guide - Backend-to-backend authentication
Key Security Features:
- π BFF Pattern: Backend controls all authentication, frontend never handles tokens
- π PKCE Flow: Prevents authorization code interception attacks
- π² Nonce Validation: Single-use tokens prevent replay attacks
- πͺ Secure Sessions: HttpOnly, Secure, SameSite cookies with server-side storage
- π΅οΈ Fingerprinting: Client binding prevents session hijacking
- π‘οΈ CSRF Protection: HMAC-based tokens with Origin validation
- β JWT Verification: Full signature and claims validation with JWKS
- π¦ Rate Limiting: Redis-based abuse prevention with sliding windows
- π JIT Provisioning: Automatic user creation from trusted identity providers
- π Session Rotation: Security through session ID rotation on refresh
Security Layers:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Layer 1: Network (HTTPS/TLS, Firewall) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 2: Authentication (OIDC, PKCE, Nonce) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 3: Session Security (Cookies, Fingerprint) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 4: Request Validation (CSRF, Origin) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 5: Authorization (JWT, Roles, Scopes) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 6: Rate Limiting (Redis, Sliding Window) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Layer 7: Monitoring (Logs, Alerts, Audit) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Configuration Checklist:
- Set
APP_ENVIRONMENT=production - Generate secrets:
./infra/secrets/generate_secrets.sh - Configure production OIDC provider
- Set explicit CORS origins (no wildcards)
- Enable HTTPS with valid TLS certificates
- Configure Redis with password and TLS
- Set appropriate rate limits
- Configure session expiration
- Set up logging and monitoring
- Test authentication flows
- Review security settings
This template provides defense in depth with multiple security layers. Each layer is designed to fail securely if bypassed, ensuring robust protection for your application.