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
- Com funciona una sessió de servidor
express-sessioni les opcions que importen- El magatzem en memòria no val en producció
- Fixació de sessió i
regenerate() - Tancar la sessió de debò
- Passport.js: què és i què no és
- L'estratègia local
serializeUserideserializeUser- Muntatge a
crearAplicacio()i ruta d'entrada - El middleware
exigirSessio - CSRF: el preu de les galetes
- Estratègies de tercers i comparativa final
- Errors comuns, exercicis i conclusió
- 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.
express-session i les opcions que importen
express-session i les opcions que importenInstal·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).
- 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:
- 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.
- 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ó». - 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.
- Fixació de sessió i
regenerate()
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.
- 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.
- 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.
- 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.
serializeUser i deserializeUser
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.
- Muntatge a
crearAplicacio() i ruta d'entrada
crearAplicacio() i ruta d'entradaL'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);
- El middleware
exigirSessio
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.
- 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.
- 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: truesense 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.clearCookieamb atributs diferents dels delSet-Cookieoriginal: el navegador no l'esborra i l'usuari es pensa que ha sortit.- Creure que
SameSite=Laxsubstitueix el token CSRF. Ajuda molt; no basta entre subdominis. - Fer servir
successRedirecten 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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
