refactor(security): clean up authentication middleware and api docs

This commit is contained in:
Fanilo-Nantenaina 2026-01-21 16:23:34 +03:00
parent 1c6c45465f
commit d25c2cffa9
2 changed files with 36 additions and 23 deletions

48
api.py
View file

@ -278,13 +278,12 @@ def get_auth_schemes_for_user(swagger_user: dict) -> dict:
allowed_tags = swagger_user.get("allowed_tags") allowed_tags = swagger_user.get("allowed_tags")
if not allowed_tags: if not allowed_tags:
# Admin complet
return { return {
"HTTPBearer": { "HTTPBearer": {
"type": "http", "type": "http",
"scheme": "bearer", "scheme": "bearer",
"bearerFormat": "JWT", "bearerFormat": "JWT",
"description": "🎫 Authentification JWT pour utilisateurs (POST /auth/login). " "description": "Authentification JWT pour utilisateurs (POST /auth/login). "
"Utilisez SOIT JWT SOIT API Key, pas les deux.", "Utilisez SOIT JWT SOIT API Key, pas les deux.",
}, },
"ApiKeyAuth": { "ApiKeyAuth": {
@ -303,7 +302,7 @@ def get_auth_schemes_for_user(swagger_user: dict) -> dict:
"type": "http", "type": "http",
"scheme": "bearer", "scheme": "bearer",
"bearerFormat": "JWT", "bearerFormat": "JWT",
"description": "🎫 Authentification JWT pour utilisateurs (POST /auth/login). " "description": "Authentification JWT pour utilisateurs (POST /auth/login). "
"Utilisez SOIT JWT SOIT API Key, pas les deux.", "Utilisez SOIT JWT SOIT API Key, pas les deux.",
} }
@ -461,9 +460,18 @@ async def custom_openapi_endpoint(request: Request):
schema = generate_filtered_openapi_schema(app, allowed_tags, swagger_user) schema = generate_filtered_openapi_schema(app, allowed_tags, swagger_user)
if request.url.scheme == "https": scheme = request.url.scheme
if "servers" not in schema or not schema["servers"]: forwarded_proto = request.headers.get("X-Forwarded-Proto")
schema["servers"] = [{"url": str(request.base_url).rstrip("/")}]
if forwarded_proto:
scheme = forwarded_proto
base_url = str(request.base_url).rstrip("/")
if scheme == "https" and base_url.startswith("http://"):
base_url = base_url.replace("http://", "https://", 1)
schema["servers"] = [{"url": base_url}]
return JSONResponse(content=schema) return JSONResponse(content=schema)
@ -479,19 +487,31 @@ async def custom_swagger_ui(request: Request):
headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'}, headers={"WWW-Authenticate": 'Basic realm="Swagger UI"'},
) )
return get_swagger_ui_html( allowed_tags = swagger_user.get("allowed_tags")
openapi_url="/openapi.json", is_restricted = allowed_tags is not None and len(allowed_tags) > 0
title=f"{app.title} - Documentation",
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png", swagger_params = {
swagger_ui_parameters={
"persistAuthorization": True, "persistAuthorization": True,
"displayRequestDuration": True, "displayRequestDuration": True,
"filter": True, "filter": True,
"tryItOutEnabled": True, "tryItOutEnabled": True,
"docExpansion": "list", "docExpansion": "list",
# CORRECTIF : Ne pas pré-remplir les credentials }
"preAuthorizeApiKey": False,
}, if is_restricted:
swagger_params["preAuthorizeApiKey"] = {
"ApiKeyAuth": {
"name": "X-API-Key",
"schema": {"type": "apiKey", "in": "header", "name": "X-API-Key"},
"value": "",
}
}
return get_swagger_ui_html(
openapi_url="/openapi.json",
title=f"{app.title} - Documentation",
swagger_favicon_url="https://fastapi.tiangolo.com/img/favicon.png",
swagger_ui_parameters=swagger_params,
) )

View file

@ -158,18 +158,14 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
api_key_header = request.headers.get("X-API-Key") api_key_header = request.headers.get("X-API-Key")
# CORRECTIF : Nettoyer et valider la clé API
if api_key_header: if api_key_header:
api_key_header = api_key_header.strip() api_key_header = api_key_header.strip()
# Si la clé est vide ou juste des espaces, la considérer comme absente
if not api_key_header or api_key_header == "": if not api_key_header or api_key_header == "":
api_key_header = None api_key_header = None
# Vérifier si c'est un token JWT Bearer
if auth_header and auth_header.startswith("Bearer "): if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ", 1)[1].strip() token = auth_header.split(" ", 1)[1].strip()
# CORRECTIF : Vérifier si c'est une API Key envoyée par erreur dans Authorization
if token.startswith("sdk_live_"): if token.startswith("sdk_live_"):
logger.warning( logger.warning(
"⚠️ API Key envoyée dans Authorization au lieu de X-API-Key" "⚠️ API Key envoyée dans Authorization au lieu de X-API-Key"
@ -178,19 +174,16 @@ class ApiKeyMiddlewareHTTP(BaseHTTPMiddleware):
request, token, path, method, call_next request, token, path, method, call_next
) )
# C'est un JWT valide, déléguer à FastAPI logger.debug(f"JWT détecté pour {method} {path} → délégation à FastAPI")
logger.debug(f"🎫 JWT détecté pour {method} {path} → délégation à FastAPI")
request.state.authenticated_via = "jwt" request.state.authenticated_via = "jwt"
return await call_next(request) return await call_next(request)
# CORRECTIF : Si une clé API est présente ET non vide, la traiter
if api_key_header: if api_key_header:
logger.debug(f" API Key détectée pour {method} {path}") logger.debug(f" API Key détectée pour {method} {path}")
return await self._handle_api_key_auth( return await self._handle_api_key_auth(
request, api_key_header, path, method, call_next request, api_key_header, path, method, call_next
) )
# Aucune authentification fournie, déléguer à FastAPI qui renverra 401
logger.debug(f"❌ Aucune auth pour {method} {path} → délégation à FastAPI") logger.debug(f"❌ Aucune auth pour {method} {path} → délégation à FastAPI")
return await call_next(request) return await call_next(request)