Arriba un correu a l'equip de la Botiga Aroma. CataBox, una aplicació de tercers que ajuda els aficionats al cafè a portar un quadern de tastos, vol oferir als seus usuaris que importin automàticament els cafès que han comprat a Aroma. La petició tècnica és simple: «podem llegir les comandes d'un client vostre?».
Amb l'autenticació que vam construir a 03-06 només hi ha una resposta possible, i és dolenta: que el client doni a CataBox el seu correu i la seva contrasenya de la Botiga Aroma, i que CataBox cridi POST /v1/sessions fent-se passar per ell. Això significa que una empresa que no controles desa les contrasenyes dels teus clients, que el token que obté té tots els permisos —inclòs crear comandes i canviar l'adreça d'enviament—, que no li pots revocar l'accés sense obligar el client a canviar la contrasenya, i que no tens manera de saber quines peticions són del client i quines de CataBox.
OAuth 2.0 existeix exactament per a això. Aquesta lliçó explica quin problema resol, com s'articula, quins fluxos es fan servir avui i quins estan desaconsellats, com OpenID Connect hi afegeix a sobre la peça que li falta —la identitat—, i com es valida un token d'un servidor d'autorització extern a la nostra API. En acabar, src/middleware/autenticacio-oauth.js existirà i conviurà amb l'autenticar de 03-06.
Advertiment. OAuth 2.0 és un protocol de seguretat i els seus detalls importen: un paràmetre mal validat converteix un flux correcte en una presa de comptes. El codi d'aquesta lliçó és didàctic i l'ha de revisar un professional de seguretat abans d'un desplegament real. Totes les dades, dominis i credencials són ficticis.
Contingut
- El problema: delegar accés sense compartir credencials
- Els quatre rols d'OAuth 2.0
- Tokens: accés, refresc i àmbits
- Tokens opacs enfront de JWT: introspecció o validació local
- Authorization Code + PKCE
- Per què el flux implícit i el de contrasenya estan desaconsellats
- Client Credentials: RàpidEnviaments, màquina a màquina
- Refresh Token i Device Code
state, CSRF i els paràmetres de la petició d'autorització- OpenID Connect: la identitat a sobre d'OAuth
- Validar el token a l'API: JWKS,
kidi les claims - El middleware
autenticarOAuthi la seva convivència ambautenticar - Àmbits i rols combinats
- Registre de clients, secrets i
redirect_uri - Revocació i tancament de sessió
- Per què no has d'implementar el teu propi servidor d'autorització
- Errors d'OAuth i la seva traducció al catàleg d'Aroma
- El problema: delegar accés sense compartir credencials
Abans d'OAuth, el patró habitual s'anomenava antipatró de la contrasenya compartida, i es veia així:
| Contrasenya compartida | OAuth 2.0 | |
|---|---|---|
| Què desa CataBox | Correu i contrasenya del client | Un token que caduca |
| Què pot fer | Tot el que pot fer el client | Només allò autoritzat (comandes.llegir) |
| Quant dura | Per sempre | Minuts o hores, amb refresc revocable |
| Com es revoca | Canviant la contrasenya (i trencant tota la resta) | Un clic a «aplicacions connectades» |
| Auditoria | Impossible distingir client d'aplicació | Cada petició identifica el client OAuth |
| Segon factor | Incompatible: CataBox no el pot passar | Compatible: el resol el servidor d'autorització |
| Si CataBox pateix una bretxa | Les contrasenyes dels teus clients són a fora | Es revoquen els tokens i s'ha acabat |
La idea central d'OAuth 2.0 cap en una frase: el client mai no veu les credencials; l'usuari s'autentica en un lloc de confiança i el que torna és un permís acotat i revocable.
I una precisió que evita el 90 % de la confusió inicial: OAuth 2.0 és un protocol d'autorització delegada, no d'autenticació. No serveix per a «iniciar sessió amb», per molt que s'utilitzi així a tot arreu. El que serveix per a això és OpenID Connect, que es construeix a sobre (apartat 10).
- Els quatre rols d'OAuth 2.0
| Rol | Nom a l'estàndard | Qui és en el nostre cas |
|---|---|---|
| Propietari del recurs | Resource Owner | Marta Garcia (cli_842), la persona propietària de les comandes |
| Client | Client | CataBox, l'aplicació que hi vol accedir. També la SPA i Aroma Mòbil |
| Servidor d'autorització | Authorization Server (AS) | https://auth.botigaaroma.example — autentica i emet tokens |
| Servidor de recursos | Resource Server (RS) | https://api.botigaaroma.example/v1 — la nostra API |
La conseqüència pràctica més important per a aquest curs: la nostra API és només el servidor de recursos. No mostra pantalles d'inici de sessió, no gestiona contrasenyes, no emet tokens i no sap res de consentiments. La seva única feina a OAuth és:
- Rebre un
Authorization: Bearer <token>. - Verificar que aquell token és autèntic, vigent i està adreçat a ella.
- Extreure qui és el subjecte i quins àmbits té.
- Decidir si aquella combinació pot fer el que demana.
Això és tot. Res més d'aquesta lliçó no s'implementa dins de la nostra API, i aquesta separació és precisament el valor del protocol.
graph LR U[Marta - propietari del recurs] -->|1. autoritza| AS[auth.botigaaroma.example<br/>Servidor d'autoritzacio] C[CataBox - client] -->|2. demana token| AS AS -->|3. emet access token| C C -->|4. Bearer token| RS[api.botigaaroma.example<br/>Servidor de recursos: LA NOSTRA API] RS -->|5. valida signatura amb JWKS| AS RS -->|6. dades autoritzades| C
Fixa't en el pas 5: l'API no pregunta al servidor d'autorització a cada petició. Es descarrega les seves claus públiques una vegada, les desa a la memòria cau i les verifica localment. Per què, a l'apartat 4.
- Tokens: accés, refresc i àmbits
Access token i refresh token
| Access token | Refresh token | |
|---|---|---|
| Per a què serveix | Accedir a l'API | Obtenir un access token nou |
| A qui es presenta | Al servidor de recursos (la nostra API) | Només al servidor d'autorització |
| Durada típica | 5–60 minuts | Dies o mesos |
| S'envia a | Authorization: Bearer |
Cos de POST /token |
| Si es roba | Dany acotat pel temps de vida | Greu: accés perllongat |
| La nostra API el veu | Sí, a cada petició | Mai |
La raó que n'existeixin dos és un compromís entre seguretat i usabilitat: vols que el token que viatja constantment per la xarxa caduqui aviat, però no vols demanar la contrasenya a l'usuari cada quinze minuts. El refresh token viatja poc, es desa millor i es pot revocar de manera centralitzada.
Aquesta arquitectura és la mateixa que ja vam construir a mà a 03-06 amb els nostres propis tokens d'accés i de refresc. La diferència és qui els emet: allà els emetia la nostra API; aquí els emet un servidor d'autorització especialitzat, i la nostra API només els verifica.
Àmbits (scopes)
Un àmbit és una etiqueta que acota el que un token permet fer. Es demanen a la petició d'autorització, l'usuari els veu a la pantalla de consentiment i viatgen dins del token.
Els de la Botiga Aroma:
| Àmbit | Permet | Qui el demana | Text de consentiment |
|---|---|---|---|
cafes.llegir |
Consultar el catàleg | Tots | «Veure el catàleg de cafès» |
comandes.llegir |
Llegir les comandes de l'usuari | CataBox, SPA, Aroma Mòbil | «Veure el teu historial de comandes» |
comandes.escriure |
Crear i pagar comandes | SPA, Aroma Mòbil | «Crear comandes en nom teu» |
ressenyes.escriure |
Publicar ressenyes | SPA, Aroma Mòbil | «Publicar ressenyes amb el teu nom» |
ressenyes.moderar |
Aprovar o rebutjar ressenyes | Tauler intern | «Moderar ressenyes de la botiga» |
enviaments.escriure |
Marcar comandes com a enviades | RàpidEnviaments | (sense consentiment: màquina) |
Quatre regles de disseny d'àmbits que eviten la majoria dels problemes:
- Granularitat
recurs.accio, no un àmbit per endpoint. Amb 24 URIs, un àmbit per endpoint produeix una pantalla de consentiment illegible. - Separar lectura d'escriptura sempre. CataBox demana
comandes.llegiri maicomandes.escriure. Aquesta separació és el 80 % del valor. - L'àmbit acota, no concedeix. Que un token tingui
comandes.llegirno significa que pugui llegir totes les comandes: significa que pot llegir les del subjecte del token. La comprovació de propietat (BOLA, 04-02) continua sent obligatòria. - Redactar el text que veurà l'usuari juntament amb l'àmbit. Si no saps explicar-lo en una línia comprensible, l'àmbit està mal dissenyat.
- Tokens opacs enfront de JWT: introspecció o validació local
L'access token pot ser de dues naturaleses, i l'elecció afecta directament com el valida la nostra API.
| Token opac | JWT (autocontingut) | |
|---|---|---|
| Què és | Una cadena aleatòria: a7f3c9... |
Tres parts Base64 amb les claims a dins |
| Qui sap què significa | Només el servidor d'autorització | Qualsevol que el verifiqui |
| Com el valida l'API | Introspecció: POST /introspect a l'AS |
Localment: verifica la signatura |
| Latència per petició | Una crida de xarxa extra | Zero |
| Revocació immediata | Sí | No: val fins al seu exp |
| Fuita del contingut | No revela res | El payload és llegible (no xifrat!) |
| Acoblament | L'API depèn de l'AS en calent | L'API només necessita les claus públiques |
La introspecció (RFC 7662) es veu així:
POST /introspect HTTP/1.1
Host: auth.botigaaroma.example
Authorization: Basic <credencials del servidor de recursos>
Content-Type: application/x-www-form-urlencoded
token=a7f3c9d2e8b1...{
"active": true,
"sub": "cli_842",
"scope": "cafes.llegir comandes.llegir",
"client_id": "catabox",
"exp": 1786000000
}El camp decisiu és active: si és false, el token no val, sense més explicacions.
Què tria la Botiga Aroma: JWT amb validació local, per tres raons. La latència importa (una crida extra per petició multiplica per dos el temps de resposta del catàleg); la disponibilitat importa (si l'AS cau, amb introspecció cau tota l'API); i la revocació immediata l'aconseguim per una altra via, amb tokens de vida curta (15 minuts) i revocació del refresh token.
El compromís: un access token JWT revocat continua sent vàlid fins que caduqui. Si això és inacceptable per a alguna operació —anul·lar una comanda, canviar la contrasenya—, es fa introspecció només per a aquelles operacions, o es consulta una llista de revocació a Redis. És una decisió per endpoint, no global.
I un recordatori que mai no sobra: un JWT està signat, no xifrat. Qualsevol que intercepti el token en llegeix el contingut amb un descodificador Base64. Mai no fiquis dades sensibles a les claims.
- Authorization Code + PKCE
És el flux. Si només en recordes un d'aquesta lliçó, que sigui aquest: serveix per a SPA, per a aplicacions mòbils, per a aplicacions de servidor i per a aplicacions de tercers com CataBox.
PKCE (Proof Key for Code Exchange, es pronuncia «pixy») és l'extensió que el fa segur per a clients que no poden desar un secret. Avui es considera obligatori per a tots els clients, inclosos els confidencials.
sequenceDiagram participant U as Marta (navegador) participant C as CataBox participant AS as auth.botigaaroma.example participant RS as api.botigaaroma.example C->>C: 1. Genera code_verifier aleatori C->>C: 2. code_challenge = BASE64URL(SHA256(verifier)) C->>U: 3. Redirigeix a /authorize amb code_challenge i state U->>AS: 4. GET /authorize?... AS->>U: 5. Pantalla de login (contrasenya + 2FA) U->>AS: 6. Credencials AS->>U: 7. Pantalla de consentiment: "CataBox vol veure les teves comandes" U->>AS: 8. Accepta AS->>U: 9. Redirigeix a redirect_uri?code=xyz&state=... U->>C: 10. Lliura el code C->>AS: 11. POST /token amb code + code_verifier AS->>AS: 12. Comprova SHA256(verifier) == challenge AS->>C: 13. access_token + refresh_token C->>RS: 14. GET /v1/comandes amb Bearer RS->>RS: 15. Verifica signatura (JWKS), iss, aud, exp, scope RS->>C: 16. 200 amb les comandes de la Marta
Vegem les peticions reals.
Pas 3-4: la petició d'autorització. Passa al navegador de l'usuari, no al servidor de CataBox:
GET /authorize
?response_type=code
&client_id=catabox
&redirect_uri=https%3A%2F%2Fcatabox.example%2Fcallback
&scope=cafes.llegir%20comandes.llegir
&state=xY9fK2mQ7pL1
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: auth.botigaaroma.examplePas 9: la redirecció de tornada. El codi arriba per la URL, i és d'un sol ús i de vida molt curta (típicament 30–60 segons):
HTTP/1.1 302 Found
Location: https://catabox.example/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xY9fK2mQ7pL1Pas 11: el bescanvi del codi pel token. Això és una petició de servidor a servidor (o des de l'app), mai una redirecció:
POST /token HTTP/1.1
Host: auth.botigaaroma.example
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fcatabox.example%2Fcallback
&client_id=catabox
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFyb21hLTIwMjYtMDgifQ...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "def50200a1b2c3...",
"scope": "cafes.llegir comandes.llegir"
}Què resol exactament PKCE
El codi d'autorització viatja per la URL del navegador, i això és un canal poc segur: queda a l'historial, pot aparèixer als logs d'un proxy i, al mòbil, una altra aplicació pot registrar el mateix esquema d'URI i robar la redirecció. Abans de PKCE, un atacant que capturés el codi podia bescanviar-lo per un token, perquè l'AS no tenia manera de saber que qui el bescanvia no és qui el va demanar.
PKCE lliga les dues peticions amb un secret d'un sol ús:
// Client (SPA o app mòbil). S'executa ABANS de redirigir l'usuari.
import crypto from 'node:crypto';
function base64url(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-') // Base64URL: + → -
.replace(/\//g, '_') // / → _
.replace(/=+$/, ''); // sense farciment
}
// 1. Un secret aleatori de 32 bytes, diferent a cada intent de login.
const codeVerifier = base64url(crypto.randomBytes(32));
// 2. El seu hash: és l'ÚNIC que viatja per la URL del navegador.
const codeChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest());
// 3. El verifier es desa localment (sessionStorage a la SPA) i encara NO s'envia.
sessionStorage.setItem('pkce_verifier', codeVerifier);
// 4. Només al pas 11, juntament amb el codi, s'envia el verifier original.
// L'AS calcula SHA256(verifier) i el compara amb el challenge que va rebre al pas 4.La propietat criptogràfica que ho fa funcionar: SHA-256 no es pot invertir. Un atacant que vegi el code_challenge a la URL no pot deduir el code_verifier, així que encara que robi el codi no el pot bescanviar. I code_challenge_method ha de ser sempre S256; el valor plain existeix per compatibilitat i no protegeix de res.
- Per què el flux implícit i el de contrasenya estan desaconsellats
Els trobaràs en tutorials antics. Les millors pràctiques actuals d'OAuth 2.0 (i OAuth 2.1) els retiren.
| Flux | Com funcionava | Per què es retira | Què fer servir |
|---|---|---|---|
Implícit (response_type=token) |
L'AS retornava l'access token directament al fragment de la URL | El token queda a l'historial, al Referer i exposat a qualsevol script de la pàgina; sense refresh token segur |
Authorization Code + PKCE |
Contrasenya del propietari (password) |
L'app demana usuari i contrasenya i els envia a l'AS | Reintrodueix l'antipatró que OAuth va venir a eliminar; incompatible amb 2FA i amb inici de sessió federat | Authorization Code + PKCE |
El cas del flux de contrasenya mereix un matís perquè genera dubtes legítims: es va crear per a aplicacions de primera part, és a dir, teves. Però fins i tot allà és mala idea, perquè la teva pròpia app acaba gestionant contrasenyes, no pot passar per un segon factor i no es beneficia de res de la infraestructura de l'AS. La SPA de la Botiga Aroma i Aroma Mòbil fan servir Authorization Code + PKCE igual que CataBox, encara que siguin nostres. La diferència entre app pròpia i de tercers és que a la pròpia se li pot saltar la pantalla de consentiment, no que faci servir un altre flux.
- Client Credentials: RàpidEnviaments, màquina a màquina
Quan no hi ha cap usuari implicat, no hi ha a qui demanar consentiment. RàpidEnviaments és un sistema que actua en nom propi per marcar comandes com a enviades.
sequenceDiagram participant R as RapidEnviaments (backend) participant AS as auth.botigaaroma.example participant RS as api.botigaaroma.example R->>AS: POST /token (grant_type=client_credentials + client_secret) AS->>R: access_token (scope=enviaments.escriure, 1 h) R->>RS: POST /v1/comandes/com_5001/enviament (Bearer) RS->>RS: Verifica signatura, aud, scope=enviaments.escriure RS->>R: 200
POST /token HTTP/1.1
Host: auth.botigaaroma.example
Authorization: Basic cmFwaWRlbnZpYW1lbnRzOnNlY3JldEZpY3RpY2k=
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=enviaments.escriureTres característiques que el distingeixen:
- No hi ha
redirect_uri, nicode, ni consentiment: només el client i el seu secret. - No hi ha refresh token: quan l'access token caduca, se'n demana un altre. És barat perquè no requereix ningú al davant.
- El
subdel token és el mateix client (rapidenviaments), no una persona. A la nostra API això correspon al rolsoci, i significa que les comprovacions de propietat per client no s'apliquen: cal autoritzar per una altra via (quines comandes pot tocar RàpidEnviaments i en quins estats).
Un client que fa servir aquest flux és confidencial per definició: desa un secret en un servidor. Mai no es fa servir en una SPA ni en una app mòbil, perquè qualsevol pot extreure el secret del codi descarregat.
- Refresh Token i Device Code
Refresh Token
POST /token HTTP/1.1
Host: auth.botigaaroma.example
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=def50200a1b2c3...
&client_id=cataboxLa resposta és un access token nou i, a les implementacions modernes, també un refresh token nou. Això s'anomena rotació de refresh tokens i és una defensa important: si un refresh token es fa servir dues vegades, l'AS sap que un dels dos usos és d'un atacant —perquè el legítim ja va rebre el substitut— i revoca tota la família de tokens, tancant la sessió. És la detecció de reutilització, i convé exigir-la en triar proveïdor.
On es desa el refresh token, per tipus de client:
| Client | Emmagatzematge | Risc |
|---|---|---|
| Backend (CataBox) | Base de dades xifrada | Baix |
| App mòbil | Clauer del sistema (Keychain / Keystore) | Baix |
| SPA | Galeta HttpOnly+Secure+SameSite, gestionada per un backend propi |
Mitjà |
| SPA | localStorage |
Alt: qualsevol XSS el roba |
Per a SPA, la recomanació actual és el patró BFF (Backend For Frontend): un petit backend propi desa els tokens i la SPA hi parla mitjançant una galeta de sessió. Si això no és viable, refresh tokens rotatius amb vida curta i mai a localStorage.
Device Code
Per a dispositius sense teclat ni navegador —una pantalla de la cafeteria que mostra l'estoc, un televisor—. El dispositiu demana un codi, mostra a l'usuari «entra a botigaaroma.example/activar i introdueix KDLF-9XZQ», i mentrestant consulta l'AS fins que l'usuari completa l'autorització al seu mòbil. Es menciona per completesa; no s'aplica als consumidors actuals de la Botiga Aroma.
state, CSRF i els paràmetres de la petició d'autorització
state, CSRF i els paràmetres de la petició d'autorització| Paràmetre | Obligatori | Què és | Risc si s'omet o no es valida |
|---|---|---|---|
response_type |
Sí | code |
— |
client_id |
Sí | Identificador públic del client | — |
redirect_uri |
Sí (recomanat) | On tornar | Redirecció oberta si l'AS no la compara exacta |
scope |
Recomanat | Permisos demanats | S'apliquen els del registre |
state |
Sí | Valor aleatori opac | CSRF d'inici de sessió |
code_challenge |
Sí | Hash del verifier | Robatori del codi |
code_challenge_method |
Sí | S256 |
plain no protegeix |
nonce |
Sí a OIDC | Aleatori, torna a l'id_token |
Reproducció d'id_token |
prompt |
No | login, consent, none |
— |
Quin atac evita state. Sense ell, un atacant inicia el flux amb el seu compte de la Botiga Aroma, captura el code de la redirecció i enganya la víctima perquè el seu navegador visiti https://catabox.example/callback?code=<el de l'atacant>. CataBox bescanvia aquell codi i associa el compte de la Botiga Aroma de l'atacant a la sessió de CataBox de la víctima. A partir d'aquí, tot el que la víctima desi a CataBox va a un compte que l'atacant controla.
La defensa és un valor aleatori lligat a la sessió del navegador:
// Abans de redirigir: es genera i es desa lligat a la sessió.
const state = base64url(crypto.randomBytes(16));
sessionStorage.setItem('oauth_state', state);
// ... redirecció a /authorize?...&state=<state>
// Al callback: comparació OBLIGATÒRIA abans de bescanviar el codi.
const rebut = new URLSearchParams(location.search).get('state');
const esperat = sessionStorage.getItem('oauth_state');
if (!rebut || rebut !== esperat) {
throw new Error('state no coincideix: possible CSRF. No es bescanvia el codi.');
}
sessionStorage.removeItem('oauth_state'); // un sol úsAmb PKCE ben implementat el risc es redueix molt, però state continua sent obligatori: protegeix d'un atac diferent (la fixació de la sessió del client, no el robatori del codi) i a més serveix per recordar on tornar dins de l'aplicació.
- OpenID Connect: la identitat a sobre d'OAuth
OAuth 2.0 respon a «pot aquesta aplicació fer això?». No respon a «qui és l'usuari?». Fer servir un access token com a prova d'identitat és un error clàssic i perillós: el token no diu res verificable sobre qui el va obtenir ni per a quina aplicació es va emetre, i per això van existir els atacs de substitució de token.
OpenID Connect (OIDC) és una capa fina i estandarditzada sobre OAuth 2.0 que afegeix exactament el que falta.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| Pregunta | Què pot fer? | Qui és? |
| Àmbit clau | comandes.llegir |
openid |
| Retorna | access_token |
access_token + id_token |
| Format del resultat | Lliure | id_token és sempre un JWT |
| Destinatari | El servidor de recursos | El client |
| Descobriment | — | /.well-known/openid-configuration |
N'hi ha prou d'afegir openid al scope perquè el flux es converteixi en OIDC:
L'id_token descodificat:
{
"iss": "https://auth.botigaaroma.example",
"sub": "cli_842",
"aud": "catabox",
"exp": 1786003600,
"iat": 1786000000,
"nonce": "n-0S6_WzA2Mj",
"auth_time": 1785999950,
"name": "Marta Garcia",
"email": "[email protected]",
"email_verified": true,
"locale": "ca-ES"
}Les claims estàndard més utilitzades:
| Claim | Significat |
|---|---|
sub |
Identificador estable i únic de l'usuari en aquell emissor. La clau real |
iss |
Qui va emetre el token |
aud |
Per a quin client es va emetre |
nonce |
L'aleatori que va enviar el client: evita reproduir un id_token vell |
auth_time |
Quan es va autenticar realment (útil per exigir reautenticació) |
name, email, picture |
Perfil (amb scope=profile email) |
email_verified |
Si l'emissor va verificar aquell correu |
Dos advertiments que causen bugs reals:
- La clau de l'usuari és
sub, maiemail. Un correu es pot canviar, es pot reassignar i pot arribar sense verificar. Vincular comptes per correu és una via de presa de compte siemail_verifiedésfalse. - L'
id_tokenés per al client, no per a l'API. No s'envia aAuthorization: Beareral servidor de recursos. La nostra API valida l'access token; l'id_tokenel consumeix CataBox per saber a qui ha connectat.
L'endpoint /userinfo completa el quadre: es crida amb l'access token i retorna les claims de perfil actualitzades. Es fa servir quan l'id_token es va emetre fa temps o quan es prefereix no engreixar-lo.
I el document de descobriment, que evita configurar URL a mà:
{
"issuer": "https://auth.botigaaroma.example",
"authorization_endpoint": "https://auth.botigaaroma.example/authorize",
"token_endpoint": "https://auth.botigaaroma.example/token",
"userinfo_endpoint": "https://auth.botigaaroma.example/userinfo",
"jwks_uri": "https://auth.botigaaroma.example/.well-known/jwks.json",
"revocation_endpoint": "https://auth.botigaaroma.example/revoke",
"introspection_endpoint": "https://auth.botigaaroma.example/introspect",
"scopes_supported": ["openid", "profile", "email", "cafes.llegir", "comandes.llegir",
"comandes.escriure", "ressenyes.moderar", "enviaments.escriure"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["S256"]
}D'aquí surt el jwks_uri, que és l'única cosa que la nostra API necessita.
- Validar el token a l'API: JWKS,
kid i les claims
kid i les claimsAquí entra per fi el nostre codi. La mecànica del JWT ja la vam veure a 03-06; el que és nou són dues coses: la signatura és asimètrica (l'AS signa amb la seva clau privada, nosaltres verifiquem amb la seva clau pública) i la clau pública es descobreix dinàmicament mitjançant JWKS.
Què és JWKS
JWKS (JSON Web Key Set) és un document públic amb les claus de verificació de l'emissor:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "aroma-2026-08",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4...",
"e": "AQAB"
},
{
"kty": "RSA",
"use": "sig",
"kid": "aroma-2026-05",
"alg": "RS256",
"n": "sXchDaQebHnPiGvyDOAT4saGEUetSyo9MKLOoWFsueri23V0dpBB...",
"e": "AQAB"
}
]
}Que hi hagi dues claus alhora no és un error: és la rotació. L'AS comença a signar amb aroma-2026-08 mentre els tokens signats amb aroma-2026-05 continuen vigents fins a caducar. La capçalera de cada JWT indica quina cal fer servir:
Això resol de manera nativa el problema de rotació de secrets que vam plantejar a 04-02: no cal redesplegar res, l'API descobreix la clau nova sola.
El middleware
jose és la biblioteca estàndard de facto per a JOSE/JWT a Node: implementa la memòria cau de JWKS, la rotació i les verificacions de l'estàndard.
// src/config/oauth.js (fitxer NOU)
import { createRemoteJWKSet } from 'jose';
import { entorn } from './entorn.js';
/**
* Conjunt remot de claus públiques del servidor d'autorització.
*
* createRemoteJWKSet retorna una funció que 'jose' fa servir per resoldre la clau
* a partir del 'kid' de la capçalera del token. Internament:
* - descarrega el JWKS la primera vegada i el manté en memòria;
* - si arriba un 'kid' desconegut, el torna a descarregar (rotació automàtica);
* - limita aquestes recàrregues perquè un token amb 'kid' inventat no es converteixi
* en un atac de denegació de servei contra l'AS.
*/
export const jwks = createRemoteJWKSet(new URL(entorn.OAUTH_JWKS_URI), {
cooldownDuration: 30_000, // no torna a buscar més d'un cop cada 30 s
cacheMaxAge: 600_000, // refresca el joc de claus cada 10 min
timeoutDuration: 5_000, // talla si l'AS no respon en 5 s
});
export const OAUTH = {
emissor: entorn.OAUTH_EMISSOR, // https://auth.botigaaroma.example
audiencia: entorn.OAUTH_AUDIENCIA, // https://api.botigaaroma.example
algorismes: ['RS256'], // llista blanca tancada
toleranciaRellotge: '30s',
};// src/middleware/autenticacio-oauth.js (fitxer NOU)
import { jwtVerify, errors as errorsJose } from 'jose';
import { jwks, OAUTH } from '../config/oauth.js';
import { errors } from '../errors/error-api.js';
/**
* Autentica la petició amb un access token emès pel servidor d'autorització
* extern. Deixa a req.usuari el subjecte i els seus àmbits.
*/
export async function autenticarOAuth(req, res, next) {
const capcalera = req.get('Authorization') ?? '';
// 1. L'esquema ha de ser exactament 'Bearer'.
const [esquema, token] = capcalera.split(' ');
if (esquema !== 'Bearer' || !token) {
res.set('WWW-Authenticate', `Bearer realm="api.botigaaroma.example"`);
return next(errors.noAutenticat('no_autenticat', 'Falta el token d\'accés.'));
}
try {
// 2. Verificació completa: signatura + claims registrades.
const { payload } = await jwtVerify(token, jwks, {
issuer: OAUTH.emissor, // iss ha de ser EL NOSTRE servidor d'autorització
audience: OAUTH.audiencia, // aud ha de ser LA NOSTRA API
algorithms: OAUTH.algorismes, // només RS256: bloqueja alg:none i confusió HS/RS
clockTolerance: OAUTH.toleranciaRellotge, // marge per a rellotges desajustats
});
// jwtVerify ja ha comprovat exp (no caducat) i nbf (no usat abans d'hora).
// 3. Els àmbits arriben com una cadena separada per espais.
const ambits = new Set((payload.scope ?? '').split(' ').filter(Boolean));
// 4. Subjecte normalitzat: l'API el fa servir igual que el de 03-06.
req.usuari = {
id: payload.sub, // cli_842, o 'rapidenviaments' en client credentials
rol: payload.rol ?? deduirRol(payload),
ambits,
clientOauth: payload.client_id ?? payload.azp ?? null, // qui actua: catabox, spa...
origenToken: 'oauth',
};
return next();
} catch (error) {
return next(traduirErrorJose(error, res));
}
}
/**
* Tradueix els errors de 'jose' al catàleg de la Botiga Aroma i fixa
* WWW-Authenticate segons el RFC 6750 (Bearer Token Usage).
*/
function traduirErrorJose(error, res) {
if (error instanceof errorsJose.JWTExpired) {
res.set('WWW-Authenticate',
`Bearer error="invalid_token", error_description="The access token expired"`);
return errors.noAutenticat('token_caducat', 'El token d\'accés ha caducat.');
}
if (error instanceof errorsJose.JWKSNoMatchingKey) {
// 'kid' desconegut: token d'un altre emissor o clau retirada.
res.set('WWW-Authenticate', `Bearer error="invalid_token"`);
return errors.noAutenticat('no_autenticat', 'El token d\'accés no és vàlid.');
}
if (error instanceof errorsJose.JWKSTimeout) {
// El servidor d'autorització no respon: NO és culpa del client.
return errors.serveiNoDisponible(
'servei_no_disponible',
'No s\'ha pogut verificar el token en aquest moment.'
);
}
res.set('WWW-Authenticate', `Bearer error="invalid_token"`);
return errors.noAutenticat('no_autenticat', 'El token d\'accés no és vàlid.');
}
/**
* Exigeix un o diversos àmbits. Es compon DESPRÉS d'autenticarOAuth.
*/
export function exigirAmbit(...requerits) {
return (req, res, next) => {
const te = requerits.every((a) => req.usuari?.ambits?.has(a));
if (!te) {
res.set('WWW-Authenticate',
`Bearer error="insufficient_scope", scope="${requerits.join(' ')}"`);
return next(
errors.permisDenegat(
'permisos_insuficients',
`El token no inclou l'àmbit requerit: ${requerits.join(', ')}.`
)
);
}
return next();
};
}Les claims que cal verificar, i què passa si te'n saltes alguna:
| Claim | Què comprova | Si no la valides |
|---|---|---|
| signatura | Que la va emetre qui diu | Qualsevol fabrica tokens |
iss |
Qui el va emetre | Acceptes tokens de qualsevol altre emissor |
aud |
Per a qui és | Acceptes un token emès per a una altra API: l'atac de confusió d'audiència |
exp |
No caducat | Els tokens no caduquen mai |
nbf |
No usat abans d'hora | Acceptes tokens preemesos |
alg (llista blanca) |
Algorisme esperat | alg:none i confusió RS/HS |
scope |
Permís concret | Qualsevol token val per a tot |
aud és la que més s'oblida i una de les més greus. Si un usuari té un token per a una altra aplicació del mateix servidor d'autorització i la teva API no comprova aud, aquell token obre la teva API.
Sobre el rellotge desincronitzat: exp i nbf són instants absoluts. Si el rellotge del teu servidor va trenta segons avançat, rebutjaràs tokens acabats d'emetre amb un token_caducat incomprensible. Per això clockTolerance, i per això els servidors porten NTP. Una tolerància raonable són 30–60 segons; més que això comença a ser un risc.
Sobre la memòria cau de claus: sense ella faries una petició HTTP a l'AS per cada petició entrant, amb la qual cosa hauries perdut l'avantatge de la validació local i hauries creat una dependència dura. Amb ella, l'AS pot estar caigut deu minuts sense que la teva API deixi d'autenticar.
- El middleware
autenticarOAuth i la seva convivència amb autenticar
autenticarOAuth i la seva convivència amb autenticarNo cal triar de cop. Durant la migració, els dos mecanismes conviuen: els tokens propis de 03-06 (HS256, emesos per la nostra API) i els d'OAuth (RS256, emesos per l'AS). Un middleware selector decideix per la capçalera del token:
// src/middleware/autenticacio.js (MODIFICAT: s'hi afegeix el selector)
import { decodeProtectedHeader } from 'jose';
import { autenticarPropi } from './autenticacio-propia.js'; // el de 03-06, reanomenat
import { autenticarOAuth } from './autenticacio-oauth.js';
/**
* Punt d'entrada únic d'autenticació.
* Tria el verificador segons l'algorisme declarat a la capçalera del token.
* Nota: la capçalera NO és de fiar per si sola; només es fa servir per ENCAMINAR,
* i cada verificador imposa després la seva pròpia llista blanca d'algorismes.
*/
export function autenticar(req, res, next) {
const token = (req.get('Authorization') ?? '').split(' ')[1];
if (!token) return autenticarPropi(req, res, next); // respondrà 401 amb WWW-Authenticate
let capcalera;
try {
capcalera = decodeProtectedHeader(token); // només descodifica, NO verifica
} catch {
return autenticarPropi(req, res, next);
}
return capcalera.alg === 'RS256'
? autenticarOAuth(req, res, next)
: autenticarPropi(req, res, next);
}El comentari del codi assenyala l'important: la capçalera del token és entrada no fiable. Es fa servir només per decidir quin verificador l'atén; aquell verificador imposa després algorithms: ['RS256'] o ['HS256'] segons correspongui, així que un atacant no hi guanya res mentint a l'alg.
Posició a la cadena. No canvia respecte a 03-06: l'autenticació continua sent el primer dins de cada ruta, no a src/app.js. L'ordre de la cadena de ruta passa a ser:
autenticar → exigirRol / exigirAmbit → exigirClauIdempotencia → validar(esquema, origen) → asincron(controlador)
I src/app.js no es toca en aquesta lliçó: OAuth no afegeix middleware global.
- Àmbits i rols combinats
Aquí hi ha una confusió freqüent que convé esvair amb precisió, perquè són dos mecanismes que responen a preguntes diferents:
Rol (client, empleat, administrador, soci) |
Àmbit (comandes.llegir) |
|
|---|---|---|
| Respon a | Qui és el subjecte | Què va deixar fer a aquesta aplicació |
| Ho decideix | La Botiga Aroma, a la seva base de dades | L'usuari, a la pantalla de consentiment |
| Canvia | Rarament | A cada autorització |
| Exemple | La Marta és client |
La Marta va permetre a CataBox comandes.llegir |
L'autorització efectiva és la intersecció de les dues. Un token amb ressenyes.moderar el subjecte del qual sigui un client no pot moderar: l'àmbit diu el que l'aplicació va demanar, però el rol diu el que la persona pot. I a l'inrevés: una administradora que fa servir CataBox, que només va demanar comandes.llegir, no pot moderar ressenyes des de CataBox encara que el seu rol l'hi permeti al tauler.
// src/rutes/ressenyes.js (MODIFICAT)
router.post(
'/:id/aprovacio',
autenticar, // qui ets? (token propi o OAuth)
exigirRol('empleat', 'administrador'), // la teva persona pot moderar?
exigirAmbit('ressenyes.moderar'), // aquesta aplicació té permís per fer-ho?
validar(esquemaAprovacio, 'body'),
asincron(controladors.ressenyes.aprovar)
);I una regla de compatibilitat per a la convivència: un token propi de 03-06 no porta scope. Perquè exigirAmbit no trenqui les rutes existents, l'autenticarPropi omple ambits amb el conjunt complet que correspon al rol; és a dir, un token de primera part es comporta com si l'usuari hagués consentit tot. És coherent, perquè en aquell flux l'usuari està fent servir directament la nostra aplicació.
- Registre de clients, secrets i
redirect_uri
redirect_uriAbans que CataBox pugui demanar res, es registra al servidor d'autorització i obté:
| Dada | Exemple | Públic |
|---|---|---|
client_id |
catabox |
Sí |
client_secret |
cbx_sk_9f3a... (fictici) |
No, només si és confidencial |
redirect_uri |
https://catabox.example/callback |
Sí |
| Àmbits permesos | cafes.llegir comandes.llegir |
Sí |
| Tipus de client | Confidencial / públic | — |
Confidencial enfront de públic:
| Confidencial | Públic | |
|---|---|---|
| Pot desar un secret | Sí (servidor sota el seu control) | No |
| Exemples | Backend de CataBox, RàpidEnviaments | SPA de la botiga, Aroma Mòbil |
Autenticació a /token |
client_secret |
Només PKCE |
| Client Credentials | Permès | Prohibit |
Un error molt repetit: incrustar el client_secret en una SPA o en una app mòbil. Tot el que es descarrega al dispositiu de l'usuari és públic, per molt que estigui ofuscat o compilat. Aquesta és la raó exacta que PKCE existeixi.
La redirect_uri es compara byte a byte. No per prefix, no per comodí, no ignorant el fragment. Un AS que accepti https://catabox.example/* permet que un atacant que controli qualsevol ruta d'aquell domini (una pàgina de perfil amb contingut pujat, per exemple) rebi els codis d'autorització. Regles: HTTPS obligatori, sense comodins, sense paràmetres dinàmics —el que necessitis recordar va a state— i al mòbil, esquemes propis o App Links verificats en comptes d'un esquema que qualsevol app pugui registrar.
- Revocació i tancament de sessió
Revocació de token (RFC 7009):
POST /revoke HTTP/1.1
Host: auth.botigaaroma.example
Authorization: Basic <credencials del client>
Content-Type: application/x-www-form-urlencoded
token=def50200a1b2c3...&token_type_hint=refresh_tokenRespon 200 fins i tot si el token no existia, deliberadament: així no es pot fer servir l'endpoint per esbrinar si un token és vàlid.
Què revocar i quin efecte té:
| Acció | Efecte immediat | Efecte diferit |
|---|---|---|
| Revocar el refresh token | No es poden demanar tokens nous | L'access token viu fins al seu exp (≤ 15 min) |
| Revocar l'access token (opac) | Deixa de valer ja | — |
| Revocar l'access token (JWT) | Cap, llevat de llista de revocació | Caduca sol |
| L'usuari retira el consentiment | Es revoca tota la família | Ídem |
| Canvi de contrasenya | Es revoquen totes les sessions | Ídem |
Tancament de sessió. Aquí hi ha tres nivells que convé no confondre, perquè l'usuari creu que «tancar sessió» és un de sol:
- Local: l'aplicació esborra els seus tokens. L'usuari continua amb sessió oberta a l'AS.
- RP-Initiated Logout (OIDC): l'aplicació redirigeix a
end_session_endpointi l'AS tanca també la seva sessió. - Back-Channel Logout: l'AS notifica per darrere a totes les aplicacions connectades perquè tanquin la sessió. És el que cal en un escenari d'inici de sessió únic real.
A la SPA de la Botiga Aroma, «tancar sessió» ha de ser com a mínim el nivell 2; si només fas l'1, prémer «entrar» un altre cop torna a fer entrar l'usuari sense demanar-li res, i en un ordinador compartit això és un problema de seguretat.
- Per què no has d'implementar el teu propi servidor d'autorització
Ja ho hauràs intuït llegint la lliçó: implementar un servidor d'autorització OAuth 2.0 correcte és un projecte en si mateix, i fer-ho malament no produeix una fallada visible, produeix una vulnerabilitat silenciosa.
El que cal construir i mantenir correctament:
- Emissió i validació de codis d'un sol ús, amb vida curta i lligats al client.
- PKCE amb
S256, comparació exacta deredirect_uri, validació destateinonce. - Signatura asimètrica, publicació de JWKS i rotació de claus sense tallar el servei.
- Rotació de refresh tokens amb detecció de reutilització i revocació en cascada.
- Pantalles de consentiment, registre de consentiments i la seva retirada.
- Gestió de contrasenyes, segon factor, recuperació de compte, bloqueig per intents.
- Endpoints de descobriment, introspecció, revocació i
userinfo. - Compliment amb les actualitzacions de l'estàndard i amb els avisos de seguretat.
A més, el teu servidor d'autorització és l'actiu més crític del sistema: qui el compromet, ho compromet tot.
| Opció | Quan té sentit | Consideracions |
|---|---|---|
| Auth0 / Okta | SaaS, vols zero manteniment | Cost per usuari actiu; dependència externa |
| AWS Cognito / Azure AD B2C | Ja ets en aquell núvol | Bona integració; personalització limitada |
| Keycloak | Autoallotjat, control total, sense cost de llicència | Tu operes, actualitzes i assegures el servei |
| Ory Hydra | Vols l'AS i gestionar tu el login | Lleuger, però més peces per integrar |
| Implementar-lo tu | Pràcticament mai | Només amb un equip de seguretat dedicat |
La decisió de la Botiga Aroma: un proveïdor gestionat a auth.botigaaroma.example. La nostra API continua sent únicament servidor de recursos, que és exactament el paper que hem implementat.
I una precisió útil: això no invalida la feina de 03-06. Els tokens propis continuen sent perfectament raonables per al cas de primera part —la teva SPA parlant amb la teva API, sense tercers—. OAuth s'incorpora quan apareix un tercer, quan necessites inici de sessió federat o quan diverses APIs comparteixen identitat.
- Errors d'OAuth i la seva traducció al catàleg d'Aroma
OAuth defineix els seus propis codis d'error, amb aquesta forma:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired",
"error_uri": "https://auth.botigaaroma.example/docs/errors#invalid_grant"
}Aquest format l'emet el servidor d'autorització, no la nostra API. Els errors que veu un client de CataBox es reparteixen així:
| Error OAuth | L'emet | Significat | Al catàleg d'Aroma |
|---|---|---|---|
invalid_request |
AS | Falta un paràmetre obligatori | — |
invalid_client |
AS | client_id/client_secret erronis |
— |
invalid_grant |
AS | Codi caducat, ja usat, o refresh revocat | — |
unauthorized_client |
AS | Aquell client no pot fer servir aquell flux | — |
unsupported_grant_type |
AS | grant_type desconegut |
— |
invalid_scope |
AS | Àmbit inexistent o no permès al client | — |
access_denied |
AS | L'usuari va rebutjar el consentiment | — |
invalid_token |
La nostra API | Token no verificable, mal format o d'un altre emissor | no_autenticat (401) |
invalid_token (caducat) |
La nostra API | exp passat |
token_caducat (401) |
insufficient_scope |
La nostra API | Falten àmbits | permisos_insuficients (403) |
La nostra API manté el seu format propi al cos i fa servir la capçalera WWW-Authenticate per parlar l'idioma d'OAuth, que és el que el RFC 6750 espera:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="comandes.escriure"
Content-Type: application/json
{
"error": {
"codi": "permisos_insuficients",
"missatge": "El token no inclou l'àmbit requerit: comandes.escriure.",
"detalls": []
}
}Així es compleixen les dues coses alhora: la consistència del catàleg cap als nostres consumidors (04-01) i la interoperabilitat amb les biblioteques OAuth genèriques, que llegeixen WWW-Authenticate per decidir si han de refrescar el token o demanar més permisos. No cal ampliar el catàleg d'errors: no_autenticat, token_caducat, permisos_insuficients i servei_no_disponible cobreixen tots els casos que emet el servidor de recursos.
Errors Comuns i Consells
Fer servir l'access token com a prova d'identitat. L'access token és per a l'API; la identitat la dona l'id_token d'OIDC. Confondre'ls obre atacs de substitució de token.
No validar aud. Un token emès per a una altra aplicació del mateix emissor obriria la teva API. És la comprovació més oblidada.
Acceptar qualsevol algorisme. Sense llista blanca, alg:none i la confusió RS/HS són explotables. Fixa ['RS256'].
Desar tokens a localStorage. Qualsevol XSS els roba. Galeta HttpOnly amb un BFF, o el clauer del sistema al mòbil.
Incrustar un client_secret en una SPA o app mòbil. És públic per definició. Client públic + PKCE.
Acceptar redirect_uri amb comodins. Una sola ruta controlable al teu domini es converteix en robatori de codis.
Ometre state perquè «ja fem servir PKCE». Protegeixen d'atacs diferents. Tots dos, sempre.
Demanar tots els àmbits «per si de cas». Baixa la conversió —l'usuari veu una pantalla alarmant— i augmenta el dany d'una bretxa. Demana el mínim i amplia quan calgui.
Consell: comença pel document de descobriment. /.well-known/openid-configuration et dona totes les URL. Configurar token_endpoint a mà és una font d'errors tontos.
Consell: prova la caducitat de debò. Configura un AS de proves amb tokens de 30 segons i comprova que el teu client refresca sol i que la teva API respon token_caducat amb el WWW-Authenticate correcte.
Consell: registra client_id als logs. Saber que una onada de 429 ve de CataBox i no de la teva SPA canvia completament el diagnòstic. Ho veurem a 04-07.
Exercicis
Exercici 1: triar el flux
Per a cada consumidor de la Botiga Aroma, indica el flux correcte, el tipus de client i els àmbits mínims. Justifica cada elecció.
- La SPA de la botiga (
https://botigaaroma.example), que permet comprar i ressenyar. - L'app Aroma Mòbil, amb les mateixes funcions.
- El backend de RàpidEnviaments, que marca comandes com a enviades.
- CataBox, que importa l'historial de compres de l'usuari.
- Un script intern nocturn que exporta estadístiques de vendes.
Exercici 2: trobar les fallades en una validació
Aquest middleware s'ha proposat per validar tokens OAuth. Troba almenys quatre fallades de seguretat i corregeix-les.
import jwt from 'jsonwebtoken';
export async function autenticarOAuth(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
const capcalera = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString());
const jwksResposta = await fetch('https://auth.botigaaroma.example/.well-known/jwks.json');
const { keys } = await jwksResposta.json();
const clau = keys.find((k) => k.kid === capcalera.kid) ?? keys[0];
const payload = jwt.verify(token, aPem(clau));
req.usuari = { id: payload.email, ambits: payload.scope };
next();
}Exercici 3: dissenyar els àmbits d'una funció nova
La Botiga Aroma permetrà que aplicacions de tercers gestionin la subscripció mensual de cafè d'un client: consultar-la, pausar-la, reprendre-la i canviar la varietat. Dissenya els àmbits necessaris, el text de consentiment de cadascun, i decideix quina combinació de rol i àmbit exigeix cada endpoint.
Solucions
Solució 1
| Consumidor | Flux | Tipus de client | Àmbits mínims |
|---|---|---|---|
| 1. SPA de la botiga | Authorization Code + PKCE (idealment amb BFF) | Públic | openid cafes.llegir comandes.llegir comandes.escriure ressenyes.escriure |
| 2. Aroma Mòbil | Authorization Code + PKCE | Públic | Els mateixos |
| 3. RàpidEnviaments | Client Credentials | Confidencial | enviaments.escriure |
| 4. CataBox | Authorization Code + PKCE | Confidencial (té backend) | openid comandes.llegir |
| 5. Script intern | Client Credentials | Confidencial | vendes.llegir (àmbit nou) |
Justificacions:
- 1 i 2 són públics encara que siguin nostres: el codi es descarrega al dispositiu i no pot desar un secret. I fan servir Authorization Code + PKCE, no el flux de contrasenya, per poder passar per 2FA i no gestionar contrasenyes.
- 3 i 5 no tenen usuari al davant, així que Client Credentials. El
subés la mateixa màquina i no hi ha consentiment. - 4 és confidencial perquè el bescanvi passa al seu servidor, però fa servir PKCE igualment: la recomanació actual és aplicar-lo sempre. Demana només
comandes.llegir; si demanéscomandes.escriurecaldria rebutjar el registre, perquè no ho necessita per al seu cas d'ús. - El cas 5 requereix ampliar el catàleg d'àmbits amb
vendes.llegir, i convé notar-ho explícitament: els àmbits, com els codis d'error, formen part del contracte i es documenten aopenapi.yaml.
Solució 2
Fallades:
- No valida
issniaud: accepta tokens de qualsevol emissor i emesos per a qualsevol altra API. - No fixa
algorithms: vulnerable aalg:nonei a la confusió RS/HS. ?? keys[0]: si elkidno coincideix amb cap clau, fa servir la primera «a veure si cola». Unkiddesconegut ha de ser un rebuig.- Descarrega el JWKS a cada petició: latència afegida, dependència dura de l'AS i un vector de denegació de servei (n'hi ha prou d'enviar tokens amb
kidinventats). id: payload.email: l'identificador estable éssub. El correu pot canviar o arribar sense verificar.ambits: payload.scopedeixa una cadena on s'espera un conjunt;.has()fallaria o, pitjor,.includes()donaria falsos positius (comandes.llegir«inclou»comandes.lleg).- Sense
try/catch: si falta la capçalera o el token està mal format, elsplito elJSON.parsellancen unTypeErrori acaba en un500en comptes d'un401— i senseWWW-Authenticate.
La correcció és el middleware de l'apartat 12: createRemoteJWKSet amb memòria cau per a 3 i 4, jwtVerify amb issuer, audience i algorithms per a 1 i 2, payload.sub per a 5, un Set per a 6 i el try/catch amb traducció d'errors per a 7.
Solució 3
Àmbits:
| Àmbit | Text de consentiment | Justificació |
|---|---|---|
subscripcions.llegir |
«Veure la teva subscripció de cafè i el seu proper lliurament» | Només lectura, risc baix |
subscripcions.escriure |
«Pausar, reprendre i canviar la teva subscripció de cafè» | Modifica l'estat; separat de la lectura |
Deliberadament no es crea un àmbit per acció (subscripcions.pausar, subscripcions.reprendre…): produiria una pantalla de consentiment illegible i cap d'aquestes accions no té un perfil de risc diferent de les altres. Tampoc no es reutilitza comandes.escriure, perquè una aplicació que gestiona subscripcions no ha de poder crear comandes soltes: barrejar les dues ampliaria el permís més enllà del necessari.
Endpoints:
| Endpoint | Rol | Àmbit | Nota |
|---|---|---|---|
GET /v1/clients/{id}/subscripcio |
client (propi), empleat, administrador |
subscripcions.llegir |
Comprovació de propietat obligatòria |
POST /v1/clients/{id}/subscripcio/pausa |
client (propi), administrador |
subscripcions.escriure |
Idempotent: pausar dues vegades ho deixa igual |
POST /v1/clients/{id}/subscripcio/represa |
client (propi), administrador |
subscripcions.escriure |
Ídem |
PATCH /v1/clients/{id}/subscripcio |
client (propi), administrador |
subscripcions.escriure |
Canvia la varietat; pedaç absolut |
DELETE /v1/clients/{id}/subscripcio |
client (propi) |
subscripcions.escriure |
Cancellació; un empleat no cancel·la pel seu compte |
router.post(
'/:id/subscripcio/pausa',
autenticar,
exigirRol('client', 'administrador'),
exigirAmbit('subscripcions.escriure'),
asincron(controladors.subscripcions.pausar) // a dins: comprovació de propietat
);Dues observacions finals. L'exigirRol('client', ...) no n'hi ha prou per impedir que un client pausi la subscripció d'un altre: això és un BOLA i es comprova al servei, com vam veure a 04-02. I les accions es modelen com a subrecursos substantius (/pausa, /represa) en comptes de verbs, coherentment amb 02-02 i amb els antipatrons de 04-01.
Conclusió
OAuth 2.0 resol un problema molt concret que l'autenticació de 03-06 no podia resoldre: que una aplicació de tercers accedeixi a les dades d'un usuari sense conèixer-ne la contrasenya, amb permisos acotats, caducitat i revocació. Has vist els quatre rols i, sobretot, que la nostra API és només el servidor de recursos: rep un token, el verifica i decideix; res més. Coneixes la diferència entre access token i refresh token, els àmbits dissenyats per a la Botiga Aroma i per què se separa sempre lectura d'escriptura; l'elecció entre token opac amb introspecció i JWT amb validació local, amb el seu compromís explícit sobre la revocació; el flux Authorization Code + PKCE amb el detall de quin atac evita exactament el code_verifier, per què l'implícit i el de contrasenya estan retirats, Client Credentials per a RàpidEnviaments i la rotació de refresh tokens amb detecció de reutilització. Has vist que OpenID Connect és la capa que respon «qui és» amb l'id_token, sub com a clau estable i /userinfo per al perfil. I al projecte ja tens src/config/oauth.js i src/middleware/autenticacio-oauth.js, amb verificació per JWKS i kid —que resol de passada la rotació de claus—, validació completa d'iss, aud, exp, nbf i algorisme, memòria cau de claus, tolerància de rellotge, el nou exigirAmbit i un selector que fa conviure els tokens propis amb els d'OAuth.
Ja saps qui crida i què pot fer. Falta quant pot cridar. A 04-04, Rate limiting i throttling, hi posarem límits: veurem per què tota API pública els necessita —abús, scraping del catàleg, bucles de clients mal programats, cost i equitat—, la diferència entre limitar, estrangular i posar quotes, i els quatre algorismes clàssics amb la seva comparativa, inclòs un cubell de fitxes implementat i comentat. Decidirem què es fa servir com a clau (IP amb el problema de NAT i X-Forwarded-For, client autenticat, client_id d'OAuth) i amb quins nivells per a anònims, clients, RàpidEnviaments i tauler; construirem src/middleware/limit-peticions.js amb express-rate-limit i la seva posició a src/app.js, amb magatzem a Redis per a diverses instàncies; retornarem el 429 amb Retry-After i les capçaleres Aroma-RateLimit-*; i veurem què ha de fer un client ben educat amb el seu retrocés exponencial i el seu jitter.
Curs de REST API: Principis de Disseny i Desenvolupament d'APIs RESTful
Mòdul 1: Introducció a les APIs RESTful
- Què és una API?
- Història i evolució de les APIs
- Fonaments d'HTTP per a APIs
- Principis bàsics de REST
- Model de maduresa de Richardson i HATEOAS
- REST vs. SOAP
- REST davant de GraphQL, gRPC i webhooks
Mòdul 2: Disseny d'APIs RESTful
- Principis de disseny d'APIs RESTful
- Recursos i URIs
- Mètodes HTTP
- Codis d'estat HTTP
- Representacions, capçaleres i negociació de contingut
- Filtratge, ordenació, paginació i cerca
- Versionat d'APIs
- Documentació d'APIs
Mòdul 3: Desenvolupament d'APIs RESTful
- Configuració de l'entorn de desenvolupament
- Creació d'un servidor bàsic
- Gestió de peticions i respostes
- Validació de dades d'entrada
- Persistència i capa d'accés a dades
- Autenticació i autorització
- Gestió d'errors
- Proves i validació
Mòdul 4: Bones Pràctiques i Seguretat
- Bones pràctiques en el disseny d'APIs
- Seguretat en APIs RESTful
- OAuth 2.0 i OpenID Connect a la pràctica
- Rate limiting i throttling
- CORS i polítiques de seguretat
- Memòria cau HTTP i rendiment
- Observabilitat: logs, mètriques i traces
Mòdul 5: Eines i Frameworks
- Postman per a proves d'APIs
- Swagger i OpenAPI per a documentació
- Frameworks populars per a APIs RESTful
- Contractes, mocks i proves automatitzades d'API
- Integració contínua i desplegament
- API gateways i portals de desenvolupador
