| """ |
| Security middleware — rate limiting and session fingerprint validation. |
| """ |
| from django.http import JsonResponse |
| from django.utils.deprecation import MiddlewareMixin |
|
|
| from apps.security.services.rate_limit_service import ( |
| build_login_key, |
| check_rate_limit, |
| ) |
| from core.exceptions import RateLimitError |
| from core.utils.ip import get_client_ip, get_device_fingerprint |
|
|
|
|
| class RateLimitMiddleware(MiddlewareMixin): |
| """ |
| Rate-limit login attempts by IP address. |
| |
| Only applies to POST requests on the login URL. |
| """ |
|
|
| LOGIN_PATHS = ["/accounts/login/"] |
|
|
| def process_request(self, request): |
| if request.method != "POST": |
| return None |
|
|
| if request.path not in self.LOGIN_PATHS: |
| return None |
|
|
| ip = get_client_ip(request) |
| key = build_login_key(ip) |
|
|
| try: |
| check_rate_limit(key) |
| except RateLimitError as e: |
| return JsonResponse( |
| {"error": str(e)}, |
| status=429, |
| ) |
|
|
| return None |
|
|
|
|
| class SessionFingerprintMiddleware(MiddlewareMixin): |
| """ |
| Bind sessions to device fingerprint. |
| |
| Validates that the current request's fingerprint matches |
| the session fingerprint. Logs a security event on mismatch. |
| """ |
|
|
| def process_request(self, request): |
| if not hasattr(request, "user") or not request.user.is_authenticated: |
| return None |
|
|
| current_fingerprint = get_device_fingerprint(request) |
| stored_fingerprint = request.session.get("device_fingerprint") |
|
|
| if not stored_fingerprint: |
| |
| request.session["device_fingerprint"] = current_fingerprint |
| elif stored_fingerprint != current_fingerprint: |
| |
| from django.contrib.auth import logout |
| from django.core.exceptions import PermissionDenied |
| |
| logout(request) |
| raise PermissionDenied("Session invalid: Device fingerprint mismatch.") |
|
|
| |
| request.device_fingerprint = current_fingerprint |
| return None |
|
|