Al final de la lliçó anterior teníem un iniciarSessio que verifica la contrasenya de la Lucía correctament… i aquí es queda. La petició següent arriba un altre cop anònima, perquè l'HTTP no té estat. Toca resoldre-ho, i ho fem primer amb la tècnica clàssica: sessions de servidor amb galeta.

Escena Viva acabarà fent servir JWT (lliçó 08-04), però saltar-se les sessions seria un error pedagògic greu. Són la base conceptual de tota la resta, continuen sent la millor opció per a una quantitat enorme d'aplicacions web, i ensenyen de primera mà els dos problemes que després reapareixen amb els tokens: on viu l'estat i com es revoca. A més, aquí coneixeràs Passport.js, la peça que veuràs pràcticament a qualsevol projecte Node amb autenticació.

Contingut

  1. Com funciona una sessió de servidor
  2. express-session i les opcions que importen
  3. El magatzem en memòria no val en producció
  4. Fixació de sessió i regenerate()
  5. Tancar la sessió de debò
  6. Passport.js: què és i què no és
  7. L'estratègia local
  8. serializeUser i deserializeUser
  9. Muntatge a crearAplicacio() i ruta d'entrada
  10. El middleware exigirSessio
  11. CSRF: el preu de les galetes
  12. Estratègies de tercers i comparativa final
  13. Errors comuns, exercicis i conclusió

  1. Com funciona una sessió de servidor

La idea és senzilla i molt antiga: el client desa només un identificador aleatori; les dades viuen al servidor.

sequenceDiagram
  participant N as Navegador
  participant A as API Escena Viva
  participant S as Magatzem de sessions
  participant D as MongoDB

  N->>A: POST /auth/entrada {correu, contrasenya}
  A->>D: Cercar usuari (+hashContrasenya)
  A->>A: bcrypt.compare -> correcte
  A->>A: req.session.regenerate() (nou id: anti fixacio)
  A->>S: Desar sessio { sid: aB3x..., usuariId: '64f...' }
  A-->>N: 200 + Set-Cookie: sid=aB3x...; HttpOnly; Secure; SameSite=Lax

  N->>A: GET /api/comandes (Cookie: sid=aB3x... automatica)
  A->>S: Llegir sessio per sid
  S-->>A: { usuariId: '64f...' }
  A->>D: Carregar usuari -> req.user i consultar les seves comandes
  A-->>N: 200 [comandes de Lucia]

  N->>A: POST /auth/sortida
  A->>S: destroy(sid)
  A-->>N: 204 + Set-Cookie: sid=; Max-Age=0

Tres propietats es deriven d'aquest disseny. La galeta és opaca: aB3x9Kq... no significa res fora del servidor, i manipular-la no serveix —o l'identificador existeix al magatzem o no hi existeix—. La revocació és immediata: esborrar la fila del magatzem expulsa l'usuari a la petició següent, que és exactament el que els JWT no poden fer. I el servidor desa estat: amb un sol procés és trivial; amb diversos, deixa de ser-ho (secció 3).

Per què la galeta no ha de contenir dades de l'usuari. La temptació de posar {"usuariId":"64f...","rol":"administrador"} a la galeta, encara que vagi signada, té tres problemes: les dades queden visibles per a qualsevol amb accés al navegador o a un registre de proxy; queden congelades (si degrades el rol d'un organitzador, la seva galeta continua dient organitzador fins que caduqui); i creixen sense control, encarint cada petició. La galeta porta un identificador. Punt.

  1. express-session i les opcions que importen

Instal·lació: npm install express-session.

// src/middleware/sessio.js
'use strict';
const session = require('express-session');
const { configuracio } = require('../config/index.js');
function crearMiddlewareSessio() {
  return session({
    // Signa la galeta per detectar manipulacions. Un array permet ROTAR
    // el secret: es signa amb el primer i es verifica amb tots, aixi les
    // sessions vives sobreviuen al canvi.
    secret: configuracio.secretsSessio,
    // El nom per defecte (connect.sid) anuncia la tecnologia emprada.
    name: 'ev.sid',
    resave: false,            // no reescriu si no ha canviat: menys curses
    saveUninitialized: false, // no crea sessio per a visitants anonims
    rolling: true,            // renova maxAge a cada peticio: caduca per inactivitat
    cookie: {
      httpOnly: true,                              // JavaScript no la llegeix: anti XSS
      secure: configuracio.entorn === 'produccio', // nomes HTTPS
      sameSite: 'lax',                             // anti CSRF en POST entre llocs
      maxAge: 30 * 60 * 1000,                      // 30 minuts d'inactivitat
      path: '/',
    },
  });
}
module.exports = { crearMiddlewareSessio };

