crowdata / app /middleware /csrf.py
YOSOYYONOSOYOTRO's picture
Upload folder using huggingface_hub
83bdb4a verified
Raw
History Blame Contribute Delete
4.95 kB
import os
import secrets
import hashlib
from typing import Callable, Optional
from fastapi import Request, Response, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class CSRFMiddleware(BaseHTTPMiddleware):
"""
CSRF Protection usando patr贸n Double-Submit Cookie.
- Genera cookie `csrf_token` (HttpOnly=False, SameSite=Lax) al iniciar sesi贸n
- Valida header `X-CSRF-Token` en m茅todos mutantes (POST, PUT, PATCH, DELETE)
- Excluye: GET, HEAD, OPTIONS, endpoints de auth (/api/auth/*)
- Skip si no hay cookie (p.ej. APIs program谩ticas con Bearer token)
"""
def __init__(
self,
app,
cookie_name: str = "csrf_token",
header_name: str = "X-CSRF-Token",
cookie_secure: bool = True,
cookie_samesite: str = "lax",
excluded_paths: Optional[list] = None,
excluded_methods: Optional[list] = None,
):
super().__init__(app)
self.cookie_name = cookie_name
self.header_name = header_name
self.cookie_secure = cookie_secure
self.cookie_samesite = cookie_samesite
self.excluded_paths = excluded_paths or [
"/api/auth/jwt/login",
"/api/auth/jwt/logout",
"/api/auth/register",
"/api/auth/forgot-password",
"/api/auth/reset-password",
"/api/auth/users/me",
]
self.excluded_methods = excluded_methods or ["GET", "HEAD", "OPTIONS"]
def _get_csrf_token(self, request: Request) -> Optional[str]:
"""Obtener token CSRF de la cookie."""
cookie_header = request.headers.get("cookie", "")
for cookie in cookie_header.split(";"):
cookie = cookie.strip()
if cookie.startswith(f"{self.cookie_name}="):
return cookie.split("=", 1)[1]
return None
def _generate_csrf_token(self) -> str:
"""Generar token CSRF criptogr谩ficamente seguro."""
return secrets.token_urlsafe(32)
def _is_excluded_path(self, path: str) -> bool:
"""Verificar si el path est谩 excluido de validaci贸n CSRF."""
for excluded in self.excluded_paths:
if path.startswith(excluded):
return True
return False
async def dispatch(self, request: Request, call_next: Callable):
# Skip CSRF para m茅todos seguros
if request.method in self.excluded_methods:
return await call_next(request)
# Skip CSRF para paths excluidos
if self._is_excluded_path(request.url.path):
return await call_next(request)
# Solo validar si hay cookie de sesi贸n (usuario logueado via cookie)
# APIs program谩ticas con Bearer token no necesitan CSRF
session_cookie = request.cookies.get("cd_token")
if not session_cookie:
# No hay cookie de sesi贸n, asumimos API program谩tica
return await call_next(request)
# Obtener token CSRF de cookie
csrf_cookie = self._get_csrf_token(request)
csrf_header = request.headers.get(self.header_name)
if not csrf_cookie:
# No hay token CSRF en cookie - generar uno nuevo y setear
response = await call_next(request)
new_token = self._generate_csrf_token()
response.set_cookie(
key=self.cookie_name,
value=new_token,
max_age=7 * 24 * 60 * 60, # 7 d铆as
httponly=False, # JS necesita leerlo para enviar en header
secure=self.cookie_secure,
samesite=self.cookie_samesite,
path="/",
)
return response
if not csrf_header:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"detail": "CSRF token requerido en header X-CSRF-Token"},
)
# Validar token (comparaci贸n timing-safe)
if not secrets.compare_digest(csrf_cookie, csrf_header):
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"detail": "CSRF token inv谩lido"},
)
# Token v谩lido - proceder
return await call_next(request)
def get_csrf_middleware(
cookie_secure: bool = True,
cookie_samesite: str = "lax",
excluded_paths: Optional[list] = None,
) -> type:
"""Factory para crear middleware CSRF con configuraci贸n personalizada."""
class ConfiguredCSRFMiddleware(CSRFMiddleware):
def __init__(self, app):
super().__init__(
app,
cookie_secure=cookie_secure,
cookie_samesite=cookie_samesite,
excluded_paths=excluded_paths,
)
return ConfiguredCSRFMiddleware