Project Oxygen & Ideo-LabIDEO LAB Dashboard 2026

🌐 django-cors-headers — CORS propre et sĂ©curisĂ© sur Django

Guide IDEO‑Lab ultra-pratique : installation, architecture CORS, config fine, piĂšges sĂ©curitĂ©, DRF, prĂ©flight, Nginx, et recettes prod.

1.1

Pourquoi CORS existe (vraiment)

Same‑Origin Policy, navigateur, Origin, headers Access‑Control‑*, prĂ©flight OPTIONS.

Web SecurityBrowserPreflight
1.2

Architecture (mental model)

Qui parle à qui ? Frontend → navigateur → Django. CORS n'est pas “un truc serveur”, c'est une rùgle navigateur.

Request FlowHeadersPolicy
2.1

Installation & intégration Django

pip install, INSTALLED_APPS, ordre MIDDLEWARE, check rapide.

pipsettings.pyMiddleware

Quickstart (dev/prod)

Config minimale propre, puis config “prod” restrictive + credentials + headers custom.

MinimalProductionChecklist
3.1

Settings essentiels

CORS_ALLOWED_ORIGINS, regex, allow‑all, methods, headers, expose, credentials.

CORS_ALLOWED_ORIGINSRegexCredentials
3.2

Préflight & performance

Quand un OPTIONS apparaĂźt, Access-Control-Request-*, cache preflight, timeouts.

OPTIONSCachePerf
4.1

SĂ©curitĂ© : CORS ≠ Auth ≠ CSRF

Erreurs classiques : * + credentials, confusion avec CSRF, fuite de cookies, “open API”.

CSRFCookiesThreats
4.2

DRF / JWT / Cookies

API Django REST : tokens vs cookies, Authorization header, withCredentials, SameSite.

DRFJWTAuth
5.1

Nginx / Reverse proxy

OĂč mettre CORS ? (souvent dans Django), mais parfois au proxy. Attention doubles headers / incohĂ©rences.

NginxProxyHeaders
5.2

Tests & vérification

curl, DevTools, reproduire un préflight, tests Django/pytest, validation CI.

curlDevToolspytest
6.1

Recettes (réelles) de prod

Multi‑env, multi‑frontends, sous‑domaines, preview deployments, API publique + backoffice.

PatternsMulti-envSaaS
★

Troubleshooting

Les 12 erreurs les plus frĂ©quentes + diagnostic “oĂč ça casse” + solutions.

DebugChecklistCommon Errors
📎

Liens & ressources

Docs officielles, spec CORS, articles, outils, snippets.

DocsSpecRefs
🎚

Niveau & complexité

Quand c’est simple, quand ça devient “piĂ©geux”, et comment le rendre robuste.

DifficultyRiskOps
⚡

Cheat‑sheet

Config prĂȘt‑à‑coller, commandes curl, checklists, matrices “frontend ↔ backend”.

Copy/PastecurlRunbook
1.1 — CORS : le vrai problĂšme que ça rĂ©sout
Same-Origin Policy (SOP) : le “mur” navigateur

Par dĂ©faut, un script JavaScript exĂ©cutĂ© sur https://app.example.com n’a pas le droit de lire la rĂ©ponse d’une requĂȘte faite vers https://api.example.com (origine diffĂ©rente). C’est la Same-Origin Policy.

Origine = scheme + host + port
Ex: https://site.com ≠ http://site.com ≠ https://site.com:8443
CORS : une permission contrÎlée par des headers

CORS est un mĂ©canisme standard du web oĂč le serveur indique au navigateur “tu peux autoriser ce JavaScript Ă  lire cette rĂ©ponse”. Le navigateur envoie un header Origin et le serveur rĂ©pond avec des headers Access-Control-Allow-*.

