infrasynth-backend-kit/infrasynth/security/two_factor/middleware.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

53 lines
1.9 KiB
Python

"""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")