A src/config/index.js, l'únic punt que llegeix process.env:

const secretsSessio = (process.env.SESSIO_SECRETS || '').split(',').map((v) => v.trim()).filter(Boolean);
// Validacio en arrencar: millor fallar en iniciar que servir insegur.
if (entorn === 'produccio' && (secretsSessio.length === 0 || secretsSessio.some((s) => s.length < 32))) {
  throw new Error("SESSIO_SECRETS ha d'existir, amb cada secret de 32+ caracters");
}
Opció Valor Per què
secret Array de 32+ caràcters aleatoris Signa la galeta; l'array permet rotació sense tancar sessions
name ev.sid Amaga el connect.sid delator
resave / saveUninitialized false / false Menys escriptures i curses; no crear sessions per a anònims
rolling true Caducitat per inactivitat, no absoluta
cookie.httpOnly / secure true / true en producció Un XSS no roba la sessió; mai en clar per la xarxa
cookie.sameSite / maxAge 'lax' / 30 min Bloqueja CSRF per POST extern; finestra curta si es filtra

Nota sobre secure darrere d'un proxy: si Express és darrere d'Nginx o d'un balancejador que acaba el TLS, la connexió interna és HTTP i la galeta Secure no s'enviaria. Cal declarar app.set('trust proxy', 1) perquè Express confiï en X-Forwarded-Proto. És la mateixa configuració que necessita express-rate-limit per no veure totes les peticions amb la mateixa IP (ho reprenem a 08-06).

  1. El magatzem en memòria no val en producció

Aquest és l'advertiment gros de la lliçó. Sense configurar store, express-session fa servir MemoryStore, i el paquet mateix el desaconsella amb totes les lletres. Els tres problemes:

  1. Es perd en reiniciar. Cada desplegament, cada reinici de PM2, cada fallada del procés fa fora tots els usuaris. Enmig d'una compra per al Festival de Jazz, això és un carretó perdut.
  2. No funciona amb diversos processos. Amb cluster (M10) o dos contenidors darrere d'un balancejador, cada procés té la seva pròpia memòria: l'entrada l'atén el procés A, la petició següent el B, i l'usuari apareix com a no autenticat de manera intermitent. El símptoma clàssic és «de vegades em tanca la sessió».
  3. Fuita de memòria. No caduca les entrades de manera fiable; amb trànsit real, la memòria creix fins que el sistema mata el procés.

La solució és un magatzem compartit: Redis (connect-redis) és l'estàndard de facto, i MongoDB també serveix (connect-mongo). El magatzem de sessions a Redis i la limitació de peticions distribuïda són matèria del mòdul 10, juntament amb cluster. Aquí n'hi ha prou de gravar la regla: sessions en memòria = desenvolupament, i només desenvolupament. I fixa't en la conseqüència conceptual, perquè justifica la lliçó següent: les sessions exigeixen infraestructura compartida, i aquest cost és precisament el que els tokens autocontinguts eviten.

  1. Fixació de sessió i regenerate()

