""" FastAPI Authentication Middleware pentru ROA2WEB Acest modul oferă middleware pentru autentificarea automată în aplicațiile FastAPI, incluzând extragerea token-urilor, validarea și injectarea datelor utilizatorului în contextul request-ului. Funcționalități: - Extragere automată token JWT din header Authorization - Validare token și user data injection - Rate limiting pentru endpoint-urile de autentificare - Logging pentru securitate și monitoring """ import logging import time from typing import Optional, Callable, Dict, Any, List, Set from collections import defaultdict, deque from datetime import datetime, timedelta from fastapi import Request, Response, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse from .jwt_handler import jwt_handler, TokenData from .auth_service import auth_service from .models import CurrentUser, AuthError logger = logging.getLogger(__name__) class RateLimiter: """ Rate limiter pentru protejarea endpoint-urilor de autentificare """ def __init__(self, max_requests: int = 5, time_window: int = 300): """ Inițializează rate limiter Args: max_requests: Numărul maxim de request-uri permise time_window: Fereastra de timp în secunde """ self.max_requests = max_requests self.time_window = time_window self.requests: Dict[str, deque] = defaultdict(deque) def is_allowed(self, client_ip: str) -> bool: """ Verifică dacă request-ul este permis pentru acest IP Args: client_ip: Adresa IP a clientului Returns: True dacă request-ul este permis """ now = time.time() client_requests = self.requests[client_ip] # Șterge request-urile vechi while client_requests and client_requests[0] < now - self.time_window: client_requests.popleft() # Verifică dacă putem accepta încă un request if len(client_requests) >= self.max_requests: return False # Adaugă request-ul curent client_requests.append(now) return True def get_reset_time(self, client_ip: str) -> int: """ Returnează timpul când rate limiting se resetează pentru acest IP Args: client_ip: Adresa IP a clientului Returns: Timestamp când se resetează """ client_requests = self.requests[client_ip] if not client_requests: return int(time.time()) return int(client_requests[0] + self.time_window) class AuthenticationMiddleware(BaseHTTPMiddleware): """ Middleware pentru autentificarea automată în FastAPI Acest middleware: - Extrage token-ul JWT din header-ul Authorization - Validează token-ul și obține datele utilizatorului - Injectează utilizatorul curent în request.state - Aplică rate limiting pentru endpoint-urile sensibile """ def __init__( self, app, excluded_paths: Optional[List[str]] = None, rate_limit_paths: Optional[List[str]] = None, rate_limiter: Optional[RateLimiter] = None ): """ Inițializează middleware-ul Args: app: Aplicația FastAPI excluded_paths: Căile care nu necesită autentificare rate_limit_paths: Căile cu rate limiting rate_limiter: Instance de rate limiter personalizat """ super().__init__(app) self.excluded_paths = excluded_paths or [ "/docs", "/redoc", "/openapi.json", "/health", "/", "/auth/login", "/auth/register" ] self.rate_limit_paths = rate_limit_paths or [ "/auth/login", "/auth/register", "/auth/forgot-password" ] self.rate_limiter = rate_limiter or RateLimiter(max_requests=5, time_window=300) logger.info(f"Authentication middleware initialized with {len(self.excluded_paths)} excluded paths") def _get_client_ip(self, request: Request) -> str: """Obține adresa IP a clientului""" # Verifică header-ele proxy forwarded_for = request.headers.get("X-Forwarded-For") if forwarded_for: return forwarded_for.split(",")[0].strip() real_ip = request.headers.get("X-Real-IP") if real_ip: return real_ip # Fallback la client IP direct return request.client.host if request.client else "unknown" def _should_exclude_path(self, path: str) -> bool: """Verifică dacă path-ul trebuie exclus de la autentificare""" # Special case for root path to avoid excluding all paths that start with "/" if "/" in self.excluded_paths and path == "/": return True # Check other excluded paths (excluding "/" to avoid matching all paths) excluded_paths_no_root = [p for p in self.excluded_paths if p != "/"] return any(path.startswith(excluded) for excluded in excluded_paths_no_root) def _should_rate_limit_path(self, path: str) -> bool: """Verifică dacă path-ul necesită rate limiting""" return any(path.startswith(limited) for limited in self.rate_limit_paths) def _extract_token_from_header(self, request: Request) -> Optional[str]: """ Extrage token-ul JWT în header-ul Authorization Args: request: Request-ul HTTP Returns: Token-ul JWT sau None """ authorization = request.headers.get("Authorization") if not authorization: return None if not authorization.startswith("Bearer "): return None return authorization[7:] # Elimină "Bearer " async def _create_current_user(self, token_data: TokenData) -> CurrentUser: """ Creează obiectul CurrentUser din token data Args: token_data: Datele din token Returns: Obiectul CurrentUser """ return CurrentUser( username=token_data.username, user_id=token_data.user_id, companies=token_data.companies, permissions=token_data.permissions, last_login=datetime.now() ) async def _handle_rate_limiting(self, request: Request, path: str) -> Optional[Response]: """ Gestionează rate limiting pentru căile sensibile Args: request: Request-ul HTTP path: Calea request-ului Returns: Response cu eroare dacă este rate limited, None altfel """ if not self._should_rate_limit_path(path): return None client_ip = self._get_client_ip(request) if not self.rate_limiter.is_allowed(client_ip): reset_time = self.rate_limiter.get_reset_time(client_ip) logger.warning(f"Rate limit exceeded for IP {client_ip} on path {path}") error = AuthError( error="rate_limit_exceeded", error_description="Too many requests. Please try again later.", error_code="RATE_LIMIT_001" ) return JSONResponse( status_code=status.HTTP_429_TOO_MANY_REQUESTS, content=error.dict(), headers={ "X-RateLimit-Limit": str(self.rate_limiter.max_requests), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": str(reset_time), "Retry-After": str(reset_time - int(time.time())) } ) return None async def dispatch(self, request: Request, call_next: Callable) -> Response: """ Procesează request-ul prin middleware Args: request: Request-ul HTTP call_next: Următorul handler din pipeline Returns: Response-ul HTTP """ start_time = time.time() path = request.url.path # IMPORTANT: Allow OPTIONS requests (CORS preflight) to pass through if request.method == "OPTIONS": response = await call_next(request) return response # Rate limiting pentru căile sensibile rate_limit_response = await self._handle_rate_limiting(request, path) if rate_limit_response: return rate_limit_response # Skip autentificare pentru căile excluse if self._should_exclude_path(path): request.state.user = None request.state.is_authenticated = False response = await call_next(request) return response # Extrage token-ul token = self._extract_token_from_header(request) if not token: # Nu există token - pentru endpoint-urile protejate returnează 401 logger.warning(f"No token provided for protected path {path}") error = AuthError( error="authentication_required", error_description="Authentication required", error_code="AUTH_003" ) return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content=error.dict(), headers={"WWW-Authenticate": "Bearer"} ) # Validează token-ul token_data = jwt_handler.verify_token(token) if not token_data: # Token invalid logger.warning(f"Invalid token used for path {path}") error = AuthError( error="invalid_token", error_description="The provided token is invalid or expired.", error_code="AUTH_001" ) return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content=error.dict(), headers={"WWW-Authenticate": "Bearer"} ) # Token valid - creează utilizatorul curent try: current_user = await self._create_current_user(token_data) request.state.user = current_user request.state.is_authenticated = True request.state.token_data = token_data logger.debug(f"User {current_user.username} authenticated successfully for path {path}") except Exception as e: logger.error(f"Error creating current user: {str(e)}") error = AuthError( error="authentication_error", error_description="Authentication processing error.", error_code="AUTH_002" ) return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=error.dict() ) # Procesează request-ul response = await call_next(request) # Adaugă header-e de securitate response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" # Log timpul de procesare process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response class HTTPBearerOptional(HTTPBearer): """ Versiune opțională pentru autentificare care nu aruncă excepții dacă token-ul lipsește - utile pentru endpoint-urile care pot funcționa atât cu cât și fără autentificare """ async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]: """ Extrage credențialele de autentificare fără să arunce excepții Args: request: Request-ul HTTP Returns: Credențialele sau None """ try: return await super().__call__(request) except HTTPException: return None # Instance predefinite pentru folosire rapidă security_optional = HTTPBearerOptional(auto_error=False) security_required = HTTPBearer() # Rate limiter default default_rate_limiter = RateLimiter(max_requests=5, time_window=300)