"""Enforces 2FA for session-authenticated users. Bearer/cookie JWT requests never reach this check: an access token is only ever minted *after* the second factor succeeds (see ``security.views``), so holding a minted token is proof the factor was completed. This middleware therefore guards the session-authenticated surface (Django admin and any session-based view). """ from django.http import JsonResponse from django.shortcuts import redirect from django.utils.deprecation import MiddlewareMixin from ..models import TwoFactorConfig class TwoFactorMiddleware(MiddlewareMixin): EXEMPT_PATHS = [ "/api/v1/auth/login/", "/api/v1/auth/logout/", "/api/v1/auth/2fa/setup/", "/api/v1/auth/2fa/verify-setup/", "/api/v1/auth/2fa/verify/", "/api/v1/auth/2fa/recovery/", "/api/v1/auth/check/", ] def process_request(self, request): if not hasattr(request, "user") or not request.user.is_authenticated: return None # A minted JWT proves the second factor already happened. if getattr(request, "auth", None) is not None: return None if request.path in self.EXEMPT_PATHS or request.path.startswith("/admin/"): return None session = getattr(request, "session", None) if session is not None and session.get("_2fa_verified"): return None try: tfa = TwoFactorConfig.objects.get(user=request.user) except TwoFactorConfig.DoesNotExist: return None if not (tfa.is_enabled and tfa.is_configured): return None if request.path.startswith("/api/"): return JsonResponse( { "code": "AUTH_2FA_REQUIRED", "message": "Second-factor verification is required.", "details": [], }, status=403, ) return redirect("2fa-verify")