L'atac, pas a pas: l'atacant visita Escena Viva i obté un identificador, sid=ATACANT123; aconsegueix que la víctima faci servir aquest mateix identificador (un enllaç amb l'id a l'URL, una galeta plantada des d'un subdomini compromès, un XSS); la víctima inicia sessió i, si el servidor conserva l'identificador i només hi afegeix usuariId, ara ATACANT123 és una sessió autenticada de la Lucía; l'atacant, que ja el tenia, hi entra com ella.

La defensa és una línia, i és obligatòria:

// En iniciar sessio, SEMPRE identificador nou.
req.session.regenerate((error) => {
  if (error) { return next(error); }
  req.session.usuariId = String(usuari._id);
  // save() explicit: amb magatzem extern l'escriptura es asincrona, i sense
  // esperar-la la resposta es pot avancar a la persistencia.
  req.session.save((errorDesat) => {
    if (errorDesat) { return next(errorDesat); }
    res.json({ usuari: { id: usuari._id, nom: usuari.nom, rol: usuari.rol } });
  });
});

Dos detalls que es passen per alt: regenerate destrueix les dades prèvies de la sessió, així que si hi desaves alguna cosa abans de l'entrada (un carretó, l'URL on tornar) cal copiar-la a mà després de regenerar; i req.session.save() explícit abans de respondre evita el clàssic «la primera entrada no funciona, la segona sí». Passport ofereix keepSessionInfo a req.login() precisament per això; per defecte regenera, que és el correcte.

  1. Tancar la sessió de debò

Tancar la sessió no és esborrar la galeta: cal destruir l'estat al servidor. Si només esborres la galeta, qui tingués còpia de l'identificador continua a dins.

// src/controladors/autenticacio.js  (fragment)
function tancarSessio(req, res, next) {
  req.logout((errorSortida) => {                   // 1) Passport neteja req.user
    if (errorSortida) { return next(errorSortida); }
    req.session.destroy((errorDestroy) => {        // 2) el sid deixa d'existir
      if (errorDestroy) { return next(errorDestroy); }
      // 3) Els atributs han de COINCIDIR amb els del Set-Cookie original o
      //    el navegador la considerara una altra galeta i no l'esborrara.
      res.clearCookie('ev.sid', { httpOnly: true, sameSite: 'lax', path: '/' });
      res.status(204).end();
    });
  });
}

A més, convé oferir «tancar la sessió a tots els dispositius»: implica esborrar del magatzem totes les sessions amb aquell usuariId, cosa que amb Redis es resol mantenint un conjunt ev:usuari:<id>:sessions. És l'operació que cal executar després d'un canvi de contrasenya (08-02) o davant d'una sospita de compromís.

  1. Passport.js: què és i què no és

Instal·lació: npm install passport passport-local. Passport és un marc d'estratègies, no un sistema d'autenticació complet:

Passport sí que fa Passport no fa
Normalitza més de 500 mecanismes (local, Google, SAML, JWT) darrere d'una mateixa API No fa el hash de contrasenyes: ho fas tu
Converteix entre req.user i la sessió No gestiona usuaris ni registre
Ofereix req.login(), req.logout(), req.isAuthenticated() No fa autorització ni rols
S'integra com a middleware d'Express No protegeix del CSRF

És a dir: Passport és el pegament. La verificació de credencials que vas escriure a 08-02 continua sent teva; Passport només l'embolcalla amb una interfície uniforme perquè canviar «entrada amb contrasenya» per «entrada amb Google» no reescrigui l'aplicació. Val la pena per a una sola estratègia local? Honestament, si només tindràs entrada amb contrasenya, un middleware propi de 20 línies és més simple i més fàcil d'auditar. Passport guanya quan preveus afegir proveïdors, i per la seva omnipresència en codi Node heretat.

  1. L'estratègia local

// src/autenticacio/estrategia-local.js
'use strict';
const { Strategy: LocalStrategy } = require('passport-local');
const { verificarContrasenya } = require('../serveis/contrasenyes.js');
const HASH_ESQUER = '$2b$12$C6UzMDM.H6dfI/f/IKcEeO6iVQ9Lm2ZaZ8Xk1lF4a2iSg9WbLh9nK';
function crearEstrategiaLocal() {
  return new LocalStrategy(
    // Per defecte Passport espera "username"/"password". Els reanomenem als
    // camps que ja fan servir els esquemes zod d'Escena Viva.
    { usernameField: 'correu', passwordField: 'contrasenya', session: true },
    async (correu, contrasenya, fet) => {
      try {
        const normalitzat = String(correu).trim().toLowerCase();
        const usuari = await Usuari.findOne({ correu: normalitzat }).select('+hashContrasenya');
        // Esquer: mateix cost temporal existeixi o no el compte (08-02).
        const hash = usuari ? usuari.hashContrasenya : HASH_ESQUER;
        const coincideix = await verificarContrasenya(contrasenya, hash);
        if (!usuari || !coincideix) {
          // usuari=false significa "fallada de credencials", no error del servidor.
          return fet(null, false, { missatge: 'Credencials incorrectes' });
        }
        if (!usuari.verificat) {
          return fet(null, false, { missatge: 'El compte encara no esta verificat' });
        }
        // Objecte de domini, no el document de Mongoose (M7).
        return fet(null, { id: String(usuari._id), correu: usuari.correu,
          nom: usuari.nom, rol: usuari.rol, salaAssignada: usuari.salaAssignada });
      } catch (error) {
        return fet(error); // error real: 500, no 401
      }
    },
  );
}
module.exports = { crearEstrategiaLocal };