Headers qui comptent le plus
HeaderRĂŽleRemarque
OriginEnvoyĂ© par le navigateur (source de la page)PrĂ©sent surtout sur requĂȘtes “CORS”.
Access-Control-Allow-OriginAutorise une origine (ou refuse)Jamais * si credentials.
Access-Control-Allow-CredentialsAutorise cookies / auth HTTPDoit ĂȘtre true + origine explicite.
Access-Control-Allow-HeadersAutorise headers non “simples”Ex: Authorization, X-Request-ID.
Access-Control-Allow-MethodsAutorise méthodes HTTPImportant sur préflight.
Point clĂ© : CORS n’est pas une auth

CORS ne “protĂšge” pas vos endpoints contre un attaquant. Il contrĂŽle seulement si le navigateur laisse un script lire la rĂ©ponse. Un attaquant peut toujours appeler votre API depuis un serveur (curl, scripts) sans CORS.

Quand tu as besoin de django-cors-headers ?
  • Ton frontend (React/Vue/Angular) est sur un autre domaine/port que ton backend Django.
  • Tu utilises un API Gateway / domaine api.* sĂ©parĂ©.
  • Tu sers l’API sur localhost:8000 et le frontend sur localhost:5173.
  • Tu as un backoffice / admin sĂ©parĂ© d’un front grand public.
À l’inverse : si ton frontend est rendu par Django (templates) sur la mĂȘme origine, tu n’as souvent pas besoin de CORS.
Le piùge n°1 : “ça marche avec Postman”

Postman / curl ne sont pas soumis Ă  la Same-Origin Policy. Donc l’API “rĂ©pond” mais le navigateur bloque la lecture. RĂ©sultat : console “CORS error” alors que le backend fonctionne.

Le piÚge n°2 : préflight invisible

Tu vois ton POST Ă©chouer mais la vraie erreur est souvent sur le OPTIONS envoyĂ© juste avant. Regarde dans DevTools → Network → requĂȘte OPTIONS.

Ressources de référence

- Page PyPI (description) : pypi.org/project/django-cors-headers
- Repo GitHub : github.com/adamchainz/django-cors-headers
- README (raw) : README.rst

Rùgle d’or Origines explicites Cookies = vigilance
1.2 — Architecture : le “flow” CORS de bout en bout
Schéma mental
[Frontend SPA] --fetch/axios--> [Navigateur]
        |                              |
        | (Origin: https://app...)      |  (applique SOP + CORS)
        |------------------------------>|
                                       |---- HTTP ----> [Django API]
                                       |               (répond avec Access-Control-*)
                                       |<--- HTTP -----|
        |<------ JS reçoit réponse si CORS OK ---------|

CORS est un contrat entre le navigateur et le serveur. Le backend “autorise” via headers, et le navigateur “fait respecter”.

Important : si tu testes depuis un serveur (SSR, backend-to-backend), la SOP ne s’applique pas. C’est pour cela qu’on “voit” les erreurs seulement cĂŽtĂ© browser.
Simple request (pas de préflight)

Le navigateur peut envoyer directement la requĂȘte si elle respecte des critĂšres “simples” (mĂ©thode, headers, content-type). Dans le doute, retiens surtout : JSON + headers custom dĂ©clenchent souvent un prĂ©flight.

GET https://api.example.com/v1/items
Origin: https://app.example.com

HTTP/200
Access-Control-Allow-Origin: https://app.example.com
PrĂ©flight (OPTIONS) : quand le navigateur “demande permission”
OPTIONS /v1/items HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

HTTP/204
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 86400

# Ensuite seulement, le navigateur envoie le POST réel.

Si le OPTIONS Ă©choue (403, 500, manque de headers), ton POST ne partira mĂȘme pas.

Position du middleware

django-cors-headers s’insĂšre comme middleware Django : il inspecte Origin et ajoute les headers CORS sur la rĂ©ponse. Il gĂšre aussi correctement les rĂ©ponses de prĂ©flight.

Ordre essentiel : le middleware CORS doit ĂȘtre placĂ© le plus haut possible (au minimum avant CommonMiddleware), afin que les rĂ©ponses (y compris erreurs) soient “corsifiĂ©es”. î„č
Pourquoi “avant CommonMiddleware” ?
  • CommonMiddleware peut rĂ©pondre tĂŽt (redirect slash, etc.).
  • Si la rĂ©ponse part sans headers CORS, le navigateur bloquera, et tu verras un “CORS error” au lieu du vrai statut.
2.1 — Installation + intĂ©gration Django (propre)
pip
# Dans ton venv
pip install django-cors-headers

# (Optionnel) pin version dans requirements
pip freeze | grep cors

Source : PyPI / repo officiel. î„č

settings.py : INSTALLED_APPS + MIDDLEWARE
INSTALLED_APPS = [
    # ...
    "corsheaders",
    # ...
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",  # CORS doit ĂȘtre avant CommonMiddleware
    # ...
]
Tip : place CorsMiddleware trĂšs haut pour que mĂȘme les rĂ©ponses d’erreur (403/404/500) aient les bons headers CORS.
DRF / API-only

Ça marche pareil. Le point clĂ© est l’ordre du middleware et la liste des origines.

Test rapide (curl)
curl -i https://api.example.com/health \
  -H "Origin: https://app.example.com"

Tu dois voir dans la réponse :

Access-Control-Allow-Origin: https://app.example.com
Test préflight
curl -i -X OPTIONS https://api.example.com/v1/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization"

Si tu ne vois pas Access-Control-Allow-Methods / Allow-Headers, ta config est incomplĂšte (ou le middleware trop bas).

Quickstart — configs prĂȘtes Ă  coller (dev & prod)
Config DEV (rapide, contrÎlée)

Objectif : autoriser uniquement ton front local (Vite/Next) + éventuellement un domaine de staging.

# settings_dev.py

INSTALLED_APPS += ["corsheaders"]

MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE

CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",   # Vite
    "http://localhost:3000",   # Next.js/React dev
]

