fix(security): improve api key authentication and error handling

This commit is contained in:
Fanilo-Nantenaina 2026-01-20 16:18:04 +03:00
parent 67ef83c4e3
commit 211dd4fd23
2 changed files with 48 additions and 25 deletions

14
api.py
View file

@ -187,8 +187,18 @@ def custom_openapi():
)
openapi_schema["components"]["securitySchemes"] = {
"HTTPBearer": {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"},
"ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "X-API-Key"},
"HTTPBearer": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Authentification JWT pour utilisateurs (POST /auth/login)",
},
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "Clé API pour intégrations externes (format: sdk_live_xxx)",
},
}
openapi_schema["security"] = [{"HTTPBearer": []}, {"ApiKeyAuth": []}]

View file

@ -132,26 +132,30 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
method = request.method
if self._is_excluded_path(path):
logger.debug(f" Route publique: {method} {path}")
return await call_next(request)
auth_header = request.headers.get("Authorization")
has_jwt = auth_header and auth_header.startswith("Bearer ")
api_key_header = request.headers.get("X-API-Key")
api_key = request.headers.get("X-API-Key")
has_api_key = bool(api_key)
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ")[1]
if has_jwt:
if token.startswith("sdk_live_"):
logger.warning(
" API Key envoyée dans Authorization au lieu de X-API-Key"
)
api_key_header = token
else:
logger.debug(f" JWT détecté pour {method} {path}")
return await call_next(request)
if has_api_key:
if api_key_header:
logger.debug(f" API Key détectée pour {method} {path}")
return await self._handle_api_key_auth(
request, api_key, path, method, call_next
request, api_key_header, path, method, call_next
)
logger.warning(f" Aucune authentification pour {method} {path}")
logger.warning(f" Aucune authentification: {method} {path}")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={
@ -170,7 +174,7 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
method: str,
call_next: Callable,
):
"""Gère l'authentification par API Key"""
"""Gère l'authentification par API Key avec vérification STRICTE"""
try:
from database.db_config import async_session_factory
from services.api_key import ApiKeyService
@ -181,7 +185,7 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
api_key_obj = await service.verify_api_key(api_key)
if not api_key_obj:
logger.warning(f" Clé API invalide pour {method} {path}")
logger.warning(f" Clé API invalide: {method} {path}")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={
@ -192,7 +196,7 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
is_allowed, rate_info = await service.check_rate_limit(api_key_obj)
if not is_allowed:
logger.warning(f"⚠️ Rate limit dépassé: {api_key_obj.name}")
logger.warning(f"⚠️ Rate limit: {api_key_obj.name}")
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={"detail": "Rate limit dépassé"},
@ -203,28 +207,37 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
)
has_access = await service.check_endpoint_access(api_key_obj, path)
if not has_access:
logger.warning(
f"Accès refusé: {api_key_obj.name}{method} {path}"
import json
allowed = (
json.loads(api_key_obj.allowed_endpoints)
if api_key_obj.allowed_endpoints
else ["Tous"]
)
logger.warning(
f" ACCÈS REFUSÉ: {api_key_obj.name}\n"
f" Endpoint demandé: {path}\n"
f" Endpoints autorisés: {allowed}"
)
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={
"detail": "Accès non autorisé à cet endpoint",
"endpoint": path,
"endpoint_requested": path,
"api_key_name": api_key_obj.name,
"allowed_endpoints": (
api_key_obj.allowed_endpoints
if api_key_obj.allowed_endpoints
else "Tous"
),
"allowed_endpoints": allowed,
"hint": "Cette clé API n'a pas accès à cet endpoint. Contactez l'administrateur.",
},
)
request.state.api_key = api_key_obj
request.state.authenticated_via = "api_key"
logger.info(f"✅ API Key valide: {api_key_obj.name}{method} {path}")
logger.info(f" ACCÈS AUTORISÉ: {api_key_obj.name}{method} {path}")
return await call_next(request)
@ -232,7 +245,7 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
logger.error(f" Erreur validation API Key: {e}", exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "Erreur interne lors de la validation"},
content={"detail": f"Erreur interne: {str(e)}"},
)