La signatura fet(error, usuari, info) distingeix tres desenllaços i confondre'ls és un error freqüent: fet(error) és fallada del servidor (500), fet(null, false, info) són credencials incorrectes (401), i fet(null, usuari) és èxit i estableix req.user.

  1. serializeUser i deserializeUser

// src/autenticacio/passport.js
'use strict';
const passport = require('passport');
const { Usuari } = require('../models/usuari.js');
const { crearEstrategiaLocal } = require('./estrategia-local.js');
function configurarPassport() {
  passport.use('local', crearEstrategiaLocal());
  // Que es desa A la sessio: nomes l'id. Mai l'objecte sencer.
  passport.serializeUser((usuari, fet) => fet(null, usuari.id));
  // Com es reconstrueix req.user a cada peticio, a partir de l'id.
  passport.deserializeUser(async (id, fet) => {
    try {
      const usuari = await Usuari.findById(id).lean();
      // Compte esborrat: la sessio queda invalidada tota sola.
      if (!usuari) { return fet(null, false); }
      fet(null, { id: String(usuari._id), correu: usuari.correu, nom: usuari.nom,
        rol: usuari.rol, salaAssignada: usuari.salaAssignada });
    } catch (error) { fet(error); }
  });
}
module.exports = { configurarPassport, passport };

Per què només l'id. Si serialitzes l'usuari sencer, en deses una fotografia congelada: en degradar un organitzador, la seva sessió continuaria dient organitzador fins a caducar. Desant l'id i rellegint a cada petició, el rol sempre és fresc. És exactament el problema que els JWT tenen per disseny (08-04) i que aquí evitem a canvi d'una consulta per petició —consulta que es desa a la memòria cau amb Redis (M10) quan el trànsit ho exigeixi.

  1. Muntatge a crearAplicacio() i ruta d'entrada

L'ordre importa i és una font inesgotable d'errors:

// src/app.js  (ordre canonic, ampliat al modul 8)
function crearAplicacio(opcions = {}) {
  const app = express();
  app.set('trust proxy', 1);            // darrere de proxy: IP i protocol reals
  app.use(idPeticio);                   // M6: tracabilitat
  app.use(helmet());
  app.use(compression());
  app.use(registreHttp);                // morgan
  app.use(corsLlistaBlanca);            // mai '*'
  app.use(express.json({ limit: '100kb' }));
  app.use(cookieParser());              // necessari per a CSRF i galetes
  app.use(crearMiddlewareSessio());     // 1) la sessio existeix
  app.use(passport.initialize());       // 2) Passport s'enganxa
  app.use(passport.session());          // 3) omple req.user des de la sessio
  app.use(proteccioCsrf);               // 4) despres de sessio i galetes
  app.use('/auth', crearRutesAutenticacio());
  app.use('/api', crearRutesApi());
  app.use(noTrobat);
  app.use(gestorDErrors);               // sempre l'ultim
  return app;
}