# Souvent utile quand tu as Authorization: Bearer ...
CORS_ALLOW_HEADERS = [
    "accept",
    "authorization",
    "content-type",
    "origin",
    "user-agent",
    "x-csrftoken",
    "x-requested-with",
    "x-request-id",
]

# Si tu utilises cookies de session en dev (rare pour API token)
CORS_ALLOW_CREDENTIALS = True
DEV : Ă©vite CORS_ALLOW_ALL_ORIGINS=True sauf pour un POC trĂšs court. Ça masque des erreurs et t’emmĂšne vers une config prod dangereuse.
Config PROD (restrictive, robuste)

Objectif : autoriser précisément tes origines frontend, gérer credentials si besoin, et expliciter ce qui est exposé.

# settings_prod.py

INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE

CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://admin.example.com",
]

# Autoriser cookies / auth HTTP seulement si nécessaire
CORS_ALLOW_CREDENTIALS = True

# Headers que le browser est autorisé à envoyer
CORS_ALLOW_HEADERS = [
    "accept",
    "authorization",
    "content-type",
    "origin",
    "user-agent",
    "x-csrftoken",
    "x-requested-with",
    "x-request-id",
]

# Headers que le browser a le droit de lire cÎté JS
CORS_EXPOSE_HEADERS = [
    "content-disposition",  # download filename
    "x-request-id",         # corr_id
]

# Cache preflight (perf)
CORS_PREFLIGHT_MAX_AGE = 86400  # 24h
RÚgle : si CORS_ALLOW_CREDENTIALS=True, le serveur doit répondre avec une origine explicite, jamais *.
3.1 — Settings : le vrai “panneau de contrîle” CORS
Origines : liste ou regex

Tu choisis qui a le droit de lire ta réponse cÎté navigateur.

