Al final de la lección anterior teníamos un iniciarSesion que verifica la contraseña de Lucía correctamente… y ahí se queda. La siguiente petición llega otra vez anónima, porque HTTP no tiene estado. Toca resolverlo, y lo hacemos primero con la técnica clásica: sesiones de servidor con cookie.
Escena Viva acabará usando JWT (lección 08-04), pero saltarse las sesiones sería un error pedagógico grave. Son la base conceptual de todo lo demás, siguen siendo la mejor opción para una enorme cantidad de aplicaciones web, y enseñan de primera mano los dos problemas que después reaparecen con los tokens: dónde vive el estado y cómo se revoca. Además aquí conocerás Passport.js, la pieza que verás en prácticamente cualquier proyecto Node con autenticación.
Contenido
- Cómo funciona una sesión de servidor
express-sessiony las opciones que importan- El almacén en memoria no vale en producción
- Fijación de sesión y
regenerate() - Cerrar sesión de verdad
- Passport.js: qué es y qué no es
- La estrategia local
serializeUserydeserializeUser- Montaje en
crearAplicacion()y ruta de login - El middleware
exigirSesion - CSRF: el precio de las cookies
- Estrategias de terceros y comparativa final
- Errores comunes, ejercicios y conclusión
- Cómo funciona una sesión de servidor
La idea es sencilla y muy antigua: el cliente guarda solo un identificador aleatorio; los datos viven en el servidor.
sequenceDiagram
participant N as Navegador
participant A as API Escena Viva
participant S as Almacen de sesiones
participant D as MongoDB
N->>A: POST /auth/login {email, contrasena}
A->>D: Buscar usuario (+hashContrasena)
A->>A: bcrypt.compare -> correcto
A->>A: req.session.regenerate() (nuevo id: anti fijacion)
A->>S: Guardar sesion { sid: aB3x..., usuarioId: '64f...' }
A-->>N: 200 + Set-Cookie: sid=aB3x...; HttpOnly; Secure; SameSite=Lax
N->>A: GET /api/pedidos (Cookie: sid=aB3x... automatica)
A->>S: Leer sesion por sid
S-->>A: { usuarioId: '64f...' }
A->>D: Cargar usuario -> req.user y consultar sus pedidos
A-->>N: 200 [pedidos de Lucia]
N->>A: POST /auth/logout
A->>S: destroy(sid)
A-->>N: 204 + Set-Cookie: sid=; Max-Age=0
Tres propiedades se derivan de este diseño. La cookie es opaca: aB3x9Kq... no significa nada fuera del servidor, y manipularla no sirve —o el identificador existe en el almacén o no existe—. La revocación es inmediata: borrar la fila del almacén expulsa al usuario en la siguiente petición, que es exactamente lo que los JWT no pueden hacer. Y el servidor guarda estado: con un solo proceso es trivial; con varios, deja de serlo (sección 3).
Por qué la cookie no debe contener datos del usuario. La tentación de meter {"usuarioId":"64f...","rol":"administrador"} en la cookie, aunque vaya firmada, tiene tres problemas: los datos quedan visibles para cualquiera con acceso al navegador o a un registro de proxy; quedan congelados (si degradas el rol de un organizador, su cookie sigue diciendo organizador hasta que caduque); y crecen sin control, encareciendo cada petición. La cookie lleva un identificador. Punto.
express-session y las opciones que importan
express-session y las opciones que importanInstalación: npm install express-session.
// src/middleware/sesion.js
'use strict';
const session = require('express-session');
const { configuracion } = require('../config/index.js');
function crearMiddlewareSesion() {
return session({
// Firma la cookie para detectar manipulaciones. Un array permite ROTAR
// el secreto: se firma con el primero y se verifica con todos, asi las
// sesiones vivas sobreviven al cambio.
secret: configuracion.secretosSesion,
// El nombre por defecto (connect.sid) anuncia la tecnologia usada.
name: 'ev.sid',
resave: false, // no reescribe si no ha cambiado: menos carreras
saveUninitialized: false, // no crea sesion para visitantes anonimos
rolling: true, // renueva maxAge en cada peticion: caduca por inactividad
cookie: {
httpOnly: true, // JavaScript no la lee: anti XSS
secure: configuracion.entorno === 'produccion', // solo HTTPS
sameSite: 'lax', // anti CSRF en POST entre sitios
maxAge: 30 * 60 * 1000, // 30 minutos de inactividad
path: '/',
},
});
}
module.exports = { crearMiddlewareSesion };En src/config/index.js, el único punto que lee process.env:
const secretosSesion = (process.env.SECRETOS_SESION || '').split(',').map((v) => v.trim()).filter(Boolean);
// Validacion al arrancar: mejor fallar al iniciar que servir inseguro.
if (entorno === 'produccion' && (secretosSesion.length === 0 || secretosSesion.some((s) => s.length < 32))) {
throw new Error('SECRETOS_SESION debe existir, con cada secreto de 32+ caracteres');
}| Opción | Valor | Por qué |
|---|---|---|
secret |
Array de 32+ caracteres aleatorios | Firma la cookie; el array permite rotación sin cerrar sesiones |
name |
ev.sid |
Oculta el connect.sid delator |
resave / saveUninitialized |
false / false |
Menos escrituras y carreras; no crear sesiones para anónimos |
rolling |
true |
Caducidad por inactividad, no absoluta |
cookie.httpOnly / secure |
true / true en producción |
Un XSS no roba la sesión; nunca en claro por la red |
cookie.sameSite / maxAge |
'lax' / 30 min |
Bloquea CSRF por POST externo; ventana corta si se filtra |
Nota sobre secure detrás de un proxy: si Express está tras Nginx o un balanceador que termina TLS, la conexión interna es HTTP y la cookie Secure no se enviaría. Hay que declarar app.set('trust proxy', 1) para que Express confíe en X-Forwarded-Proto. Es la misma configuración que necesita express-rate-limit para no ver todas las peticiones con la misma IP (lo retomamos en 08-06).
- El almacén en memoria no vale en producción
Esta es la advertencia grande de la lección. Sin configurar store, express-session usa MemoryStore, y el propio paquete lo desaconseja con todas las letras. Los tres problemas:
- Se pierde al reiniciar. Cada despliegue, cada reinicio de PM2, cada fallo del proceso echa a todos los usuarios. En mitad de una compra para el Festival de Jazz, eso es un carrito perdido.
- No funciona con varios procesos. Con
cluster(M10) o dos contenedores tras un balanceador, cada proceso tiene su propia memoria: el login lo atiende el proceso A, la siguiente petición el B, y el usuario aparece como no autenticado de forma intermitente. El síntoma clásico es «a veces me desloguea». - Fuga de memoria. No caduca las entradas de forma fiable; con tráfico real, la memoria crece hasta que el sistema mata el proceso.
La solución es un almacén compartido: Redis (connect-redis) es el estándar de hecho, y MongoDB también sirve (connect-mongo). El almacén de sesiones en Redis y el rate limiting distribuido son materia del módulo 10, junto con cluster. Aquí basta con grabar la regla: sesiones en memoria = desarrollo, y solo desarrollo. Y fíjate en la consecuencia conceptual, porque justifica la lección siguiente: las sesiones exigen infraestructura compartida, y ese coste es precisamente lo que los tokens autocontenidos evitan.
- Fijación de sesión y
regenerate()
regenerate()El ataque, paso a paso: el atacante visita Escena Viva y obtiene un identificador, sid=ATACANTE123; consigue que la víctima use ese mismo identificador (un enlace con el id en la URL, una cookie plantada desde un subdominio comprometido, un XSS); la víctima inicia sesión y, si el servidor conserva el identificador y solo le añade usuarioId, ahora ATACANTE123 es una sesión autenticada de Lucía; el atacante, que ya lo tenía, entra como ella.
La defensa es una línea, y es obligatoria:
// Al iniciar sesion, SIEMPRE identificador nuevo.
req.session.regenerate((error) => {
if (error) { return next(error); }
req.session.usuarioId = String(usuario._id);
// save() explicito: con almacen externo la escritura es asincrona, y sin
// esperarla la respuesta puede adelantarse a la persistencia.
req.session.save((errorGuardado) => {
if (errorGuardado) { return next(errorGuardado); }
res.json({ usuario: { id: usuario._id, nombre: usuario.nombre, rol: usuario.rol } });
});
});Dos detalles que se pasan por alto: regenerate destruye los datos previos de la sesión, así que si guardabas algo antes del login (un carrito, la URL a la que volver) hay que copiarlo a mano tras regenerar; y req.session.save() explícito antes de responder evita el clásico «el primer login no funciona, el segundo sí». Passport ofrece keepSessionInfo en req.login() precisamente para esto; por defecto regenera, que es lo correcto.
- Cerrar sesión de verdad
Cerrar sesión no es borrar la cookie: hay que destruir el estado en el servidor. Si solo borras la cookie, quien tuviera copia del identificador sigue dentro.
// src/controladores/autenticacion.js (fragmento)
function cerrarSesion(req, res, next) {
req.logout((errorLogout) => { // 1) Passport limpia req.user
if (errorLogout) { return next(errorLogout); }
req.session.destroy((errorDestroy) => { // 2) el sid deja de existir
if (errorDestroy) { return next(errorDestroy); }
// 3) Los atributos deben COINCIDIR con los del Set-Cookie original o
// el navegador la considerara otra cookie y no la borrara.
res.clearCookie('ev.sid', { httpOnly: true, sameSite: 'lax', path: '/' });
res.status(204).end();
});
});
}Conviene además ofrecer «cerrar sesión en todos los dispositivos»: implica borrar del almacén todas las sesiones con ese usuarioId, lo que con Redis se resuelve manteniendo un conjunto ev:usuario:<id>:sesiones. Es la operación que hay que ejecutar tras un cambio de contraseña (08-02) o ante sospecha de compromiso.
- Passport.js: qué es y qué no es
Instalación: npm install passport passport-local. Passport es un marco de estrategias, no un sistema de autenticación completo:
| Passport sí hace | Passport no hace |
|---|---|
| Normaliza más de 500 mecanismos (local, Google, SAML, JWT) tras una misma API | No hashea contraseñas: lo haces tú |
Convierte entre req.user y la sesión |
No gestiona usuarios ni registro |
Ofrece req.login(), req.logout(), req.isAuthenticated() |
No hace autorización ni roles |
| Se integra como middleware de Express | No protege de CSRF |
Es decir: Passport es el pegamento. La verificación de credenciales que escribiste en 08-02 sigue siendo tuya; Passport solo la envuelve con una interfaz uniforme para que cambiar «login con contraseña» por «login con Google» no reescriba la aplicación. ¿Merece la pena para una sola estrategia local? Honestamente, si solo vas a tener login con contraseña, un middleware propio de 20 líneas es más simple y más fácil de auditar. Passport gana cuando prevés añadir proveedores, y por su omnipresencia en código Node heredado.
- La estrategia local
// src/autenticacion/estrategia-local.js
'use strict';
const { Strategy: LocalStrategy } = require('passport-local');
const { verificarContrasena } = require('../servicios/contrasenas.js');
const HASH_SENUELO = '$2b$12$C6UzMDM.H6dfI/f/IKcEeO6iVQ9Lm2ZaZ8Xk1lF4a2iSg9WbLh9nK';
function crearEstrategiaLocal() {
return new LocalStrategy(
// Por defecto Passport espera 'username'/'password'. Renombramos a los
// campos que ya usan los esquemas zod de Escena Viva.
{ usernameField: 'email', passwordField: 'contrasena', session: true },
async (email, contrasena, hecho) => {
try {
const normalizado = String(email).trim().toLowerCase();
const usuario = await Usuario.findOne({ email: normalizado }).select('+hashContrasena');
// Senuelo: mismo coste temporal exista o no la cuenta (08-02).
const hash = usuario ? usuario.hashContrasena : HASH_SENUELO;
const coincide = await verificarContrasena(contrasena, hash);
if (!usuario || !coincide) {
// usuario=false significa "fallo de credenciales", no error del servidor.
return hecho(null, false, { mensaje: 'Credenciales incorrectas' });
}
if (!usuario.verificado) {
return hecho(null, false, { mensaje: 'La cuenta aun no esta verificada' });
}
// Objeto de dominio, no el documento de Mongoose (M7).
return hecho(null, { id: String(usuario._id), email: usuario.email,
nombre: usuario.nombre, rol: usuario.rol, salaAsignada: usuario.salaAsignada });
} catch (error) {
return hecho(error); // error real: 500, no 401
}
},
);
}
module.exports = { crearEstrategiaLocal };La firma hecho(error, usuario, info) distingue tres desenlaces y confundirlos es un error frecuente: hecho(error) es fallo del servidor (500), hecho(null, false, info) son credenciales incorrectas (401), y hecho(null, usuario) es éxito y establece req.user.
serializeUser y deserializeUser
serializeUser y deserializeUser// src/autenticacion/passport.js
'use strict';
const passport = require('passport');
const { Usuario } = require('../modelos/usuario.js');
const { crearEstrategiaLocal } = require('./estrategia-local.js');
function configurarPassport() {
passport.use('local', crearEstrategiaLocal());
// Que se guarda EN la sesion: solo el id. Nunca el objeto entero.
passport.serializeUser((usuario, hecho) => hecho(null, usuario.id));
// Como se reconstruye req.user en cada peticion, a partir del id.
passport.deserializeUser(async (id, hecho) => {
try {
const usuario = await Usuario.findById(id).lean();
// Cuenta borrada: la sesion queda invalidada sola.
if (!usuario) { return hecho(null, false); }
hecho(null, { id: String(usuario._id), email: usuario.email, nombre: usuario.nombre,
rol: usuario.rol, salaAsignada: usuario.salaAsignada });
} catch (error) { hecho(error); }
});
}
module.exports = { configurarPassport, passport };Por qué solo el id. Si serializas el usuario completo, guardas una fotografía congelada: al degradar a un organizador, su sesión seguiría diciendo organizador hasta caducar. Guardando el id y releyendo en cada petición, el rol siempre está fresco. Es exactamente el problema que los JWT tienen por diseño (08-04) y que aquí evitamos a cambio de una consulta por petición —consulta que se cachea con Redis (M10) cuando el tráfico lo exija.
- Montaje en
crearAplicacion() y ruta de login
crearAplicacion() y ruta de loginEl orden importa y es una fuente inagotable de errores:
// src/app.js (orden canonico, ampliado en el modulo 8)
function crearAplicacion(opciones = {}) {
const app = express();
app.set('trust proxy', 1); // detras de proxy: IP y protocolo reales
app.use(idPeticion); // M6: trazabilidad
app.use(helmet());
app.use(compression());
app.use(registroHttp); // morgan
app.use(corsListaBlanca); // nunca '*'
app.use(express.json({ limit: '100kb' }));
app.use(cookieParser()); // necesario para CSRF y cookies
app.use(crearMiddlewareSesion()); // 1) la sesion existe
app.use(passport.initialize()); // 2) Passport se engancha
app.use(passport.session()); // 3) rellena req.user desde la sesion
app.use(proteccionCsrf); // 4) despues de sesion y cookies
app.use('/auth', crearRutasAutenticacion());
app.use('/api', crearRutasApi());
app.use(noEncontrado);
app.use(manejadorDeErrores); // siempre el ultimo
return app;
}Las tres reglas de orden que hay que memorizar: session() antes de passport.session() (Passport lee de req.session, que aún no existiría); express.json() antes de las rutas de login (la estrategia local necesita req.body); y CSRF después de la sesión y del análisis de cookies, antes de las rutas que modifican estado.
// src/rutas/autenticacion.js (fragmento)
router.post('/login', limiteLogin, validar(esquemaLogin, ORIGENES_VALIDOS.CUERPO), (req, res, next) => {
// Callback personalizado: controlamos la respuesta en vez de dejar que
// Passport redirija (successRedirect/failureRedirect son para HTML).
passport.authenticate('local', (error, usuario, info) => {
if (error) { return next(error); }
if (!usuario) { return next(new ErrorDeAutenticacion(info?.mensaje)); }
// req.login establece la sesion y regenera el id (anti fijacion).
req.login(usuario, (errorLogin) => {
if (errorLogin) { return next(errorLogin); }
res.json({ usuario: { id: usuario.id, nombre: usuario.nombre, rol: usuario.rol } });
});
})(req, res, next); // authenticate devuelve un middleware: hay que invocarlo
});
router.post('/logout', exigirSesion, cerrarSesion);
- El middleware
exigirSesion
exigirSesion// src/middleware/exigir-sesion.js
'use strict';
const { ErrorDeAutenticacion } = require('../errores.js');
function exigirSesion(req, res, next) {
if (req.isAuthenticated && req.isAuthenticated() && req.user) {
// Normalizamos a req.usuario para que el resto de la aplicacion no
// dependa de si autenticamos con sesion (aqui) o con JWT (08-04).
req.usuario = req.user;
return next();
}
// 401: no autenticado. Formato de error del modulo 6.
next(new ErrorDeAutenticacion('Debes iniciar sesion para esta operacion'));
}
module.exports = { exigirSesion };Y así desaparece por fin el agujero del módulo 7:
// src/rutas/pedidos.js (fragmento)
router.post('/compras', exigirSesion, limiteCompra, validar(esquemaCompra), async (req, res, next) => {
const { sesionId, cantidad } = req.datosValidados.cuerpo;
// El usuarioId ya NO viene del cliente: viene de la sesion verificada.
const resultado = await comprarEntradas({ usuarioId: req.usuario.id, sesionId, cantidad });
res.status(201).json(resultado);
});Esa línea —usuarioId: req.usuario.id— es el objetivo de todo el módulo. El identificador ya no es un dato de entrada: es una conclusión del servidor.
- CSRF: el precio de las cookies
Las cookies se envían automáticamente en toda petición al dominio, la origine quien la origine. Esa comodidad es también la vulnerabilidad. El ataque contra Escena Viva: Marc, con sesión abierta, visita promos-baratas.test, que contiene un formulario oculto apuntando a https://api.escenaviva.test/api/compras con sesionId y cantidad, y un script que lo envía solo. El navegador adjunta la cookie de sesión de Marc: diez entradas compradas sin que él haga nada.
Por qué los tokens en cabecera no sufren esto: Authorization: Bearer ... no se envía sola. Otro sitio tendría que leer el token y añadirlo a mano, y el navegador se lo impide. Es una razón de peso a favor de los tokens en una API.
| Escenario | ¿Se envía la cookie con SameSite=Lax? |
|---|---|
Formulario POST desde otro sitio |
No — CSRF bloqueado |
fetch/XHR desde otro sitio |
No |
<img src> a un GET que modifica estado |
No |
Navegación con enlace GET desde otro sitio |
Sí — por eso ningún GET debe modificar estado |
| Petición desde un subdominio del mismo sitio | Sí — un subdominio comprometido sigue siendo un riesgo |
Lax cubre la mayoría, pero no basta por sí solo: no protege entre subdominios y los navegadores antiguos lo ignoran. Para operaciones sensibles se añade el patrón de token sincronizador: el servidor emite un token CSRF que el cliente debe reenviar en una cabecera; como otro sitio no puede leerlo, no puede reproducirlo.
// src/middleware/csrf.js -> npm install csrf-csrf
'use strict';
const { doubleCsrf } = require('csrf-csrf');
const { configuracion } = require('../config/index.js');
const { doubleCsrfProtection, generateCsrfToken } = doubleCsrf({
getSecret: () => configuracion.secretoCsrf,
// El token se liga a la sesion: no vale uno de otro usuario.
getSessionIdentifier: (req) => req.sessionID,
cookieName: '__Host-ev.csrf',
cookieOptions: { httpOnly: true, sameSite: 'lax', secure: true, path: '/' },
ignoredMethods: ['GET', 'HEAD', 'OPTIONS'], // no modifican estado
getCsrfTokenFromRequest: (req) => req.headers['x-csrf-token'],
});
module.exports = { doubleCsrfProtection, generateCsrfToken };Su lugar en la cadena: después de session() y cookieParser(), antes de las rutas. El cliente obtiene el token con un GET /auth/csrf y lo envía en X-CSRF-Token en cada POST, PATCH o DELETE. Importante para la lección siguiente: si Escena Viva autentica con Authorization: Bearer, esas rutas no necesitan CSRF; pero el token de refresco irá en cookie, así que POST /auth/refrescar sí queda expuesto, y por eso lo protegeremos con SameSite=Strict y Path restringido.
- Estrategias de terceros y comparativa final
Passport brilla cuando añades proveedores: passport-google-oauth20 o passport-github2 se montan con la misma forma que la estrategia local, recibiendo clientID, clientSecret y callbackURL, y devolviendo el Usuario correspondiente al perfil. El flujo de código de autorización con PKCE, resumido: el usuario pulsa «Entrar con Google» y la API redirige al proveedor con client_id, redirect_uri, scope y un state aleatorio; el usuario se autentica en Google y consiente; Google redirige a /auth/google/callback?code=...&state=...; la API comprueba que state coincide (es la defensa CSRF de OAuth) y canjea el code por tokens de servidor a servidor; y finalmente crea o localiza el Usuario y abre su propia sesión. Los dos puntos donde más se falla: no validar state y aceptar tokens enviados por el cliente en vez de canjear el código en el servidor. Escena Viva no implementa OAuth, pero el hueco está preparado: deserializeUser no distingue de dónde vino el usuario.
| Criterio | Sesión de servidor | Token autocontenido (JWT) |
|---|---|---|
| Estado en servidor | Sí (necesita Redis con varios procesos) | No (salvo lista de revocación) |
| Revocación | Inmediata | Difícil: hay que esperar a que caduque |
| Rol siempre actualizado | Sí (se relee) | No (congelado en el token) |
| Entre dominios / móvil | Incómodo | Natural |
| Vulnerable a CSRF | Sí (mitigable) | No, si va en cabecera |
| Vulnerable a XSS | La cookie HttpOnly no se roba |
Grave si se guarda en localStorage |
| Tamaño por petición | ~30 bytes | 300–800 bytes |
Cuándo cada una. Sesiones: aplicación web de un solo dominio, panel de administración, cualquier caso donde revocar al instante sea un requisito. Tokens: API pública, cliente móvil, varios front-ends en dominios distintos, servicio a servicio. Las dos: lo más habitual en sistemas reales, y lo que hará Escena Viva — el patrón de token de acceso corto más refresco en cookie es, en el fondo, una sesión con otro nombre.
Errores Comunes y Consejos
- Poner
saveUninitialized: truesin pensarlo. Crea una sesión y una cookie para cada robot que visita el catálogo: llena el almacén y complica el cumplimiento de la normativa de cookies. - Olvidar
req.session.regenerate()en el login. Deja abierta la fijación de sesión. - Responder antes de que la sesión se haya guardado. Con almacén externo produce el clásico «el primer login no funciona, el segundo sí».
res.clearCookiecon atributos distintos a los delSet-Cookieoriginal: el navegador no la borra y el usuario cree que ha salido.- Creer que
SameSite=Laxsustituye al token CSRF. Ayuda mucho; no basta entre subdominios. - Usar
successRedirecten una API JSON. Devuelve un 302 que el cliente no espera; usa el callback personalizado. - Consejo: normaliza siempre a
req.usuario—cuando en 08-04 metas JWT, los controladores y la autorización no se enterarán del cambio—, y en desarrollo arranca dos procesos en puertos distintos para ver fallar el almacén en memoria: entenderlo en carne propia vale más que leerlo diez veces.
Ejercicios
Ejercicio 1: auditar una configuración
Señala los problemas de esta configuración y corrígelos:
app.use(session({
secret: 'secreto',
resave: true,
saveUninitialized: true,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 },
}));
app.use(passport.session());
app.use(passport.initialize());Ejercicio 2: sesión inconsistente
Escena Viva se despliega con dos procesos tras un balanceador. Los usuarios informan de que «a veces» aparecen desconectados y de que al desplegar se les cierra la sesión. Explica la causa y describe la solución, indicando qué módulo del curso la desarrolla.
Ejercicio 3: middleware exigirVerificado
Escribe un middleware exigirVerificado que se ejecute después de exigirSesion y devuelva un 403 con el formato de error del módulo 6 si req.usuario.verificado es false. Justifica por qué es 403 y no 401.
Soluciones
Ejercicio 1. Seis problemas: secret: 'secreto' es corto, adivinable y está escrito en el código (debe venir de configuracion, con 32+ caracteres aleatorios y en array para rotar); resave: true reescribe la sesión en cada petición aunque no cambie, con carga inútil y carreras; saveUninitialized: true crea sesiones para anónimos; la cookie sin httpOnly la roba cualquier XSS; sin secure ni sameSite viaja en claro y queda expuesta a CSRF; y passport.session() está antes de passport.initialize(), orden invertido que no funciona. Además, 30 días de maxAge es excesivo para una sesión de compra.
app.use(session({
secret: configuracion.secretosSesion, name: 'ev.sid',
resave: false, saveUninitialized: false, rolling: true,
cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 60 * 1000, path: '/' },
store: almacenCompartido,
}));
app.use(passport.initialize());
app.use(passport.session());Ejercicio 2. La causa es el MemoryStore por defecto: cada proceso guarda las sesiones en su propia memoria, así que si el login lo atiende el proceso A y la siguiente petición el B, el identificador no existe allí y el usuario aparece como anónimo; al desplegar, la memoria se pierde entera y caen todas las sesiones. La solución es un almacén compartido y persistente: Redis con connect-redis (o connect-mongo sobre la base que ya tenemos). El almacén de sesiones en Redis y el rate limiting distribuido se desarrollan en el módulo 10, junto con cluster y los worker threads.
Ejercicio 3
// src/middleware/exigir-verificado.js
'use strict';
const { ErrorDeAutorizacion } = require('../errores.js');
function exigirVerificado(req, res, next) {
if (req.usuario && req.usuario.verificado) { return next(); }
next(new ErrorDeAutorizacion('Debes verificar tu correo antes de comprar entradas', {
accion: 'reenviar-verificacion',
}));
}
module.exports = { exigirVerificado };Es 403 y no 401 porque la identidad está perfectamente establecida: sabemos quién es y hemos verificado su credencial. Lo que falta es un permiso, no una autenticación. Devolver 401 haría que el cliente enviase al usuario otra vez al formulario de login, donde entraría bien y volvería a fallar: un bucle frustrante nacido de confundir los dos conceptos de la lección 08-01.
Conclusión
Ya sabemos mantener la identidad entre peticiones a la manera clásica. Una sesión de servidor es un identificador aleatorio en una cookie HttpOnly y un estado guardado en el servidor; la cookie es opaca a propósito, y por eso la revocación es inmediata y el rol siempre está fresco. Hemos configurado express-session con las opciones que de verdad deciden la seguridad, aprendido por qué el almacén en memoria solo sirve en desarrollo, cerrado la fijación de sesión con regenerate(), y cerrado sesión de verdad con destroy() más el borrado de la cookie. Passport nos ha dado una interfaz uniforme —estrategia local, serializeUser con solo el id, req.user— y hemos pagado el precio de las cookies con protección CSRF. Y sobre todo: comprarEntradas ya recibe un usuarioId que el servidor deduce, no que el cliente declara.
Pero Escena Viva es una API con front-end estático y un cliente móvil en el horizonte, y acabamos de ver el coste de las sesiones: estado compartido y encaje incómodo fuera del navegador. En la lección siguiente, Autenticación con JWT, cambiamos de enfoque: veremos qué hay dentro de un token, por qué firmar no es cifrar, cómo se evitan los ataques alg: none y de confusión de algoritmos, y —lo más importante— cómo se resuelve el problema real de los JWT, que es que no se pueden revocar, con un token de acceso corto y un token de refresco rotatorio guardado en cookie HttpOnly.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