Les tres regles d'ordre que cal memoritzar: session() abans de passport.session() (Passport llegeix de req.session, que encara no existiria); express.json() abans de les rutes d'entrada (l'estratègia local necessita req.body); i CSRF després de la sessió i de l'anàlisi de galetes, abans de les rutes que modifiquen estat.

// src/rutes/autenticacio.js  (fragment)
router.post('/entrada', limitEntrada, validar(esquemaEntrada, ORIGENS_VALIDS.COS), (req, res, next) => {
  // Callback personalitzat: controlem la resposta en lloc de deixar que
  // Passport redirigeixi (successRedirect/failureRedirect son per a HTML).
  passport.authenticate('local', (error, usuari, info) => {
    if (error) { return next(error); }
    if (!usuari) { return next(new ErrorDAutenticacio(info?.missatge)); }
    // req.login estableix la sessio i regenera l'id (anti fixacio).
    req.login(usuari, (errorEntrada) => {
      if (errorEntrada) { return next(errorEntrada); }
      res.json({ usuari: { id: usuari.id, nom: usuari.nom, rol: usuari.rol } });
    });
  })(req, res, next); // authenticate retorna un middleware: cal invocar-lo
});
router.post('/sortida', exigirSessio, tancarSessio);

  1. El middleware exigirSessio

// src/middleware/exigir-sessio.js
'use strict';
const { ErrorDAutenticacio } = require('../errors.js');
function exigirSessio(req, res, next) {
  if (req.isAuthenticated && req.isAuthenticated() && req.user) {
    // Normalitzem a req.usuari perque la resta de l'aplicacio no
    // depengui de si autentiquem amb sessio (aqui) o amb JWT (08-04).
    req.usuari = req.user;
    return next();
  }
  // 401: no autenticat. Format d'error del modul 6.
  next(new ErrorDAutenticacio("Has d'iniciar sessio per a aquesta operacio"));
}
module.exports = { exigirSessio };

I així desapareix per fi el forat del mòdul 7:

// src/rutes/comandes.js  (fragment)
router.post('/compres', exigirSessio, limitCompra, validar(esquemaCompra), async (req, res, next) => {
  const { sessioId, quantitat } = req.dadesValidades.cos;
  // L'usuariId ja NO ve del client: ve de la sessio verificada.
  const resultat = await comprarEntrades({ usuariId: req.usuari.id, sessioId, quantitat });
  res.status(201).json(resultat);
});

Aquesta línia —usuariId: req.usuari.id— és l'objectiu de tot el mòdul. L'identificador ja no és una dada d'entrada: és una conclusió del servidor.

  1. CSRF: el preu de les galetes

Les galetes s'envien automàticament en tota petició al domini, l'originï qui l'originï. Aquesta comoditat és també la vulnerabilitat. L'atac contra Escena Viva: en Marc, amb la sessió oberta, visita promos-barates.test, que conté un formulari ocult apuntant a https://api.escenaviva.test/api/compres amb sessioId i quantitat, i un script que l'envia tot sol. El navegador hi adjunta la galeta de sessió d'en Marc: deu entrades comprades sense que ell faci res.

Per què els tokens en capçalera no pateixen això: Authorization: Bearer ... no s'envia sola. Un altre lloc hauria de llegir el token i afegir-lo a mà, i el navegador li ho impedeix. És una raó de pes a favor dels tokens en una API.

Escenari S'envia la galeta amb SameSite=Lax?
Formulari POST des d'un altre lloc No — CSRF blocat
fetch/XHR des d'un altre lloc No
<img src> a un GET que modifica estat No
Navegació amb enllaç GET des d'un altre lloc Sí — per això cap GET no ha de modificar estat
Petició des d'un subdomini del mateix lloc Sí — un subdomini compromès continua sent un risc

Lax cobreix la majoria, però no basta tot sol: no protegeix entre subdominis i els navegadors antics l'ignoren. Per a operacions sensibles s'hi afegeix el patró de token sincronitzador: el servidor emet un token CSRF que el client ha de reenviar en una capçalera; com que un altre lloc no el pot llegir, no el pot reproduir.

// src/middleware/csrf.js  ->  npm install csrf-csrf
'use strict';
const { doubleCsrf } = require('csrf-csrf');
const { configuracio } = require('../config/index.js');
const { doubleCsrfProtection, generateCsrfToken } = doubleCsrf({
  getSecret: () => configuracio.secretCsrf,
  // El token es lliga a la sessio: no val el d'un altre usuari.
  getSessionIdentifier: (req) => req.sessionID,
  cookieName: '__Host-ev.csrf',
  cookieOptions: { httpOnly: true, sameSite: 'lax', secure: true, path: '/' },
  ignoredMethods: ['GET', 'HEAD', 'OPTIONS'], // no modifiquen estat
  getCsrfTokenFromRequest: (req) => req.headers['x-csrf-token'],
});
module.exports = { doubleCsrfProtection, generateCsrfToken };

El seu lloc a la cadena: després de session() i cookieParser(), abans de les rutes. El client obté el token amb un GET /auth/csrf i l'envia a X-CSRF-Token a cada POST, PATCH o DELETE. Important per a la lliçó següent: si Escena Viva autentica amb Authorization: Bearer, aquelles rutes no necessiten CSRF; però el token de refresc anirà en galeta, així que POST /auth/refrescar sí que queda exposat, i per això el protegirem amb SameSite=Strict i Path restringit.

  1. Estratègies de tercers i comparativa final

Passport brilla quan hi afegeixes proveïdors: passport-google-oauth20 o passport-github2 es munten amb la mateixa forma que l'estratègia local, rebent clientID, clientSecret i callbackURL, i retornant l'Usuari corresponent al perfil. El flux de codi d'autorització amb PKCE, resumit: l'usuari prem «Entrar amb Google» i l'API redirigeix al proveïdor amb client_id, redirect_uri, scope i un state aleatori; l'usuari s'autentica a Google i hi consent; Google redirigeix a /auth/google/callback?code=...&state=...; l'API comprova que state coincideix (és la defensa CSRF d'OAuth) i bescanvia el code per tokens de servidor a servidor; i finalment crea o localitza l'Usuari i obre la seva pròpia sessió. Els dos punts on més es falla: no validar state i acceptar tokens enviats pel client en lloc de bescanviar el codi al servidor. Escena Viva no implementa OAuth, però el buit està preparat: deserializeUser no distingeix d'on va venir l'usuari.

Criteri Sessió de servidor Token autocontingut (JWT)
Estat al servidor Sí (necessita Redis amb diversos processos) No (llevat de llista de revocació)
Revocació Immediata Difícil: cal esperar que caduqui
Rol sempre actualitzat Sí (es rellegeix) No (congelat dins del token)
Entre dominis / mòbil Incòmode Natural
Vulnerable a CSRF Sí (mitigable) No, si va en capçalera
Vulnerable a XSS La galeta HttpOnly no es roba Greu si es desa a localStorage
Mida per petició ~30 bytes 300–800 bytes

Quan cadascuna. Sessions: aplicació web d'un sol domini, tauler d'administració, qualsevol cas on revocar a l'instant sigui un requisit. Tokens: API pública, client mòbil, diversos front-ends en dominis diferents, servei a servei. Totes dues: el més habitual en sistemes reals, i el que farà Escena Viva — el patró de token d'accés curt més refresc en galeta és, al capdavall, una sessió amb un altre nom.

Errors Comuns i Consells

  • Posar saveUninitialized: true sense pensar-hi. Crea una sessió i una galeta per a cada robot que visita el catàleg: omple el magatzem i complica el compliment de la normativa de galetes.
  • Oblidar req.session.regenerate() a l'entrada. Deixa oberta la fixació de sessió.
  • Respondre abans que la sessió s'hagi desat. Amb magatzem extern produeix el clàssic «la primera entrada no funciona, la segona sí».
  • res.clearCookie amb atributs diferents dels del Set-Cookie original: el navegador no l'esborra i l'usuari es pensa que ha sortit.
  • Creure que SameSite=Lax substitueix el token CSRF. Ajuda molt; no basta entre subdominis.
  • Fer servir successRedirect en una API JSON. Retorna un 302 que el client no espera; fes servir el callback personalitzat.
  • Consell: normalitza sempre a req.usuari —quan a 08-04 hi posis JWT, els controladors i l'autorització no s'assabentaran del canvi—, i en desenvolupament arrenca dos processos en ports diferents per veure fallar el magatzem en memòria: entendre-ho en carn pròpia val més que llegir-ho deu vegades.

Exercicis

Exercici 1: auditar una configuració

Assenyala els problemes d'aquesta configuració i corregeix-los:

app.use(session({
  secret: 'secret',
  resave: true,
  saveUninitialized: true,
  cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 },
}));
app.use(passport.session());
app.use(passport.initialize());

Exercici 2: sessió inconsistent

Escena Viva es desplega amb dos processos darrere d'un balancejador. Els usuaris informen que «de vegades» apareixen desconnectats i que en desplegar se'ls tanca la sessió. Explica'n la causa i descriu la solució, indicant quin mòdul del curs la desenvolupa.

Exercici 3: middleware exigirVerificat

Escriu un middleware exigirVerificat que s'executi després d'exigirSessio i retorni un 403 amb el format d'error del mòdul 6 si req.usuari.verificat és false. Justifica per què és 403 i no 401.

Solucions

Exercici 1. Sis problemes: secret: 'secret' és curt, endevinable i està escrit al codi (ha de venir de configuracio, amb 32+ caràcters aleatoris i en array per rotar); resave: true reescriu la sessió a cada petició encara que no canviï, amb càrrega inútil i curses; saveUninitialized: true crea sessions per a anònims; la galeta sense httpOnly la roba qualsevol XSS; sense secure ni sameSite viatja en clar i queda exposada a CSRF; i passport.session() és abans de passport.initialize(), ordre invertit que no funciona. A més, 30 dies de maxAge és excessiu per a una sessió de compra.

app.use(session({
  secret: configuracio.secretsSessio, name: 'ev.sid',
  resave: false, saveUninitialized: false, rolling: true,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30 * 60 * 1000, path: '/' },
  store: magatzemCompartit,
}));
app.use(passport.initialize());
app.use(passport.session());

