Ir al contenido
angel.library

Cookies vs sesiones — JWT en cookie o session store

Una vez que el servidor autenticó al usuario, hace falta que las siguientes requests “recuerden” quién es — hay dos formas principales de resolver esto, con trade-offs distintos.

En vez de devolver el JWT en el body de la respuesta (donde JavaScript del cliente lo tendría que guardar en localStorage), se manda como cookie:

Ventana de terminal
pnpm add cookie-parser
import cookieParser from "cookie-parser"
app.use(cookieParser())
app.post("/login", async (req, res) => {
// ... verificar credenciales ...
const token = firmarToken({ sub: usuario.id, rol: usuario.rol })
res.cookie("token", token, {
httpOnly: true, // JavaScript del navegador NO puede leer esta cookie (protege contra XSS)
secure: true, // solo se manda por HTTPS
sameSite: "lax", // limita cuándo se manda en requests cross-site (protege contra CSRF)
maxAge: 60 * 60 * 1000 // 1 hora, en ms
})
res.json({ ok: true })
})
// Leer el token en requests siguientes
app.get("/perfil", (req, res) => {
const token = req.cookies.token
const payload = verificarToken(token)
res.json({ userId: payload.sub })
})

httpOnly: true es la razón principal para preferir cookie sobre localStorage: un JWT en localStorage es legible por cualquier script que corra en la página (incluyendo uno inyectado por un XSS) — una cookie httpOnly no, el navegador la manda automáticamente pero JavaScript no puede tocarla.

Opción 2: sesión tradicional (server-side)

Sección titulada «Opción 2: sesión tradicional (server-side)»

En vez de un JWT autocontenido, la cookie solo guarda un id de sesión random — los datos reales (quién es el usuario) viven en el servidor (memoria, Redis, base de datos).

Ventana de terminal
pnpm add express-session
import session from "express-session"
app.use(
session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, maxAge: 60 * 60 * 1000 }
})
)
app.post("/login", async (req, res) => {
// ... verificar credenciales ...
req.session.userId = usuario.id // Express guarda esto server-side, la cookie solo tiene el id de sesión
res.json({ ok: true })
})
app.get("/perfil", (req, res) => {
if (!req.session.userId)
return res.status(401).json({ error: "No autenticado" })
res.json({ userId: req.session.userId })
})
JWT (autocontenido)Sesión server-side
VerificarSin tocar la base — solo la firmaConsulta al store de sesiones (Redis/DB) en cada request
Cerrar sesión antes de que expireNo es directo (ver nota en JWT)Trivial — borrar esa sesión del store
Escala horizontalSin estado compartido — cualquier instancia del servidor puede verificarloNecesita un store compartido (Redis) entre instancias

No hay una respuesta universalmente “mejor” — JWT gana en no depender de un store centralizado; sesión server-side gana en poder revocar acceso inmediatamente (crítico para “cerrar sesión en todos los dispositivos”, o banear una cuenta al instante).

Cookie optionQué hace
httpOnly: trueJavaScript del navegador no puede leer la cookie (protege contra XSS)
secure: trueSolo se manda por HTTPS
sameSite: 'lax' | 'strict'Limita cuándo se manda en requests cross-site (protege contra CSRF)
maxAgeCuánto dura antes de expirar, en ms
  • sameSite: 'strict' bloquea incluso navegación normal desde un link externo (el usuario llega “deslogueado” la primera vez); 'lax' (el default recomendado) permite eso pero sigue bloqueando requests cross-site iniciadas por scripts o forms — el balance correcto para la mayoría de los casos.
  • En aplicaciones full-stack del mismo origen, Better Auth y Auth.js pueden implementar estas decisiones con opciones seguras configurables. Aun así, conviene comprender el modelo para revisar sus cookies y expiración.