SettingButExemple
CORS_ALLOWED_ORIGINSListe explicite d’origines autorisĂ©es["https://app.example.com"]
CORS_ALLOWED_ORIGIN_REGEXESAutoriser dynamiquement (preview env, sous-domaines)[r"^https://.*\.preview\.example\.com$"]
CORS_ALLOW_ALL_ORIGINSAutoriser toutes les origines (dangereux)True (à éviter en prod)
# Exemple “preview deployments” (Netlify/Vercel-like)
CORS_ALLOWED_ORIGIN_REGEXES = [
  r"^https://[a-z0-9-]+--app\.example\.pages\.dev$",
  r"^https://pr-\d+\.preview\.example\.com$",
]
Anti‑pattern : autoriser des regex trop larges (ex: .*) revient Ă  un allow‑all dĂ©guisĂ©.
Méthodes & headers (préflight)

Le prĂ©flight vĂ©rifie si le browser peut envoyer certaines mĂ©thodes/headers. Si tu utilises Authorization, assure-toi qu’il est autorisĂ©.

# Autoriser des méthodes (souvent inutile de surcharger)
CORS_ALLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]

# Autoriser des headers custom (la liste ci-dessous est un bon “baseline”)
CORS_ALLOW_HEADERS = [
  "accept",
  "authorization",
  "content-type",
  "origin",
  "user-agent",
  "x-csrftoken",
  "x-requested-with",
  "x-request-id",
]
Exposer des headers au JS
CORS_EXPOSE_HEADERS = [
  "content-disposition",  # download
  "x-request-id",         # corr_id
  "x-rate-limit-remaining"
]
Si tu ne “exposes” pas un header, il est visible dans DevTools mais inaccessible en JS via response.headers.get().
Credentials (cookies / auth) : le point “dangereux”

CORS_ALLOW_CREDENTIALS permet au navigateur d’envoyer des cookies et d’exposer la rĂ©ponse au JS quand le frontend appelle l’API avec credentials: "include" (fetch) ou withCredentials: true (axios).

# Django
CORS_ALLOW_CREDENTIALS = True

# fetch (frontend)
fetch("https://api.example.com/v1/me", {
  method: "GET",
  credentials: "include",
})

# axios (frontend)
axios.get("https://api.example.com/v1/me", { withCredentials: true })
Interdit : Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true. Les navigateurs refusent par design. Et c’est une mauvaise idĂ©e sĂ©curitĂ©.
Cookies & SameSite (rappel)

Si tu relies CORS Ă  des cookies de session, tu dois aussi gĂ©rer SameSite / Secure cĂŽtĂ© cookie. Sinon “ça marche en local” mais pas en HTTPS / cross-site.

Scopes / exclusions / hooks

Tu peux exclure des URLs (ex: admin) ou appliquer une logique plus fine via signaux. (Typiquement : autoriser certains origins uniquement pour certains chemins).

# Exemple de pattern (si disponible selon version):
# CORS_URLS_REGEX = r"^/api/.*$"

# Et pour des cas avancés, regarder le repo (signals / configuration).

Pour les options avancĂ©es exactes, rĂ©fĂšre-toi au README officiel (settings list) : î„č README.rst

3.2 — PrĂ©flight : logique, erreurs, performance
Quand un préflight est déclenché ?

Le navigateur dĂ©clenche souvent un OPTIONS avant la requĂȘte “rĂ©elle” quand :

  • MĂ©thode ≠ GET/HEAD/POST “simple” (PUT/PATCH/DELETE
)
  • Content-Type non “simple” (souvent application/json)
  • Headers custom (ex: Authorization, X-Request-ID)
ConsĂ©quence : sur une API trĂšs chattĂ©e, les prĂ©flights peuvent doubler le nombre de requĂȘtes. D’oĂč l’intĂ©rĂȘt de CORS_PREFLIGHT_MAX_AGE (cache).
Cache preflight
# Django
CORS_PREFLIGHT_MAX_AGE = 86400

Ce paramĂštre se traduit en Access-Control-Max-Age. Attention : certains navigateurs imposent des plafonds.