Exercici 2. La causa és el MemoryStore per defecte: cada procés desa les sessions a la seva pròpia memòria, així que si l'entrada l'atén el procés A i la petició següent el B, l'identificador no hi existeix i l'usuari apareix com a anònim; en desplegar, la memòria es perd sencera i cauen totes les sessions. La solució és un magatzem compartit i persistent: Redis amb connect-redis (o connect-mongo sobre la base que ja tenim). El magatzem de sessions a Redis i la limitació de peticions distribuïda es desenvolupen al mòdul 10, juntament amb cluster i els worker threads.

Exercici 3

// src/middleware/exigir-verificat.js
'use strict';
const { ErrorDAutoritzacio } = require('../errors.js');
function exigirVerificat(req, res, next) {
  if (req.usuari && req.usuari.verificat) { return next(); }
  next(new ErrorDAutoritzacio('Has de verificar el teu correu abans de comprar entrades', {
    accio: 'reenviar-verificacio',
  }));
}
module.exports = { exigirVerificat };

És 403 i no 401 perquè la identitat està perfectament establerta: sabem qui és i n'hem verificat la credencial. El que falta és un permís, no una autenticació. Retornar 401 faria que el client enviés l'usuari un altre cop al formulari d'entrada, on entraria bé i tornaria a fallar: un bucle frustrant nascut de confondre els dos conceptes de la lliçó 08-01.

