feat: Implement TOTP-based Two-Factor Authentication

- Add django-otp and qrcode dependencies
- Create comprehensive 2FA views and templates in German
- Add 2FA setup, verification, and management interfaces
- Implement backup token system with 10 recovery codes
- Add TwoFactorMiddleware for session enforcement
- Integrate 2FA controls into user navigation menu
- Support QR code generation for authenticator apps
- Add forms for secure 2FA operations with validation
- Configure OTP settings and admin site integration

Features:
- Optional 2FA (users can enable/disable)
- TOTP compatible with Google Authenticator, Authy, etc.
- Backup codes for emergency access
- German language interface
- Session-based 2FA enforcement
- Password confirmation for sensitive operations
- Production-ready with HTTPS support
This commit is contained in:
2025-09-30 00:10:02 +02:00
parent 92b689f5e7
commit ed6a02232e
29 changed files with 41444 additions and 1 deletions

View File

@@ -8,7 +8,10 @@ import threading
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.http import HttpResponseRedirect
from django.shortcuts import reverse
from django.utils.deprecation import MiddlewareMixin
from django_otp.plugins.otp_totp.models import TOTPDevice
from stiftung.audit import get_client_ip, log_action, track_model_changes
@@ -36,6 +39,40 @@ class AuditMiddleware(MiddlewareMixin):
return response
class TwoFactorMiddleware(MiddlewareMixin):
"""
Middleware that enforces 2FA verification for users with 2FA enabled
"""
def process_request(self, request):
"""Check if user needs 2FA verification"""
# Skip if user is not authenticated
if not request.user.is_authenticated:
return None
# Skip for admin URLs and 2FA URLs themselves
if (request.path.startswith('/admin/') or
request.path.startswith('/two-factor/') or
request.path.startswith('/auth/2fa/') or
request.path == '/logout/' or
request.path.startswith('/static/') or
request.path.startswith('/media/')):
return None
# Check if user has 2FA enabled
has_2fa = TOTPDevice.objects.filter(user=request.user, confirmed=True).exists()
if has_2fa:
# Check if user has completed 2FA verification in this session
if not request.session.get('2fa_verified', False):
# Redirect to 2FA verification page
return HttpResponseRedirect(
reverse('stiftung:two_factor_verify') + f'?next={request.path}'
)
return None
def get_current_request():
"""Get the current request from thread-local storage"""
return getattr(_local, "request", None)