Préflight qui échoue : symptÎmes typiques
SymptÎmeCause fréquenteFix
Console “CORS policy
” et ton POST n’apparaüt pasOPTIONS 403/404/500Mettre le middleware plus haut + autoriser headers/methods
OPTIONS 301/302Redirect slash / http→httpsCORS middleware avant CommonMiddleware + corriger URL
OPTIONS 405 Method not allowedEndpoint ne gÚre pas OPTIONSMiddleware CORS doit répondre / config DRF
OK en dev, KO en prodNginx supprime headers / cache / CDNInspecter headers au niveau final (curl -I)
Reproduire un préflight en curl
curl -i -X OPTIONS https://api.example.com/v1/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization, x-request-id"
4.1 — SĂ©curitĂ© : CORS vs Auth vs CSRF (ne pas confondre)
Clarification ultra importante
ConceptProtĂšge quoi ?OĂč ?
CORSEmpĂȘche un script browser non autorisĂ© de lire la rĂ©ponseNavigateur (SOP)
AuthN/AuthZContrîle l’accùs aux ressourcesServeur (Django)
CSRFEmpĂȘche des actions non voulues via cookies (forgery)Navigateur + serveur
Donc : “Activer CORS” ne rend pas ton API sĂ»re. Ton API doit ĂȘtre sĂ©curisĂ©e par authentification (JWT, session, API keys, OAuth...) + autorisations.
PiÚges sécurité (top)
  1. ALLOW_ALL en prod : expose ton API au JS de n’importe quel site (mauvais signe).
  2. Cookies + CORS mal réglé : fuite de session potentielle, confusion avec SameSite.
  3. Origines trop larges : regex permissive = attaque par sous-domaine contrÎlé.
  4. “CORS = firewall” : faux. Les requĂȘtes serveur‑à‑serveur passent.
  5. RĂ©ponses d’erreur sans CORS : tu dĂ©bogues Ă  l’aveugle (CORS error masque le vrai code).
Le cas “cookies cross-site” (le plus risquĂ©)

Si ton API utilise session cookies, un navigateur peut envoyer ces cookies automatiquement sur des requĂȘtes cross-site selon la politique SameSite/secure. CORS intervient seulement pour la lecture de la rĂ©ponse par le script.

Recommandation moderne : pour SPA séparée, préfÚre souvent JWT (Authorization header) ou OAuth, plutÎt que session cookies cross-site, sauf si tu maßtrises trÚs bien CSRF + SameSite + domaines.
Checklist “safe-ish” si cookies nĂ©cessaires
  • Origines explicitement listĂ©es (pas de wildcard)
  • CORS_ALLOW_CREDENTIALS=True
  • CSRF_TRUSTED_ORIGINS configurĂ© (si POST authentifiĂ©s via cookie)
  • Cookies marquĂ©s Secure en HTTPS
  • SameSite ajustĂ© selon architecture
Rappel : “CORS error” ≠ sĂ©curitĂ© compromise

Un “CORS error” indique le plus souvent une configuration manquante ou trop restrictive. Ce n’est pas un “hack”, juste le browser qui applique SOP/CORS.

4.2 — DRF / JWT / Cookies : patterns qui marchent
Pattern recommandé : JWT via Authorization header

C’est souvent le plus simple (et le plus clean) pour une SPA sĂ©parĂ©e : le browser envoie Authorization: Bearer ... donc tu dois autoriser ce header cĂŽtĂ© CORS.

# settings.py
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_HEADERS = [
  "accept","authorization","content-type","origin","user-agent",
  "x-requested-with","x-request-id",
]

# frontend (fetch)
fetch("https://api.example.com/v1/private", {
  headers: { "Authorization": "Bearer " + token },
})
Si tu oublies authorization dans CORS_ALLOW_HEADERS, tu auras un préflight qui échoue.
Pattern cookies : utile, mais plus “touchy”
# settings.py
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True

# frontend
axios.get("https://api.example.com/v1/me", { withCredentials: true })

En plus, si tu utilises CSRF, configure aussi Django :

