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:
fromsrc.app.api.http.depsimportenforce_origin, require_csrf@router.post("/protected", dependencies=[Depends(enforce_origin), Depends(require_csrf)])asyncdefprotected_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)defextract_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=Noneforheaderinforwarded_headers:
value=request.headers.get(header)
ifvalue:
client_ip=value.split(",")[0].strip()
break# Fallback to direct client IPifnotclient_ipandrequest.client:
client_ip=request.client.hostreturnhash_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 handlerawaitjwt_verify_service.verify_jwt(
token=tokens.id_token,
expected_nonce=auth_session.nonce, # Single-use nonceexpected_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 cachingjwks_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 addressroles: "roles"# User roles/permissionsgroups: "groups"# User groupsscope: "scope"# OAuth scopesname: "name"# Full namepreferred_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 windowwindow_ms: 5000# Window size in milliseconds (5 seconds)enabled: true # Enable/disable rate limitingper_endpoint: true # Separate limits per endpointper_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:
fromsrc.app.api.http.middleware.limiterimportget_rate_limiter@router.post("/api/resource")asyncdefcreate_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")asyncdefexpensive_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 RequestsContent-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 limitsrate_limiter:
requests: 100# 100 requestswindow_ms: 60000# per minuteper_endpoint: trueper_method: trueAuthentication Endpoints (stricter limits):
# Login/callback endpointsrequests: 10window_ms: 60000# 10 attempts per minutePublic API Endpoints (more lenient):
# Read-only public datarequests: 1000window_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 managementsession_signing_secret: "${SESSION_SIGNING_SECRET}"# From secret managercsrf_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 onlyallow_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 productionformat: "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.