Sessions versus JWTs: Elegir el modelo de autenticación adecuado para Node.js
Aprenda cómo difieren realmente las sesiones y los JWT en la autenticación de Node.js, cuáles son sus características respectivas, y cómo elegir entre ellos sin arrepentirse más tarde.
El problema subyacente en ambos enfoques
HTTP no cuenta con memoria integrada de nada entre una solicitud y otra. Cada solicitud entrante parece provenir de un completo desconocido a menos que lleve consigo una prueba de identidad. Tanto las sesiones como los JWT resuelven este problema de la misma manera fundamental: le proporcionan al cliente un dato que debe devolver en cada solicitud posterior. Lo que realmente los diferencia es la naturaleza de ese dato y, lo que es más importante, dónde se encuentra realmente el registro oficial de “quién está conectado”.
Opción uno: Sesiones
En un entorno basado en sesiones, el servidor es quien tiene la autoridad. Una vez que un usuario se conecta, el servidor crea un identificador de sesión aleatorio, guarda la información real del usuario asociada a ese identificador en algún lugar como Redis o una base de datos, y solo entrega al cliente el propio identificador, generalmente almacenado en una cookie.
app.post("/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
const sessionId = generateSecureId();
await redis.set(`session:${sessionId}`, JSON.stringify({ userId: user.id }), "EX", 86400);
res.cookie("sessionId", sessionId, { httpOnly: true, secure: true });
res.json({ success: true });
});
app.use(async (req, res, next) => {
const sessionId = req.cookies.sessionId;
const session = await redis.get(`session:${sessionId}`);
req.user = session ? JSON.parse(session).userId : null;
next();
});
A partir de ese momento, cada solicitud entrante desencadena una búsqueda en el lugar donde se almacenan los datos de la sesión. Ese único mecanismo explica tanto por qué las sesiones son útiles como por qué conllevan un costo adicional.
La ventaja: se obtiene un control inmediato y total sobre quiénes están conectados. Finalizar una sesión, ya sea por que un usuario cierra la sesión, se restablece la contraseña o un administrador desactiva una cuenta comprometida, consiste simplemente en borrarla del almacén de sesiones. No es necesario esperar a que algo caduque por sí solo.
El compromiso: ahora cada solicitud requiere un viaje de ida y vuelta al almacén de sesiones, lo que añade una pequeña pero real cantidad de latencia y carga en la infraestructura. Además, convierte el almacén de sesiones en un estado compartido al que deben acceder todos los servidores, lo cual se convierte en un problema arquitectónico real en cuanto se escala más allá de un solo servidor.
Opción dos: JWTs
app.post("/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "1h" });
res.cookie("token", token, { httpOnly: true, secure: true });
res.json({ success: true });
});
app.use((req, res, next) => {
try {
const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
req.user = decoded.userId;
} catch {
req.user = null;
}
next();
});
No hay llamadas a la base de datos ni a Redis. La firma criptográfica por sí sola es suficiente para confirmar que el token no ha sido manipulado desde su emisión.
Las ventajas: no se realizan búsquedas en la base de datos o en el caché por cada solicitud, lo que acelera significativamente las operaciones; además, no existe un almacén de sesiones compartido al que deban acceder todos los servidores, lo cual simplifica la escalabilidad horizontal y hace que las arquitecturas de microservicios sin estado sean mucho más fáciles de gestionar.
El compromiso: una vez que se ha entregado un JWT, el servidor no cuenta con una forma nativa de invalidarlo antes de que llegue su fecha de vencimiento. Si el token es robado, o si es necesario bloquear inmediatamente el acceso de un usuario, no hay nada que el servidor pueda eliminar, ya que en primer lugar nunca se almacenó nada de forma centralizada. Este es el compromiso que más a menudo toma por sorpresa a las personas, generalmente después de haber diseñado su sistema basándose en la idea de que un JWT es simplemente una versión más rápida y mejor de una sesión.
La idea errónea que causa problemas reales
La frase “Los JWTs no tienen estado” se utiliza con tanta frecuencia como argumento de venta sin matices que la gente pasa por alto lo que realmente implica: no tener estado también significa ser, por defecto, sin posibilidad de revocación. Si un cookie de sesión se ve comprometido, basta con eliminarlo. En cambio, un JWT robado permanece completamente válido y es plenamente confiable para tu servidor durante todo el tiempo que dure su ventana de vencimiento, a menos que hayas implementado medidas adicionales para evitarlo.
La solución típica a la que recurren las personas es mantener una lista de bloqueo con los tokens revocados:
app.use(async (req, res, next) => {
try {
const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
const isRevoked = await redis.get(`revoked:${decoded.jti}`);
if (isRevoked) throw new Error("Token revoked");
req.user = decoded.userId;
} catch {
req.user = null;
}
next();
});
Esto sí funciona, pero fíjese bien en lo que acaba de hacer: ha vuelto a implementar una verificación por solicitud contra un almacén de datos compartido, que es precisamente la carga adicional de la que los JWTs estaban destinados a liberarnos. En este punto ya no se trata realmente de un sistema sin estado. Lo que tiene es un sistema basado en sesiones disfrazado, con un modo de fallo aún más complicado por debajo.
Entonces, ¿cuál debería usar realmente?
Utilice sesiones cuando: necesite una revocación inmediata y fiable (piense en aplicaciones bancarias, paneles de administración o cualquier cosa donde los riesgos de seguridad sean altos), esté ejecutando una aplicación detrás de un balanceador de carga que pueda apuntar a un almacén de sesiones compartido sin muchos problemas, o si prefiere honestamente mantener una única fuente de verdad en lugar de lidiar con la duración de los tokens y la lógica de listas negras.
Utilice JWT cuando: esté gestionando la autenticación en servicios verdaderamente independientes que no necesitan acceder directamente a un almacén de sesiones compartido, esté creando un sistema donde mantener los tokens con vida por poco tiempo (minutos en lugar de días) hace que el período entre la revocación y su aplicación sea aceptable, o la razón real por la que desea un modelo sin estado es porque atiende a muchos consumidores de API independientes, y no solo porque suene más limpio.
Una nuanza importante: en la práctica, la mayoría de los sistemas de producción no eligen un lado de forma definitiva. El patrón al que tienden es el uso de JWTs de corta duración combinados con un token de actualización que se mantiene en el servidor. Se trata de una combinación y no de una opción binaria: las sesiones gestionan la confianza a largo plazo y su revocación, mientras que los JWTs cubren períodos breves de verificación sin estado entre ellas. Si consideras que “sesión versus JWT” es una decisión de tipo o bien/o mal, eso suele ser señal de que aún no has alcanzado el nivel de escala en el que este enfoque híbrido justifique su complejidad, lo que significa que una simple sesión es probablemente el punto de partida más honesto y sencillo.
La decisión real
Bajo todo esto, la cuestión nunca fue puramente técnica; en realidad se trata de decidir dónde se absorbe el costo. Las sesiones cobran ese costo en cada solicitud, pero a cambio se obtiene un control siempre actual, nunca obsoleto. Los JWT eliminan ese costo por solicitud, pero el precio que se paga es un período de tiempo en el que lo que cree tu servidor y lo que realmente ocurre pueden separarse silenciosamente. Ninguno de estos enfoques merece ser etiquetado como “moderno” o “obsoleto”, sin importar cómo se enmarque la discusión alrededor de ellos. Simplemente son dos formas diferentes de abordar el mismo compromiso que nunca desaparece realmente.
Lecturas relacionadas
- Estrategia de Token de Refresco para Sistemas de Autenticación en Node.js — Aprenda cómo diseñar, rotar, revocar y almacenar de forma segura los tokens de refresco en Node.js para que el robo de tokens y el cierre de sesión funcionen como se espera.
- Explicación de Flujos en Node.js: Cómo Arreglar Caídas por Falta de Memoria al Tratar Archivos — Entienda por qué cargar archivos completos en la memoria provoca caídas en los servidores de Node.js y cómo los flujos legibles, escribibles, duales y de transformación solucionan este problema mediante la contrapresión.