Gestion sécurisée des cookies avec Claude Code : sessions Next.js, CSRF et consentement
Implémentez des cookies Next.js sûrs avec Claude Code : HttpOnly, Secure, SameSite, CSRF, logout et consentement.
La gestion des cookies semble simple jusqu’au moment où elle devient la cause d’une prise de contrôle de compte. Une cookie de session n’est qu’une petite valeur dans un en-tête HTTP, mais le navigateur l’envoie automatiquement. C’est pratique pour l’authentification, et dangereux si les attributs sont mal choisis.
Claude Code peut écrire le code très vite. Le problème est qu’une demande vague comme “ajoute une cookie de login” laisse souvent des détails critiques : HttpOnly absent, SameSite=None sans Secure, logout qui ne supprime rien parce que le Path diffère, ou cookies d’analyse mélangés aux cookies d’authentification.
Ce guide part de Next.js App Router et donne un flux complet : inventaire des cookies, Route Handler copiable, logout, lecture côté serveur, CSRF, prévention de la fixation de session, comportement navigateur, frontière de consentement, commandes de vérification et liens officiels.
Commencer par l’inventaire
Avant d’écrire cookies().set(), nommez le but de chaque cookie. Une cookie d’authentification est une clé d’accès. Une cookie de préférence garde un état d’interface, comme le thème ou la langue. Une cookie d’analyse ou de publicité sert à mesurer et suivre. Les règles de sécurité et de consentement ne sont pas les mêmes.
| Usage | Exemple | Attributs recommandés | Frontière de consentement |
|---|---|---|---|
| Session d’authentification | __Host-session | HttpOnly, Secure, SameSite=Lax, Path=/, Max-Age court | Souvent nécessaire au service demandé, mais à vérifier selon la juridiction |
| Token CSRF | csrf-token | Secure, SameSite=Lax, Max-Age court | Cookie de sécurité, pas d’identifiant marketing |
| Préférence UI | theme, locale | Secure, SameSite=Lax, durée limitée | Explication selon votre politique et votre région |
| Analyse ou publicité | _ga, campaign ID | Déclenchée après consentement si requis | Séparée de login et checkout |
HttpOnly veut dire que JavaScript côté navigateur ne peut pas lire la cookie via document.cookie. Secure limite l’envoi à HTTPS, avec un traitement particulier de localhost. SameSite contrôle l’envoi dans les requêtes cross-site. Max-Age est une durée en secondes ; Expires est une date absolue.
MDN recommande dans Secure cookie configuration de limiter la portée avec Secure, HttpOnly, SameSite et les préfixes. La référence Set-Cookie précise aussi que SameSite=None nécessite Secure, et que Max-Age prime sur Expires si les deux sont définis.
Pour une session, préférez le préfixe __Host-. Les navigateurs compatibles n’acceptent une cookie __Host- que si elle a Secure, pas d’attribut Domain, et Path=/. Cela réduit le risque qu’un sous-domaine forge ou écrase l’identifiant de session.
Définir la cookie de session dans Next.js
La documentation Next.js de cookies décrit cookies() comme une fonction asynchrone et liste les options httpOnly, secure, sameSite, maxAge, path, domain, priority et partitioned. Les Server Components peuvent lire les cookies ; les modifications doivent passer par un Route Handler ou une Server Action.
Créez app/api/login/route.ts avec cet exemple. Le stockage en mémoire sert uniquement à tester. En production, remplacez-le par Redis, PostgreSQL, DynamoDB ou un autre stockage durable.
import { createHmac, randomBytes } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
export const runtime = "nodejs";
const env = z
.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
SESSION_SECRET: z.string().min(32),
})
.parse(process.env);
const SESSION_COOKIE = "__Host-session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8;
type SessionRecord = {
userId: string;
expiresAt: number;
};
declare global {
var demoSessions: Map<string, SessionRecord> | undefined;
}
const sessions = globalThis.demoSessions ?? new Map<string, SessionRecord>();
globalThis.demoSessions = sessions;
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(12),
});
function createSessionToken() {
const id = randomBytes(32).toString("base64url");
const signature = createHmac("sha256", env.SESSION_SECRET)
.update(id)
.digest("base64url");
return `${id}.${signature}`;
}
async function authenticate(email: string, password: string) {
if (email === "masa@example.com" && password === "correct-horse-battery-staple") {
return { id: "user_123" };
}
return null;
}
export async function POST(request: NextRequest) {
const body = loginSchema.safeParse(await request.json());
if (!body.success) {
return NextResponse.json({ error: "Invalid login payload" }, { status: 400 });
}
const user = await authenticate(body.data.email, body.data.password);
if (!user) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
const token = createSessionToken();
sessions.set(token, {
userId: user.id,
expiresAt: Date.now() + SESSION_MAX_AGE_SECONDS * 1000,
});
const response = NextResponse.json({ ok: true });
response.cookies.set({
name: SESSION_COOKIE,
value: token,
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: SESSION_MAX_AGE_SECONDS,
});
return response;
}
Le token est régénéré à chaque login réussi. C’est la défense de base contre la fixation de session : l’attaquant essaie de faire authentifier la victime avec un identifiant connu. OWASP détaille ce risque dans Session Fixation.
Logout et lecture serveur
Supprimer une cookie implique le même périmètre. Si vous l’avez créée avec Path=/, supprimez-la avec Path=/. Si vous avez utilisé Domain, le même Domain doit être repris. Avec __Host-, ne définissez pas Domain.
app/api/logout/route.ts:
import { NextResponse } from "next/server";
const SESSION_COOKIE = "__Host-session";
export async function POST() {
const response = NextResponse.json({ ok: true });
response.cookies.set({
name: SESSION_COOKIE,
value: "",
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 0,
});
return response;
}
En production, supprimez ou révoquez aussi la session côté serveur. Effacer uniquement la cookie du navigateur n’invalide pas forcément un token déjà volé.
Lecture côté serveur :
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
const SESSION_COOKIE = "__Host-session";
export default async function AccountPage() {
const cookieStore = await cookies();
const sessionToken = cookieStore.get(SESSION_COOKIE)?.value;
if (!sessionToken) {
redirect("/login");
}
return <main>Account dashboard</main>;
}
Ne considérez jamais l’existence d’une cookie comme preuve suffisante. Le serveur doit vérifier le stockage de session, l’expiration, la révocation, l’utilisateur et les droits.
CSRF n’est pas résolu par HttpOnly
CSRF signifie cross-site request forgery : un autre site pousse le navigateur authentifié à envoyer une action non voulue. Comme le navigateur attache les cookies automatiquement, HttpOnly protège contre la lecture par JavaScript, mais n’empêche pas l’envoi.
OWASP recommande dans CSRF Prevention Cheat Sheet des tokens pour les requêtes qui modifient l’état. Voici un helper de token signé lié à la session.
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
const CSRF_SECRET = process.env.SESSION_SECRET;
if (!CSRF_SECRET || CSRF_SECRET.length < 32) {
throw new Error("SESSION_SECRET must be at least 32 characters");
}
export function createCsrfToken(sessionToken: string) {
const nonce = randomBytes(16).toString("base64url");
const signature = createHmac("sha256", CSRF_SECRET)
.update(`${sessionToken}.${nonce}`)
.digest("base64url");
return `${nonce}.${signature}`;
}
export function verifyCsrfToken(sessionToken: string, token: string) {
const [nonce, signature] = token.split(".");
if (!nonce || !signature) return false;
const expected = createHmac("sha256", CSRF_SECRET)
.update(`${sessionToken}.${nonce}`)
.digest("base64url");
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Exigez ce token pour POST, PUT, PATCH et DELETE. Ne modifiez pas l’état avec GET. SameSite=Lax ajoute une couche utile, mais ne remplace pas un token, ni une vérification d’origine, ni une conception correcte des méthodes HTTP.
Comportement navigateur et expiration
Le navigateur filtre Set-Cookie pour le JavaScript frontend. Vous pouvez le voir avec DevTools ou curl -i, mais pas le lire proprement dans les headers de fetch(). Pour les requêtes cross-origin, CORS et credentials doivent aussi être cohérents.
Max-Age indique une durée relative en secondes. Expires indique une date. En application, Max-Age est souvent plus prévisible, car il dépend moins des horloges client et serveur. Si les deux sont présents, Max-Age gagne.
SameSite=Lax permet certaines navigations de premier niveau avec méthodes sûres. Un endpoint GET qui modifie l’état reste donc une faille. Strict protège davantage, mais peut gêner les parcours depuis un lien externe. None doit rester réservé aux vrais besoins cross-site, toujours avec Secure.
Frontière de consentement et cas d’usage
La frontière de consentement sépare les cookies nécessaires au service demandé de celles utilisées pour l’analyse, la publicité ou l’expérimentation. La politique cookies de la Commission européenne présente cette séparation entre préférence de consentement, authentification et analyse. Ce n’est pas un avis juridique, mais c’est une règle d’architecture : la sécurité d’authentification ne doit pas dépendre de l’acceptation marketing.
Cas 1 : login SaaS. Utilisez __Host-session, Max-Age court, révocation serveur, token CSRF et nouveau token après login. Pour l’administration ou la facturation, envisagez SameSite=Strict et une vérification supplémentaire.
Cas 2 : site de contenu avec PDF gratuit, produits et demande de conseil. Les clics CTA peuvent être mesurés, mais refuser l’analyse ne doit pas casser téléchargement, achat, login ou formulaire.
Cas 3 : préférence de langue ou de thème. Elle peut devoir être lisible par JavaScript, donc non HttpOnly. Ne mettez jamais dans ce type de cookie un token, un rôle, un prix ou un droit d’accès.
Échecs fréquents
Premier échec : __Host-session sans Secure, avec Domain, ou sans Path=/. Le préfixe ne joue plus son rôle.
Deuxième échec : logout inefficace parce que Path ou Domain ne correspond pas.
Troisième échec : croire que SameSite remplace CSRF. Il faut aussi token, méthode HTTP correcte et vérification côté serveur.
Quatrième échec : stocker les tokens dans localStorage ou une cookie lisible par le client. Une XSS suffit alors à les exposer.
Cinquième échec : bloquer les cookies de sécurité dans la bannière de consentement. Refuser l’analyse ne doit pas désactiver login, panier, checkout ou protection CSRF.
Prompt et vérification
Prompt recommandé :
Implémente une cookie de login Next.js App Router.
Exigences:
- nom: __Host-session
- attributs: HttpOnly, Secure, SameSite=Lax, Path=/, Max-Age
- ne jamais définir Domain
- générer un nouveau token à chaque login réussi
- supprimer avec Max-Age=0 et le même Path
- ne pas mélanger consentement analytique et authentification
- expliquer le token CSRF pour les requêtes qui modifient l'état
- vérifier avec MDN, Next.js et OWASP
Vérification du login :
curl -i -X POST http://localhost:3000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"masa@example.com","password":"correct-horse-battery-staple"}'
Forme attendue :
Set-Cookie: __Host-session=...; Path=/; Max-Age=28800; HttpOnly; Secure; SameSite=Lax
Vérification du logout :
curl -i -X POST http://localhost:3000/api/logout
La réponse doit contenir le même nom, Path=/ et Max-Age=0. Avec Playwright, inspectez context.cookies() pour httpOnly, secure, sameSite et l’expiration.
Liens, CTA et résultat testé
Pour l’authentification complète, poursuivez avec le guide d’implémentation de l’authentification, l’article sur l’authentification JWT et le guide d’audit de sécurité. Références officielles : MDN Set-Cookie, Next.js cookies, OWASP Session Management et OWASP CSRF Prevention.
Pour des prompts réutilisables, checklists et modèles de review, consultez les produits ClaudeCodeLab. Si votre équipe doit appliquer ces règles à un vrai dépôt avec consentement, checkout et revue sécurité, la suite logique est formation et conseil.
Après test, le gain principal vient du contrat explicite : __Host-, scope du logout, token CSRF et frontière d’analyse. Une demande courte comme “sécurise les cookies” produit encore trop souvent un code qui demande une correction manuelle.
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
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.