# (selon Django)
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
Le couple “cookies cross-site + CSRF” peut devenir un sujet à part entiùre. Si tu veux un setup stable : documente-le et teste-le automatiquement (E2E).
DRF : points pratiques
  • Les vues DRF n’ont pas besoin de “gĂ©rer CORS” : c’est au middleware.
  • Si tu as un endpoint qui renvoie des fichiers (CSV/PDF), expose Content-Disposition.
  • Ajoute un X-Request-ID (corr_id) en reverse proxy/app, puis CORS_EXPOSE_HEADERS.
# Exposer Content-Disposition (downloads)
CORS_EXPOSE_HEADERS = ["content-disposition", "x-request-id"]
5.1 — Nginx / Reverse proxy : oĂč gĂ©rer CORS ?
Recommandation générale

Mets CORS dans Django via django-cors-headers : la logique est proche de l’app, versionnĂ©e, testable, et cohĂ©rente avec les routes.

Tu peux mettre CORS au proxy si :
  • plusieurs apps derriĂšre un gateway unique
  • tu veux normaliser / centraliser les headers
  • tu gĂšres des prĂ©flights au niveau edge/CDN
Anti‑patterns courants
  1. Doubles headers : Django ajoute, Nginx ajoute → incohĂ©rences.
  2. Nginx rĂ©pond au OPTIONS mais pas Django → ok en prod, KO en dev/staging.
  3. Cache/CDN qui sert une réponse avec mauvais Access-Control-Allow-Origin.
Snippet Nginx (si tu centralises)

Exemple “minimal” (Ă  adapter ; prĂ©fĂšre une whitelist stricte).

# location /api/ {
#   if ($request_method = OPTIONS) {
#     add_header Access-Control-Allow-Origin "https://app.example.com" always;
#     add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
#     add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Request-ID" always;
#     add_header Access-Control-Max-Age 86400 always;
#     return 204;
#   }
#   add_header Access-Control-Allow-Origin "https://app.example.com" always;
#   add_header Access-Control-Allow-Credentials "true" always;
#   proxy_pass http://upstream_django;
# }
Attention : gĂ©rer CORS au proxy est facile en apparence, mais compliquĂ© en cas de multi-origines (tu dois reflĂ©ter dynamiquement l’Origin, et vĂ©rifier qu’elle est autorisĂ©e).
5.2 — Tests : comment prouver que CORS est OK (et le garder OK)
curl : la mĂ©thode “chirurgicale”
# 1) simple request
curl -i https://api.example.com/v1/ping \
  -H "Origin: https://app.example.com"

# 2) preflight
curl -i -X OPTIONS https://api.example.com/v1/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization"

# 3) vérifier que l'origine refusée ne reçoit pas d'Allow-Origin
curl -i https://api.example.com/v1/ping \
  -H "Origin: https://evil.example"
Teste sur l’URL finale (celle qui passe par le proxy/CDN), sinon tu testes “le faux endroit”.
DevTools (Chrome/Firefox)
  • Network → filtrer “OPTIONS”
  • Cliquer la requĂȘte OPTIONS → vĂ©rifier Response Headers
  • Si 301/302 : vĂ©rifier redirect (slash, http→https)
  • Console : noter l’erreur exacte (souvent “blocked by CORS policy”)
Astuce : active “Preserve log” pour voir l’OPTIONS qui part juste avant le POST.
Tests Django / pytest (exemple minimal)
# tests/test_cors.py
import pytest

@pytest.mark.django_db
def test_cors_allows_known_origin(client, settings):
    settings.CORS_ALLOWED_ORIGINS = ["https://app.example.com"]

    resp = client.get("/v1/ping", HTTP_ORIGIN="https://app.example.com")
    assert resp.status_code in (200, 204)
    assert resp.headers.get("Access-Control-Allow-Origin") == "https://app.example.com"

