Guide CORS complet avec Claude Code : APIs cross-origin sécurisées
Configurez CORS avec Claude Code : preflight, credentials, allowlist d'origin, tests et prompts de revue.
Configurer CORS correctement avec Claude Code
Un frontend sur localhost:3000 et une API sur localhost:8787 suffisent à déclencher une erreur CORS dans le navigateur. Le réflexe courant consiste à ajouter Access-Control-Allow-Origin: *, mais cette solution devient dangereuse dès que l’API utilise des cookies, un header Authorization ou une interface d’administration.
CORS, Cross-Origin Resource Sharing, est le mécanisme qui permet à un serveur d’indiquer quelles autres origins peuvent lire ses réponses depuis du JavaScript exécuté dans le navigateur. Une origin combine scheme, host et port. https://app.example.com, https://api.example.com, http://localhost:3000 et http://localhost:5173 sont des origins différentes.
Ce guide découpe la configuration en décisions faciles à relire avec Claude Code. Vous y trouverez des exemples copiables pour Express, Fastify, Cloudflare Workers et Next.js Route Handler, ainsi que les règles de preflight, credentials, allowlist d’origin, commandes de test et prompts de revue.
Le point à retenir : CORS n’est pas une authentification. Il contrôle seulement si le JavaScript du navigateur peut lire une réponse cross-origin. Il ne bloque pas curl, les appels serveur à serveur ni les utilisateurs non autorisés. Authentification, autorisation, CSRF, rate limiting et headers de sécurité restent des contrôles séparés.
sequenceDiagram
participant Browser as Browser
participant API as API server
Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
API-->>Browser: 204 + Access-Control-Allow-*
Browser->>API: POST /api/messages<br/>Cookie or Authorization
API-->>Browser: 200 + Access-Control-Allow-Origin
Décisions avant le code
Définissez ces valeurs avant de demander du code CORS à Claude Code. Des exigences vagues produisent souvent des exemples trop permissifs pour la production.
| Décision | Exemple | Attention |
|---|---|---|
| Origins autorisées | https://app.example.com, https://admin.example.com | Pas de chemin ni de slash final |
| Credentials | Cookie, Authorization header | Avec des cookies, vérifier aussi SameSite=None; Secure |
| Méthodes | GET,POST,PUT,PATCH,DELETE,OPTIONS | N’autoriser que les méthodes utilisées |
| Headers | Content-Type,Authorization,X-Request-ID | Doivent correspondre au preflight |
Le preflight est la vérification envoyée par le navigateur avant la vraie requête. Pour un POST JSON, Authorization, PUT, DELETE ou des headers personnalisés, le navigateur envoie d’abord OPTIONS. Sans Access-Control-Allow-Methods et Access-Control-Allow-Headers compatibles, la requête réelle n’est pas envoyée.
Configuration Express
Cet exemple cible Node.js 20 ou plus. Le middleware officiel cors pour Express accepte une fonction dans origin, ce qui permet de vérifier chaque requête avec une allowlist. Comme l’API accepte des credentials, elle ne reflète que les origins autorisées et active credentials: true.
npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";
const app = express();
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
"http://localhost:5173",
]);
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.has(origin)) return true;
return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}
const corsOptions = {
origin(origin, callback) {
callback(null, isAllowedOrigin(origin));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
exposedHeaders: ["X-Request-ID"],
maxAge: 86400,
optionsSuccessStatus: 204,
};
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
return res.status(403).json({ error: "Origin not allowed" });
}
next();
});
app.use(cors(corsOptions));
app.use(express.json());
app.get("/api/health", (_req, res) => {
res.setHeader("X-Request-ID", crypto.randomUUID());
res.json({ ok: true });
});
app.post("/api/messages", (req, res) => {
res.setHeader("X-Request-ID", crypto.randomUUID());
res.json({ ok: true, received: req.body });
});
app.listen(8787, () => {
console.log("API listening on http://localhost:8787");
});
En production, exécutez avec NODE_ENV=production et conservez uniquement vos vrais domaines dans allowedOrigins. Les requêtes sans header Origin ne sont pas des requêtes CORS de navigateur ; l’exemple les laisse donc passer. Les API keys, JWT et permissions utilisateur doivent toujours être vérifiés par le middleware d’authentification.
Configuration Fastify
Fastify utilise @fastify/cors. Son README officiel autorise booléens, chaînes, tableaux, expressions régulières et fonctions pour origin, mais une correspondance exacte avec Set est plus simple à auditer. Évitez les regex larges sans raison solide.
npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";
const app = Fastify({ logger: true });
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
"http://localhost:5173",
]);
function isAllowedOrigin(origin) {
if (!origin) return true;
if (allowedOrigins.has(origin)) return true;
return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}
app.addHook("onRequest", async (request, reply) => {
const origin = request.headers.origin;
if (origin && !isAllowedOrigin(origin)) {
return reply.code(403).send({ error: "Origin not allowed" });
}
});
await app.register(cors, {
origin(origin, callback) {
callback(null, isAllowedOrigin(origin));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
exposedHeaders: ["X-Request-ID"],
maxAge: 86400,
strictPreflight: true,
});
app.get("/api/health", async () => ({ ok: true }));
app.post("/api/messages", async (request) => {
return { ok: true, received: request.body };
});
await app.listen({ port: 8787, host: "0.0.0.0" });
Dans Fastify, l’ordre des plugins et hooks compte. Si un hook d’authentification rejette OPTIONS avant que le plugin CORS réponde, le navigateur ne fera jamais la vraie requête. Demandez à Claude Code de relire aussi l’ordre d’enregistrement.
Configuration Cloudflare Workers
Cloudflare Workers expose l’API Fetch standard. Gérez OPTIONS explicitement, ajoutez les headers CORS sur les succès comme sur les erreurs, et utilisez Vary: Origin si la réponse varie selon l’origin.
// src/index.ts
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function getCorsHeaders(request: Request): HeadersInit | null {
const origin = request.headers.get("Origin");
if (!origin) return {};
if (!allowedOrigins.has(origin)) return null;
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
"Access-Control-Max-Age": "86400",
"Vary": "Origin",
};
}
export default {
async fetch(request: Request): Promise<Response> {
const corsHeaders = getCorsHeaders(request);
if (corsHeaders === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders });
}
const url = new URL(request.url);
if (url.pathname === "/api/messages" && request.method === "POST") {
const body = await request.json().catch(() => ({}));
return Response.json({ ok: true, received: body }, { headers: corsHeaders });
}
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
},
};
L’erreur fréquente avec Workers consiste à ajouter les headers seulement au chemin de succès. Si OPTIONS, 401, 403 ou 500 n’ont pas de CORS, DevTools peut n’afficher qu’un échec CORS et masquer l’erreur réelle.
Configuration Next.js Route Handler
Avec l’App Router, app/api/.../route.ts utilise les objets Web Request et Response. La documentation Next.js montre comment ajouter des headers CORS ; pour une API avec credentials, utilisez une allowlist au lieu de *.
// app/api/messages/route.ts
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function getCorsHeaders(request: Request): HeadersInit | null {
const origin = request.headers.get("Origin");
if (!origin) return {};
if (!allowedOrigins.has(origin)) return null;
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "POST,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
"Access-Control-Max-Age": "86400",
"Vary": "Origin",
};
}
export async function OPTIONS(request: Request) {
const headers = getCorsHeaders(request);
if (headers === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
return new Response(null, { status: 204, headers });
}
export async function POST(request: Request) {
const headers = getCorsHeaders(request);
if (headers === null) {
return Response.json({ error: "Origin not allowed" }, { status: 403 });
}
const body = await request.json().catch(() => ({}));
return Response.json({ ok: true, received: body }, { headers });
}
headers() dans next.config.js convient aux APIs publiques avec headers statiques. Si l’origin doit être évaluée par requête, la logique est plus lisible dans le Route Handler.
Commandes de test
Utilisez curl pour séparer preflight et vraie requête. Vérifiez que Access-Control-Allow-Origin correspond exactement au Origin envoyé et que Access-Control-Allow-Credentials: true n’apparaît que pour les origins autorisées.
curl -i -X OPTIONS http://localhost:8787/api/messages \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
curl -i -X POST http://localhost:8787/api/messages \
-H "Origin: http://localhost:3000" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer dev-token" \
--data '{"text":"hello"}'
curl -i -X OPTIONS http://localhost:8787/api/messages \
-H "Origin: https://evil.example" \
-H "Access-Control-Request-Method: POST"
Côté navigateur, un test avec credentials ressemble à ceci. Si credentials: "include" est utilisé, une réponse CORS wildcard sera rejetée par le navigateur.
await fetch("http://localhost:8787/api/messages", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer dev-token",
},
body: JSON.stringify({ text: "hello" }),
});
Cas d’usage concrets
Premier cas : une SPA et une API sur des domaines séparés. Si React est sur https://app.example.com et l’API sur https://api.example.com, une allowlist explicite est nécessaire. Avec des cookies de connexion, vérifiez credentials, les attributs de cookie et CSRF ensemble.
Deuxième cas : un frontend d’administration. Ajoutez https://admin.example.com à l’allowlist, mais ne confondez pas CORS et permission administrateur. L’autorisation doit rester dans l’API.
Troisième cas : un Cloudflare Worker utilisé comme BFF ou proxy léger. Le navigateur appelle le Worker, puis le Worker appelle l’API upstream. La réponse du Worker vers le navigateur doit toujours contenir les bons headers CORS.
Quatrième cas : une API publique en lecture seule. Sans cookies, sans Authorization et sans données privées, Access-Control-Allow-Origin: * peut être acceptable. Si l’authentification arrive plus tard, commencez dès maintenant avec une allowlist.
Pièges précis
| Piège | Résultat | Correction |
|---|---|---|
Combiner * et credentials: true | Le navigateur bloque la réponse | Retourner l’origin explicite |
Enregistrer https://app.example.com/ | Le slash final casse la correspondance | Garder https://app.example.com |
Autoriser seulement localhost | Les ports différents échouent | Écrire http://localhost:3000 |
Exiger l’auth sur OPTIONS | Preflight s’arrête en 401/403 | Traiter preflight avant l’auth |
| Oublier CORS sur les erreurs | DevTools masque la vraie erreur | Ajouter les headers aux 4xx/5xx |
| CDN cache des headers par origin | Les headers se mélangent | Ajouter Vary: Origin |
| Traiter CORS comme une autorisation | Les clients non navigateur appellent encore | Implémenter auth et CSRF à part |
MDN est clair : les requêtes CORS avec credentials ne peuvent pas utiliser Access-Control-Allow-Origin: *. Si Claude Code génère cette combinaison, considérez-la comme un bug.
Prompts de revue pour Claude Code
Review this repository's CORS configuration.
Check:
- No Access-Control-Allow-Origin: * when credentials are enabled
- Allowlist uses exact scheme/host/port matching
- OPTIONS preflight runs before authentication middleware
- 4xx/5xx responses include the required CORS headers
- Vary: Origin is present when responses vary by origin
If changes are needed, propose the smallest safe diff.
Diagnose this CORS error by cause.
Browser error:
<paste the full DevTools Console message>
curl preflight:
<paste curl -i -X OPTIONS output>
Expected origin:
https://app.example.com
Read the relevant API files and return reproduction steps, root cause, fix, and tests.
Review the Express/Fastify/Next.js/Workers CORS implementation as a security reviewer.
Focus on:
- Whether request origins are blindly reflected
- Whether localhost remains enabled in production
- Whether Authorization is allowed without proper authorization checks
- Whether cookie flows mention SameSite=None; Secure and CSRF protection
- Whether test commands separate preflight and the real request
Group findings as Critical, Must fix, and Improvement.
Références et liens internes
Prenez le guide CORS de MDN comme base. Pour l’implémentation, consultez Express cors middleware, @fastify/cors, les exemples CORS de Cloudflare Workers et Next.js Route Handlers. Pour les workflows réutilisables, voir Claude Code commands.
À lire aussi : le guide de développement API, le guide des headers de sécurité web, le guide Cloudflare Workers et la checklist de revue de code.
Prochaine étape
Après avoir remplacé les domaines d’exemple par les vôtres, lancez les prompts de revue sur votre dépôt et utilisez le guide des bonnes pratiques sécurité avec Claude Code pour vérifier cookies, CSRF, autorisation et headers ensemble. Pour un projet client ou une plateforme interne, cette checklist peut devenir un livrable de revue et soutenir une proposition d’accompagnement ou de templates réutilisables.
Résultat après test réel
Dans le test local de Masa, les exemples Express et Fastify ont tourné sur localhost:8787; le preflight et le POST depuis Origin: http://localhost:3000 ont réussi, tandis que https://evil.example a renvoyé 403. Les oublis les plus faciles concernaient les headers CORS sur les erreurs et le traitement explicite de OPTIONS dans Workers. Le flux le plus fiable a été d’implémenter l’allowlist, de lancer les curl, puis de demander à Claude Code de vérifier l’absence de wildcard avec credentials, la présence de Vary: Origin et l’impossibilité de laisser localhost en production.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.