Gestión segura de cookies con Claude Code: sesiones Next.js, CSRF y consentimiento
Implementa cookies seguras en Next.js con Claude Code: HttpOnly, Secure, SameSite, CSRF, logout y consentimiento.
La gestión de cookies parece una tarea pequeña hasta que una cuenta queda expuesta. Una cookie de sesión es solo un valor corto en una cabecera HTTP, pero el navegador la envía automáticamente. Eso la hace cómoda para autenticar y peligrosa cuando los atributos no están bien definidos.
Claude Code puede generar el código muy rápido, pero una instrucción vaga como “pon una cookie de login” suele dejar huecos. Puede faltar HttpOnly, aparecer SameSite=None sin Secure, fallar el logout porque se borra otro Path, o mezclarse la cookie de autenticación con cookies de analítica que dependen del consentimiento.
Esta guía muestra un flujo práctico con Next.js App Router: inventario de cookies, Route Handler copiable, logout, lectura en servidor, CSRF, prevención de session fixation, comportamiento del navegador, límite de consentimiento, comandos de verificación, enlaces oficiales y CTA de monetización.
Empieza con un inventario de cookies
Antes de elegir atributos, define el propósito. Una cookie de autenticación es una credencial. Una cookie de preferencia guarda estado de interfaz, como idioma o tema. Una cookie de analítica o publicidad pertenece a medición y seguimiento. No deberían vivir bajo la misma regla de consentimiento ni el mismo nivel de riesgo.
| Propósito | Ejemplo | Atributos recomendados | Límite de consentimiento |
|---|---|---|---|
| Sesión de autenticación | __Host-session | HttpOnly, Secure, SameSite=Lax, Path=/, Max-Age corto | Normalmente necesaria para el servicio solicitado, pero revisa la norma local |
| Token CSRF | csrf-token | Secure, SameSite=Lax, Max-Age corto | Cookie de apoyo de seguridad, no identificador de analítica |
| Preferencia UI | theme, locale | Secure, SameSite=Lax, vida limitada | Explicación según región y política |
| Analítica o anuncios | _ga, campaign ID | Solo después del consentimiento cuando sea obligatorio | Separada de login y checkout |
HttpOnly significa que JavaScript del navegador no puede leer la cookie con document.cookie. Secure limita el envío a HTTPS, con tratamiento especial para localhost. SameSite decide cuándo se adjunta la cookie en solicitudes entre sitios. Max-Age expresa la duración en segundos; Expires usa una fecha absoluta.
MDN recomienda en Secure cookie configuration limitar el alcance con Secure, HttpOnly, SameSite y prefijos. La referencia Set-Cookie explica además que SameSite=None requiere Secure, y que Max-Age tiene prioridad sobre Expires si ambos aparecen.
Para sesiones, usa el prefijo __Host- cuando puedas. En navegadores compatibles, una cookie __Host- necesita Secure, no puede tener Domain y debe usar Path=/. Esto reduce la posibilidad de que un subdominio sobrescriba el identificador de sesión.
Crear una cookie de sesión segura en Next.js
La documentación actual de Next.js para cookies describe cookies() como una API asíncrona y lista opciones como httpOnly, secure, sameSite, maxAge, path y domain. Los Server Components pueden leer cookies; las mutaciones deben hacerse en Route Handlers o Server Actions.
Crea app/api/login/route.ts con este ejemplo. Usa un Map en memoria para que puedas probar la cabecera al instante. En producción, reemplázalo por Redis, Postgres, DynamoDB u otro almacén persistente.
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;
}
El token se genera de nuevo en cada login correcto. Ese detalle evita session fixation, un ataque en el que la aplicación no cambia el identificador de sesión durante la autenticación. OWASP describe el problema en Session Fixation.
Logout y lectura del lado servidor
Borrar una cookie no consiste solo en usar el mismo nombre. El navegador también compara el alcance. Si la cookie se emitió con Path=/, el logout debe usar Path=/. Si se emitió con Domain, el borrado debe usar el mismo Domain. Con __Host-, no uses 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 una aplicación real, invalida también el registro de sesión en el servidor. Si solo borras la cookie del navegador, un token robado puede seguir siendo válido hasta que expire en el almacén.
Lectura en servidor:
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>;
}
No confundas “hay cookie” con “usuario autenticado”. El servidor debe validar existencia, vencimiento, revocación, usuario y permisos.
CSRF no se resuelve con HttpOnly
CSRF significa cross-site request forgery: otro sitio hace que el navegador autenticado envíe una solicitud no deseada a tu aplicación. Como el navegador adjunta cookies automáticamente, HttpOnly protege contra lectura por JavaScript, pero no impide que la cookie viaje.
OWASP recomienda en CSRF Prevention Cheat Sheet usar tokens para solicitudes que cambian estado. Este helper crea un token firmado ligado a la sesión.
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));
}
Envía el token en X-CSRF-Token o en un campo oculto para POST, PUT, PATCH y DELETE. No cambies estado desde GET. SameSite=Lax ayuda, pero no sustituye el token; XSS también puede romper mitigaciones de CSRF, así que revisa escape de salida y CSP.
Comportamiento del navegador y caducidad
El navegador no expone Set-Cookie a JavaScript de frontend. Lo verás en DevTools o con curl -i, no en las cabeceras leídas por fetch(). En solicitudes cross-origin, CORS y credentials deben estar alineados con los atributos de la cookie.
Max-Age dice cuántos segundos vive la cookie desde que se recibe. Expires usa una fecha exacta. Para código de aplicación, Max-Age suele ser más fácil y menos dependiente del reloj del cliente. Si ambos existen, Max-Age manda.
SameSite=Lax permite navegaciones superiores con métodos seguros, por eso un GET que cambia estado sigue siendo un error. Strict es más fuerte, pero puede afectar enlaces externos o correos. None debe reservarse para contextos cross-site reales y siempre con Secure.
Límite de consentimiento y casos de uso
El límite de consentimiento separa cookies necesarias para el servicio de cookies de analítica, publicidad o experimentos. La política de cookies de la Comisión Europea muestra esa separación práctica entre consentimiento, autenticación y analítica. No es asesoría legal, pero sí una regla técnica: no mezcles seguridad de autenticación con medición comercial.
Caso 1: login SaaS. Usa __Host-session, Max-Age corto, revocación de servidor, CSRF token y nuevo token al iniciar sesión. Para administración o facturación, considera SameSite=Strict y verificación adicional.
Caso 2: sitio de contenidos con PDF gratuito, productos y consultas. Puedes medir CTA, pero rechazar analítica no debe romper descarga, compra, login ni formulario.
Caso 3: preferencias de idioma o tema. A veces deben ser legibles por JavaScript, así que no serán HttpOnly. Nunca guardes tokens, roles, permisos, precios o entitlements en ellas.
Fallos frecuentes
El primer fallo es usar __Host-session sin Secure, con Domain o sin Path=/. El prefijo deja de cumplir su función.
El segundo es un logout que no borra nada porque Path o Domain no coinciden.
El tercero es confiar solo en SameSite para CSRF. Revisa token, método HTTP, Origin y endpoints GET.
El cuarto es poner tokens en localStorage o en cookies legibles por cliente. Un XSS los expone.
El quinto es bloquear cookies de seguridad desde el banner de consentimiento. Rechazar analítica no debe desactivar login, carrito, checkout ni CSRF.
Prompt y verificación
Una instrucción útil para Claude Code:
Implementa una cookie de login para Next.js App Router.
Requisitos:
- nombre: __Host-session
- atributos: HttpOnly, Secure, SameSite=Lax, Path=/, Max-Age
- no usar Domain
- generar nuevo token en cada login correcto
- borrar con Max-Age=0 y el mismo Path
- no mezclar consentimiento de analítica con autenticación
- explicar CSRF token para solicitudes que cambian estado
- revisar contra documentación oficial de MDN, Next.js y OWASP
Verifica el 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"}'
Cabecera esperada:
Set-Cookie: __Host-session=...; Path=/; Max-Age=28800; HttpOnly; Secure; SameSite=Lax
Verifica logout:
curl -i -X POST http://localhost:3000/api/logout
Debe devolver el mismo nombre, Path=/ y Max-Age=0. Con Playwright, usa context.cookies() para comprobar httpOnly, secure, sameSite y expiración.
Enlaces, CTA y resultado probado
Para el flujo completo de autenticación, sigue con la guía de autenticación con Claude Code, la comparación de autenticación JWT y la auditoría de seguridad. Referencias oficiales: MDN Set-Cookie, Next.js cookies, OWASP Session Management y OWASP CSRF Prevention.
Si quieres prompts reutilizables, checklists y plantillas de review, revisa los productos de ClaudeCodeLab. Si necesitas aplicar esto a un repositorio real con consentimiento, checkout y revisión de seguridad, la ruta natural es training y consultoría.
Al probar este flujo, la mejora más clara fue escribir el contrato de cookies antes de pedir cambios: __Host-, alcance del logout, CSRF token y límite de analítica. Con una petición vaga como “haz seguras las cookies”, Claude Code todavía dejaba detalles importantes para revisión manual.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.