@pytest.mark.django_db
def test_cors_blocks_unknown_origin(client, settings):
    settings.CORS_ALLOWED_ORIGINS = ["https://app.example.com"]

    resp = client.get("/v1/ping", HTTP_ORIGIN="https://evil.example")
    # Souvent: pas de header Allow-Origin => browser bloquera
    assert resp.headers.get("Access-Control-Allow-Origin") is None
L’objectif n’est pas de tester la lib, mais de verrouiller ta configuration dans le temps.
6.1 — Recettes de prod (patterns Ă©prouvĂ©s)
Pattern A : API publique + backoffice privé

- API publique consommĂ©e par plusieurs frontends → liste d’origines
- Admin interne → origine sĂ©parĂ©e + credentials

CORS_ALLOWED_ORIGINS = [
  "https://app.example.com",
  "https://admin.example.com",
  "https://partner.example.com",
]
CORS_ALLOW_CREDENTIALS = True
Pattern B : multi-environnements (dev/staging/prod)

Bon plan : déclarer les origins par env, via variables.

# settings.py
import os

CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS","").split(",")
CORS_ALLOWED_ORIGINS = [o.strip() for o in CORS_ALLOWED_ORIGINS if o.strip()]

# .env
CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
Attention aux espaces et aux virgules. Logue la liste au démarrage en staging (pas en prod).
Pattern C : preview deployments

Ton frontend a des URLs temporaires, il faut une regex stricte et “safe”.

CORS_ALLOWED_ORIGIN_REGEXES = [
  r"^https://pr-\d+\.preview\.example\.com$",
]
Pattern D : downloads (CSV/PDF)

Si tu veux lire le nom de fichier cÎté JS : expose Content-Disposition.

CORS_EXPOSE_HEADERS = ["content-disposition"]
Pattern E : Observabilité
  • Ajoute X-Request-ID (corr_id) partout
  • Expose-le via CORS pour debug front
  • Logue l’Origin sur erreurs CORS (en staging)
CORS_EXPOSE_HEADERS = ["x-request-id"]
Troubleshooting — diagnostic rapide (12 erreurs frĂ©quentes)
Arbre de décision (simple)
1) Est-ce que l’Origin envoyĂ©e est exactement celle que tu as whitelistĂ©e ? (scheme/port)
2) Est-ce que l’OPTIONS (prĂ©flight) rĂ©pond 2xx/204 ?
3) Est-ce que les headers CORS existent sur la réponse finale (proxy/CDN inclus) ?
4) Est-ce que tu mélanges cookies/CSRF/credentials ?
Top erreurs & solutions
Erreur / symptĂŽmeCauseFix
“No 'Access-Control-Allow-Origin' header
”Origin non autorisĂ©e, middleware absent ou trop basAjouter corsheaders + ordonner middleware + whitelist origin
Préflight 403CSRF / auth bloque OPTIONS, ou view interditMiddleware CORS haut + vérifier CSRF/permissions OPTIONS
PrĂ©flight 301/302Redirect (slash, http→https)Corriger URL + CORS avant CommonMiddleware
“Request header field authorization is not allowed
”Authorization pas dans allow-headersAjouter authorization à CORS_ALLOW_HEADERS
“Credentials flag is true but Allow-Origin is *”Wildcard + credentialsOrigine explicite (pas *)
Ça marche en local, pas en prodNginx/CDN modifie headers, ou origin diffĂ©rentecurl sur URL finale + inspecter Response Headers
Deux frontends, un seul marcheWhitelist incomplĂšteAjouter origin 2 + re‑tester prĂ©flight
Erreur seulement sur POST/PUTPréflight déclenché uniquement sur ces méthodesTester OPTIONS et allow-headers/methods
Download : filename introuvable en JSHeader non exposéCORS_EXPOSE_HEADERS=["content-disposition"]
Tout est ok sauf Safari (parfois)Cache/preflight/détails navigateurRéduire complexité, vérifier max-age, tester sans cache
Cookies non envoyĂ©sFront n’utilise pas credentials ou cookie SameSitecredentials include + config cookies/CSRF
RĂ©ponses 404/500 “masquĂ©es”RĂ©ponses d’erreur sans CORSRemonter CorsMiddleware + corriger chaĂźne proxy
Commandes “debug pack”
# Voir juste les headers CORS
curl -s -D - https://api.example.com/v1/ping \
  -H "Origin: https://app.example.com" -o /dev/null | egrep -i "access-control|vary|origin"

# Préflight complet
curl -s -D - -X OPTIONS https://api.example.com/v1/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization" -o /dev/null
Niveau & complexitĂ© — quand CORS devient difficile
Échelle de difficultĂ© (1 → 10)
NiveauSituationComplexité
2/10SPA et API sur 2 origines fixes, tokens JWTWhitelist + allow Authorization
4/10Plusieurs frontends + downloadsExpose headers, tests de non‑rĂ©gression
6/10Preview deployments (origines dynamiques)Regex stricte + risque sécurité
7/10Cookies cross-site + CSRFSameSite/CSRF trusted origins + E2E
8/10CDN / cache / edge proxy + multi-appsHeaders cohérents + Vary: Origin
9/10Multi-tenant (origines par client)Validation stricte + stockage + audit
Ce qui rend CORS “galùre”
  • DiffĂ©rences de scheme/port entre dev/staging/prod
  • PrĂ©flight masquĂ© derriĂšre des redirects
  • Headers supprimĂ©s par proxy/CDN
  • Credentials + cookies + CSRF (triple combo)
  • Regex trop permissives (sĂ©curitĂ©)
StratĂ©gie IDEO‑Lab : versionner la config, tester en CI, et avoir un “debug pack” curl + checklists.
Cheat‑sheet — prĂȘt Ă  coller (configs + commandes)
Baseline “API token” (recommandĂ©)
INSTALLED_APPS += ["corsheaders"]
MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware"] + MIDDLEWARE

CORS_ALLOWED_ORIGINS = [
  "https://app.example.com",
]

CORS_ALLOW_HEADERS = [
  "accept",
  "authorization",
  "content-type",
  "origin",
  "user-agent",
  "x-requested-with",
  "x-request-id",
]

CORS_EXPOSE_HEADERS = ["x-request-id"]
CORS_PREFLIGHT_MAX_AGE = 86400
Baseline “cookies”
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True
CORS_EXPOSE_HEADERS = ["x-request-id", "content-disposition"]
# + CSRF_TRUSTED_ORIGINS cÎté Django si besoin
curl pack
# Simple
curl -i https://api.example.com/v1/ping -H "Origin: https://app.example.com"

# Preflight (POST + Authorization)
curl -i -X OPTIONS https://api.example.com/v1/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type, authorization, x-request-id"

# Grep headers CORS
curl -s -D - https://api.example.com/v1/ping \
  -H "Origin: https://app.example.com" -o /dev/null | egrep -i "access-control|vary|origin"
Checklist DEV
  1. Whitelist exacte : scheme + port (ex: http://localhost:5173)
  2. authorization autorisé si JWT
  3. Préflight OPTIONS visible dans Network
  4. Middleware CORS avant CommonMiddleware
Checklist PROD
  1. Pas de ALLOW_ALL
  2. Origines explicitement listées (ou regex trÚs stricte)
  3. Si credentials : origine explicite + tests E2E
  4. curl sur URL finale (proxy/CDN inclus)
  5. Expose X-Request-ID pour debug
  6. Changelog lu lors des upgrades
fetch
// JWT
fetch("https://api.example.com/v1/private", {
  headers: { "Authorization": "Bearer " + token }
});

// Cookies
fetch("https://api.example.com/v1/me", {
  credentials: "include"
});
axios
// JWT
axios.get("https://api.example.com/v1/private", {
  headers: { Authorization: `Bearer ${token}` }
});

// Cookies
axios.get("https://api.example.com/v1/me", { withCredentials: true });