Files
roa2web-service-auto/shared/auth/middleware.py
Marius Mutu f42eff71a6 Fix .gitignore and add missing authentication source files
This commit fixes overly broad .gitignore patterns that were excluding
important source code files from version control. Previously, wildcard
patterns like *auth*, *token*, *secret*, *connection*, and *credential*
were excluding ALL files containing these words, including critical
application code.

Changes:
- Updated .gitignore with specific patterns for sensitive config files
  (*.json, *.txt, *.yml, *.yaml extensions only)
- Removed broad wildcards that excluded source code files

Added missing source files:
- shared/auth/ (9 files): Complete authentication system
  - JWT handler, middleware, auth service, models, routes
- reports-app/backend/app/routers/auth.py: Authentication API router
- reports-app/backend/app/auth_middleware_wrapper.py: Middleware wrapper
- reports-app/frontend/src/stores/auth.js: Vue.js auth store
- reports-app/frontend/tests/: E2E tests and fixtures for auth
- reports-app/telegram-bot/app/auth/: Telegram auth linking module
- deployment/windows/scripts/Setup-ClaudeAuth.ps1: Windows deployment script
- security/secrets_scanner.py: Security scanning utility

These files are essential for the application to function and should
have been included in the initial commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 15:02:28 +03:00

373 lines
13 KiB
Python

"""
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
"""
print(f"[ORIGINAL MIDDLEWARE] dispatch called for path: {request.url.path}")
start_time = time.time()
path = request.url.path
# 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
print(f"[MIDDLEWARE DEBUG] Extracting token for path: {path}")
token = self._extract_token_from_header(request)
print(f"[MIDDLEWARE DEBUG] Extracted token: {token[:30] if token else 'None'}...")
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
print(f"[MIDDLEWARE DEBUG] Validating token: {token[:30]}...")
token_data = jwt_handler.verify_token(token)
print(f"[MIDDLEWARE DEBUG] Token validation result: {token_data}")
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)