""" CONFIGURATION CORS POUR API AVEC CLÉS API MULTIPLES Problématique: - Les clés API seront utilisées depuis de nombreux domaines/IPs différents - Impossible de lister tous les origins autorisés à l'avance - Solution: CORS permissif mais sécurisé par les clés API Stratégies: 1. CORS ouvert avec validation par clé API (RECOMMANDÉ) 2. CORS dynamique basé sur whitelist 3. CORS avec wildcard et credentials=False """ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from typing import List import os import logging logger = logging.getLogger(__name__) # ============================================ # STRATÉGIE 1: CORS OUVERT + VALIDATION API KEY # ============================================ # ✅ RECOMMANDÉ pour les API publiques avec clés # # Principe: # - Accepter toutes les origines (allow_origins=["*"]) # - La sécurité est assurée par la validation des clés API # - Les clés API protègent l'accès, pas le CORS def configure_cors_open(app: FastAPI): """ Configuration CORS ouverte (RECOMMANDÉE) ✅ Accepte toutes les origines ✅ Sécurité assurée par les clés API ✅ Simplifie l'utilisation pour les clients ⚠️ Attention: credentials=False obligatoire avec allow_origins=["*"] """ app.add_middleware( CORSMiddleware, allow_origins=["*"], # Accepte toutes les origines allow_credentials=False, # ⚠️ Obligatoire avec "*" allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], allow_headers=["*"], # Accepte tous les headers (dont X-API-Key) expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"], max_age=3600, # Cache preflight requests pendant 1h ) logger.info("🌐 CORS configuré: Mode OUVERT (sécurisé par API Keys)") logger.info(" - Origins: * (toutes)") logger.info(" - Headers: * (dont X-API-Key)") logger.info(" - Credentials: False") # ============================================ # STRATÉGIE 2: CORS AVEC WHITELIST DYNAMIQUE # ============================================ # 🔶 Pour environnements contrôlés # # Principe: # - Lister explicitement les domaines autorisés # - Peut inclure des patterns wildcards # - Credentials possible (cookies, etc.) def configure_cors_whitelist(app: FastAPI): """ Configuration CORS avec whitelist (MODE CONTRÔLÉ) ✅ Meilleur contrôle des origines ✅ Credentials possible ❌ Nécessite maintenance de la liste À utiliser si vous connaissez tous les domaines clients """ # Charger depuis .env ou config allowed_origins_str = os.getenv("CORS_ALLOWED_ORIGINS", "") if allowed_origins_str: allowed_origins = [ origin.strip() for origin in allowed_origins_str.split(",") if origin.strip() ] else: # Valeurs par défaut allowed_origins = [ "http://localhost:3000", # Frontend dev React/Vue "http://localhost:5173", # Vite dev "http://localhost:8080", # Frontend dev alternatif "https://app.votre-domaine.com", "https://admin.votre-domaine.com", # Ajouter vos domaines de production ] app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, allow_credentials=True, # ✅ Possible avec liste explicite allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], allow_headers=["Content-Type", "Authorization", "X-API-Key"], expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"], max_age=3600, ) logger.info("🌐 CORS configuré: Mode WHITELIST") logger.info(f" - Origins autorisées: {len(allowed_origins)}") for origin in allowed_origins: logger.info(f" • {origin}") # ============================================ # STRATÉGIE 3: CORS AVEC REGEX (AVANCÉ) # ============================================ # 🔶 Pour patterns complexes # # Exemple: Autoriser tous les sous-domaines *.votre-domaine.com def configure_cors_regex(app: FastAPI): """ Configuration CORS avec patterns regex (AVANCÉ) ✅ Flexible pour sous-domaines ✅ Supporte patterns complexes ❌ Plus complexe à configurer Utilise allow_origin_regex au lieu de allow_origins """ # Pattern regex pour autoriser tous les sous-domaines origin_regex = r"https://.*\.votre-domaine\.com" app.add_middleware( CORSMiddleware, allow_origin_regex=origin_regex, # ⚠️ Utilise regex au lieu de liste allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], allow_headers=["Content-Type", "Authorization", "X-API-Key"], expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"], max_age=3600, ) logger.info("🌐 CORS configuré: Mode REGEX") logger.info(f" - Pattern: {origin_regex}") # ============================================ # STRATÉGIE 4: CORS HYBRIDE (PRODUCTION) # ============================================ # ✅ RECOMMANDÉ pour production # # Principe: # - Whitelist pour les domaines connus (credentials=True) # - Fallback sur "*" pour le reste (credentials=False) def configure_cors_hybrid(app: FastAPI): """ Configuration CORS hybride (PRODUCTION) ✅ Meilleur des deux mondes ✅ Whitelist pour domaines connus ✅ Fallback ouvert pour API Keys externes Note: Nécessite un middleware custom pour gérer les deux modes """ from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import Response class HybridCORSMiddleware(BaseHTTPMiddleware): def __init__(self, app, known_origins: List[str]): super().__init__(app) self.known_origins = set(known_origins) async def dispatch(self, request, call_next): origin = request.headers.get("origin") # Si origin connue → CORS strict avec credentials if origin in self.known_origins: response = await call_next(request) response.headers["Access-Control-Allow-Origin"] = origin response.headers["Access-Control-Allow-Credentials"] = "true" response.headers["Access-Control-Allow-Methods"] = ( "GET, POST, PUT, DELETE, PATCH, OPTIONS" ) response.headers["Access-Control-Allow-Headers"] = ( "Content-Type, Authorization, X-API-Key" ) return response # Sinon → CORS ouvert sans credentials response = await call_next(request) response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Methods"] = ( "GET, POST, PUT, DELETE, PATCH, OPTIONS" ) response.headers["Access-Control-Allow-Headers"] = "*" return response # Domaines connus (whitelist) known_origins = [ "https://app.votre-domaine.com", "https://admin.votre-domaine.com", "http://localhost:3000", "http://localhost:5173", ] app.add_middleware(HybridCORSMiddleware, known_origins=known_origins) logger.info("🌐 CORS configuré: Mode HYBRIDE") logger.info(f" - Whitelist: {len(known_origins)} domaines") logger.info(" - Fallback: * (ouvert)") # ============================================ # FONCTION PRINCIPALE # ============================================ def setup_cors(app: FastAPI, mode: str = "open"): """ Configure CORS selon le mode choisi Args: app: Instance FastAPI mode: "open" | "whitelist" | "regex" | "hybrid" Recommandations: - Development: "open" - Production (API publique): "open" ou "hybrid" - Production (API interne): "whitelist" """ if mode == "open": configure_cors_open(app) elif mode == "whitelist": configure_cors_whitelist(app) elif mode == "regex": configure_cors_regex(app) elif mode == "hybrid": configure_cors_hybrid(app) else: logger.warning( f"⚠️ Mode CORS inconnu: {mode}. Utilisation de 'open' par défaut." ) configure_cors_open(app) # ============================================ # EXEMPLE D'UTILISATION DANS api.py # ============================================ """ # Dans api.py from config.cors_config import setup_cors app = FastAPI(...) # DÉVELOPPEMENT setup_cors(app, mode="open") # PRODUCTION (API publique avec clés) setup_cors(app, mode="hybrid") # PRODUCTION (API interne uniquement) setup_cors(app, mode="whitelist") """ # ============================================ # VARIABLES D'ENVIRONNEMENT (.env) # ============================================ """ # Pour mode "whitelist" CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com,http://localhost:3000 # Pour mode "regex" CORS_ORIGIN_REGEX=https://.*\.example\.com # Choisir le mode CORS_MODE=open """ # ============================================ # FAQ CORS + API KEYS # ============================================ """ Q: Pourquoi allow_origins=["*"] est sécurisé avec des API Keys ? R: Les clés API protègent l'accès aux données. CORS empêche seulement les navigateurs web de faire des requêtes cross-origin. Un attaquant peut contourner CORS facilement (curl, postman), donc la vraie sécurité vient de la validation des clés API, pas du CORS. Q: Pourquoi credentials=False avec allow_origins=["*"] ? R: C'est une restriction du navigateur (spec CORS). Vous ne pouvez pas avoir credentials=True ET origins=["*"] en même temps. Q: Mes clients utilisent des IPs dynamiques, que faire ? R: Utilisez mode "open". Les clés API sont justement faites pour ça - permettre l'accès depuis n'importe quelle origine, de manière sécurisée. Q: Je veux quand même utiliser des cookies/sessions ? R: Utilisez mode "whitelist" ou "hybrid" pour les domaines qui ont besoin de credentials, et laissez les autres utiliser X-API-Key sans credentials. Q: Comment tester CORS localement ? R: Créez un simple fichier HTML avec fetch() et ouvrez-le en file:// Ou utilisez un serveur local (python -m http.server) """