Conclusió

Ja sabem mantenir la identitat entre peticions a la manera clàssica. Una sessió de servidor és un identificador aleatori en una galeta HttpOnly i un estat desat al servidor; la galeta és opaca a propòsit, i per això la revocació és immediata i el rol sempre és fresc. Hem configurat express-session amb les opcions que de debò decideixen la seguretat, après per què el magatzem en memòria només serveix en desenvolupament, tancat la fixació de sessió amb regenerate(), i tancat la sessió de debò amb destroy() més l'esborrat de la galeta. Passport ens ha donat una interfície uniforme —estratègia local, serializeUser amb només l'id, req.user— i hem pagat el preu de les galetes amb protecció CSRF. I sobretot: comprarEntrades ja rep un usuariId que el servidor dedueix, no que el client declara.

Però Escena Viva és una API amb front-end estàtic i un client mòbil a l'horitzó, i acabem de veure el cost de les sessions: estat compartit i encaix incòmode fora del navegador. A la lliçó següent, Autenticació amb JWT, canviem d'enfocament: veurem què hi ha dins d'un token, per què signar no és xifrar, com s'eviten els atacs alg: none i de confusió d'algorismes, i —el més important— com es resol el problema real dels JWT, que és que no es poden revocar, amb un token d'accés curt i un token de refresc rotatori desat en galeta HttpOnly.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats