321 lines
10 KiB
Python
321 lines
10 KiB
Python
"""
|
|
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)
|
|
"""
|