feat(auth): implement TOTP two-factor authentication with enrollment and verification

Adds TOTP (Time-based One-Time Password) two-factor authentication support including:
- New TOTP service with secret generation, QR code provisioning, and code verification
- New auth endpoints for enrollment, verification, status, and backup code management
- New TOTP authentication method type and user methods for TOTP management
- Backup codes generation and verification for account recovery
- Updated OIDC endpoints with timezone-aware datetime handling and RFC-compliant responses
- Added "roles" scope support for OIDC userinfo and ID tokens
- New pyotp dependency for TOTP operations
- Comprehensive unit tests for TOTP service
This commit is contained in:
2026-01-14 18:06:17 +10:30
parent 977abf66df
commit cfd79190ee
26 changed files with 2176 additions and 263 deletions
+2
View File
@@ -26,6 +26,7 @@ def setup_cors(app):
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Request-ID"
response.headers["Access-Control-Max-Age"] = "3600"
response.headers["Cache-Control"] = "no-cache, no-store"
return response
elif origin and origin in cors_origins:
response = make_response("", 204)
@@ -34,6 +35,7 @@ def setup_cors(app):
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Request-ID"
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Max-Age"] = "3600"
response.headers["Cache-Control"] = "no-cache, no-store"
return response
@app.after_request
+11
View File
@@ -51,4 +51,15 @@ class SecurityHeadersMiddleware:
"geolocation=(), microphone=(), camera=()"
)
# Cache-Control: Allow OIDC endpoints to set their own Cache-Control
# Only set no-cache for API responses that haven't set their own cache headers
if "Cache-Control" not in response.headers:
# Check if this is a JSON API response (shouldn't be cached)
content_type = response.headers.get("Content-Type", "")
if "application/json" in content_type:
response.headers["Cache-Control"] = "no-cache, no-store"
elif "text/html" not in content_type:
# For non-HTML responses, add Pragma for HTTP/1.0 compatibility
response.headers["Pragma"] = "no-cache"
return response