@nodefony/security 10.0.0-alpha.8 → 10.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/nodefony/command/security-user-add.js +2 -18
  5. package/dist/nodefony/command/security-user-password.js +2 -2
  6. package/dist/nodefony/config/config.js +2 -2
  7. package/dist/nodefony/service/auditService.js +3 -2
  8. package/dist/nodefony/service/tokenService.js +3 -2
  9. package/dist/nodefony/service/totp.js +5 -3
  10. package/dist/nodefony/service/webAuthn.js +3 -2
  11. package/dist/nodefony/service/webhooks.js +5 -3
  12. package/dist/nodefony/src/token/MemoryTokenStore.js +18 -5
  13. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +15 -3
  14. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +24 -6
  15. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +18 -10
  16. package/dist/types/nodefony/command/security-user-add.d.ts +0 -16
  17. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +3 -3
  18. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +0 -9
  19. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +0 -8
  20. package/docs/api-keys.md +62 -60
  21. package/docs/audit.md +77 -77
  22. package/docs/authenticators.md +55 -43
  23. package/docs/authorization.md +13 -13
  24. package/docs/cors.md +29 -29
  25. package/docs/csrf.md +26 -26
  26. package/docs/external-jwt.md +4 -4
  27. package/docs/firewall.md +29 -29
  28. package/docs/headers.md +26 -26
  29. package/docs/oauth2.md +60 -60
  30. package/docs/obtenir-un-jeton.md +2 -2
  31. package/docs/tokens.md +55 -55
  32. package/docs/totp.md +73 -72
  33. package/docs/webauthn.md +81 -81
  34. package/docs/webhooks.md +43 -37
  35. package/package.json +15 -12
package/docs/headers.md CHANGED
@@ -122,8 +122,8 @@ fait l'inverse : **tout ce qui est constant est calculé une fois au démarrage*
122
122
  statique) et la **gèle** avec `Object.freeze` (`securityHeaders.ts:77`). Par requête, le firewall
123
123
  se contente de la parcourir et de la poser : zéro concaténation, zéro objet créé.
124
124
  - Côté transport, même principe : `HttpKernel.computeSecurityHeaderCaches()`
125
- (`http-kernel.ts:330`) précalcule la chaîne HSTS (`max-age`, `includeSubDomains`, `preload`) au
126
- boot ; `onHttpRequest` (`http-kernel.ts:819`) ne fait plus que trois `setHeader`.
125
+ (`http-kernel.ts:335`) précalcule la chaîne HSTS (`max-age`, `includeSubDomains`, `preload`) au
126
+ boot ; `onHttpRequest` (`http-kernel.ts:949`) ne fait plus que trois `setHeader`.
127
127
  - Le seul coût variable est le **nonce CSP**, et il est **paresseux** : `Context.cspNonce`
128
128
  (`Context.ts:253`) ne génère ses 128 bits (`randomBytes(16)` en base64) qu'à la première lecture,
129
129
  puis mémoïse. Une réponse qui n'a aucun script inline à signer ne paie aucun appel crypto.
@@ -173,7 +173,7 @@ export default defineConfig(() => ({
173
173
  ```
174
174
 
175
175
  Les clés sont **typées et auto-complétées** : le module augmente le registre `NodefonyModuleConfig`
176
- du core (`index.ts:28`), donc `use("@nodefony/security", …)` propose les clés **et** les valeurs
176
+ du core (`index.ts:33`), donc `use("@nodefony/security", …)` propose les clés **et** les valeurs
177
177
  d'enum (`referrerPolicy`, `coop`, `corp`…). Une valeur hors enum casse le boot, pas la production.
178
178
 
179
179
  ### 2. Ce qu'on observe
@@ -255,7 +255,7 @@ Choisir en cinq secondes — puis le détail dans les cartes.
255
255
  **La menace** : n'importe quelle entrée non échappée (commentaire, nom d'utilisateur, paramètre
256
256
  réfléchi) devient un vecteur d'exécution de code. Le CSP est le filet quand l'échappement a raté.
257
257
 
258
- **Le défaut Nodefony** est délibérément « secure-but-usable » (`config.ts:218`) : seul `script-src`
258
+ **Le défaut Nodefony** est délibérément « secure-but-usable » (`config.ts:238`) : seul `script-src`
259
259
  est **strict** — `'self'` plus le nonce de la requête, ce qui est la vraie défense XSS. Le reste
260
260
  couvre les besoins réels d'une app moderne (CSS-in-JS via `style-src 'unsafe-inline'`, images
261
261
  `data:`/`blob:`, workers, fetch/WS same-origin) et ajoute les durcissements gratuits :
@@ -275,8 +275,8 @@ privilégient `frame-ancestors`, `X-Frame-Options` reste le filet pour les ancie
275
275
  victime est capté par ton interface.
276
276
 
277
277
  Posé par le **transport** depuis un cache calculé au boot — `secFrameOptions`
278
- (`http-kernel.ts:270`) — et configuré côté `@nodefony/http` avec `frameOptions`
279
- (`http/nodefony/config/config.ts:121`), qui vaut `DENY` par défaut. `SAMEORIGIN` si ton propre site
278
+ (`http-kernel.ts:267`) — et configuré côté `@nodefony/http` avec `frameOptions`
279
+ (`http/nodefony/config/config.ts:136`), qui vaut `DENY` par défaut. `SAMEORIGIN` si ton propre site
280
280
  s'auto-encadre. C'est un des trois en-têtes que security **ne ré-émet pas** : il doit valoir aussi
281
281
  pour un HTML statique servi directement depuis `public/`.
282
282
 
@@ -297,15 +297,15 @@ vient précisément des fichiers servis hors pipeline applicatif — un banc liv
297
297
  installer un intercepteur.
298
298
 
299
299
  La chaîne est assemblée au boot par `HttpKernel.computeSecurityHeaderCaches()`
300
- (`http-kernel.ts:330`) : `max-age`, puis `includeSubDomains` et `preload` selon la config.
300
+ (`http-kernel.ts:335`) : `max-age`, puis `includeSubDomains` et `preload` selon la config.
301
301
 
302
302
  Elle n'est posée que **sur une réponse HTTPS ou HTTP/2** — le cache `secHsts` est conditionné au type
303
- de serveur (`http-kernel.ts:965`). C'est conforme à la RFC 6797, qui veut qu'un HSTS reçu en clair
303
+ de serveur (`http-kernel.ts:969`). C'est conforme à la RFC 6797, qui veut qu'un HSTS reçu en clair
304
304
  soit ignoré : l'émettre sur du HTTP simple ne ferait que polluer. Défaut : un an, sous-domaines
305
305
  inclus.
306
306
 
307
307
  > [!CAUTION]
308
- > `preload: true` (`http/nodefony/config/config.ts:92`) inscrit ton domaine dans la liste
308
+ > `preload: true` (`http/nodefony/config/config.ts:107`) inscrit ton domaine dans la liste
309
309
  > pré-chargée des navigateurs. C'est un **engagement quasi irréversible** : tout sous-domaine
310
310
  > incapable de servir en HTTPS devient inaccessible, et la sortie de liste prend des mois. À ne
311
311
  > jamais activer « pour voir ».
@@ -315,7 +315,7 @@ inclus.
315
315
  **La menace** : la fuite d'URL. Chemins parlants, identifiants de session dans une query, jetons de
316
316
  réinitialisation — tout part chez le site suivant via le `Referer`.
317
317
 
318
- Défaut Nodefony : `no-referrer` (`security/nodefony/config/config.ts:263`), la valeur la plus stricte. La valeur est un
318
+ Défaut Nodefony : `no-referrer` (`security/nodefony/config/config.ts:278`), la valeur la plus stricte. La valeur est un
319
319
  **enum W3C fermé** — huit valeurs validées au boot, donc pas de faute de frappe qui passerait en
320
320
  silence (l'écriture libre `no-refferer` casserait la protection sans prévenir).
321
321
 
@@ -379,7 +379,7 @@ config security est la première source de confusion sur ce sujet.
379
379
 
380
380
  ### Couche applicative — `use("@nodefony/security", { headers })`
381
381
 
382
- Dérivé du schéma Zod `headersSchema` (`config.ts:194`).
382
+ Dérivé du schéma Zod `headersSchema` (`config.ts:206`).
383
383
 
384
384
  <!-- prettier-ignore -->
385
385
  | Option | Type | Défaut | Effet |
@@ -396,8 +396,8 @@ Dérivé du schéma Zod `headersSchema` (`config.ts:194`).
396
396
 
397
397
  ### Socle transport — `use("@nodefony/http", { securityHeaders })`
398
398
 
399
- Dérivé de `securityHeadersSchema` (`http/nodefony/config/config.ts:108`). Ces trois réglages sont
400
- **éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts:290`)
399
+ Dérivé de `securityHeadersSchema` (`http/nodefony/config/config.ts:123`). Ces trois réglages sont
400
+ **éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts:385`)
401
401
  recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
402
402
 
403
403
  | Option | Type | Défaut | Effet |
@@ -411,10 +411,10 @@ recalcule les caches, donc la valeur suivante s'applique sans redémarrage.
411
411
 
412
412
  > [!WARNING]
413
413
  > Les clés `hsts`, `hstsMaxAgeS`, `frameguard` et `noSniff` **existent** dans la config security
414
- > (`config.ts:200`, `config.ts:227`, `config.ts:233`) mais **ne pilotent rien** : la couche
414
+ > (`config.ts:212`, `config.ts:223`, `config.ts:247`, `config.ts:257`) mais **ne pilotent rien** : la couche
415
415
  > applicative ne les lit pas (`securityHeaders.ts:6`), elles ne servent qu'à l'introspection
416
416
  > affichée dans Studio. Pour changer réellement `X-Frame-Options`, c'est `securityHeaders.frameOptions`
417
- > **du module http**. Même remarque pour `hidePoweredBy` (`config.ts:254`) : Nodefony n'émet aucun
417
+ > **du module http**. Même remarque pour `hidePoweredBy` (`config.ts:282`) : Nodefony n'émet aucun
418
418
  > `X-Powered-By`, l'option est un no-op documenté.
419
419
 
420
420
  ## 🏗️ Le CSP en détail — deux régimes, trois façons de l'étendre
@@ -439,7 +439,7 @@ Le chemin complet, sans surprise :
439
439
 
440
440
  1. **Au boot**, la chaîne CSP est **pré-découpée** autour de `{{nonce}}` (`securityHeaders.ts:58`).
441
441
  Aucun parsing ni regex n'aura lieu pendant une requête.
442
- 2. **Par requête**, `Firewall.applySecurityHeaders()` (`firewall.ts:835`) lit `context.cspNonce` —
442
+ 2. **Par requête**, `Firewall.applySecurityHeaders()` (`firewall.ts:1045`) lit `context.cspNonce` —
443
443
  ce qui **génère** le jeton à cet instant (`Context.ts:253`) — puis appelle
444
444
  `SecurityHeaders.cspFor()` (`securityHeaders.ts:100`) : un seul `join`.
445
445
  3. **Dans la vue**, le contrôleur relit `context.cspNonce`, qui est **mémoïsé** : l'en-tête et le
@@ -452,7 +452,7 @@ rester imprévisible, jamais pilotable par le client — contrairement au `reque
452
452
  une corrélation entrante.
453
453
 
454
454
  **Placement dans le pipeline** : `applySecurityHeaders` est appelé **après le resolve** et **avant**
455
- le repli statique et le `writeHead` (`http-kernel.ts:1372`). Cet ordre n'est pas cosmétique : il
455
+ le repli statique et le `writeHead` (`http-kernel.ts:1386`). Cet ordre n'est pas cosmétique : il
456
456
  faut que le routeur ait posé les directives `@Csp` de la route pour pouvoir les fusionner, et il faut
457
457
  être avant l'écriture des en-têtes pour pouvoir en poser.
458
458
 
@@ -476,7 +476,7 @@ directive absente est ajoutée en fin. La fonction est **pure et déterministe**
476
476
  les tests fiables.
477
477
 
478
478
  **Coût** : le merge d'un module est payé **une fois**, au (dés)enregistrement
479
- (`Firewall.#rebuildSecurityHeaders()`, `firewall.ts:1085`), jamais par requête. Le merge d'une route
479
+ (`Firewall.#rebuildSecurityHeaders()`, `firewall.ts:1101`), jamais par requête. Le merge d'une route
480
480
  `@Csp` est payé **uniquement sur les routes décorées** (`SecurityHeaders.cspForExtra()`,
481
481
  `securityHeaders.ts:115`) ; le cas courant reste le simple `join`.
482
482
 
@@ -502,15 +502,15 @@ Trois propriétés à retenir :
502
502
 
503
503
  - **Aucun couplage** : la résolution par nom de service évite un cycle de dépendances, et
504
504
  `registerCspOrigins` est optionnel — un module fonctionne dans une app **sans** security.
505
- - **Réversible** : `Firewall.unregisterCspOrigins()` (`firewall.ts:1074`) retire le fragment et
505
+ - **Réversible** : `Firewall.unregisterCspOrigins()` (`firewall.ts:1090`) retire le fragment et
506
506
  reconstruit le CSP de base. C'est ce que fait `@nodefony/frontend` à l'arrêt du serveur Vite.
507
507
  - **Idempotent** : la reconstruction repart **toujours** du `headers.csp` d'origine
508
- (`firewall.ts:1088`), jamais d'un CSP déjà fusionné — pas d'accumulation entre deux
508
+ (`firewall.ts:1104`), jamais d'un CSP déjà fusionné — pas d'accumulation entre deux
509
509
  enregistrements.
510
510
 
511
511
  L'exemple de référence vit dans le framework : en développement, `@nodefony/frontend` déclare les
512
512
  origines du serveur Vite et `'unsafe-eval'` (exigé par le Fast Refresh de React) via
513
- `FrontendService.#viteCspFragment()` (`FrontendService.ts:1012`) — ce qui explique qu'un CSP observé
513
+ `FrontendService.#viteCspFragment()` (`FrontendService.ts:1034`) — ce qui explique qu'un CSP observé
514
514
  en dev soit plus large qu'en production, où ce fragment n'existe pas.
515
515
 
516
516
  ## 📜 Normes appliquées
@@ -519,12 +519,12 @@ en dev soit plus large qu'en production, où ce fragment n'existe pas.
519
519
  | ------------------------------------ | -------------------------------- | ------------------------------------------------------------ |
520
520
  | Politique de sécurité du contenu | W3C CSP Level 3 | `mergeCspFragments()` (`csp.ts:56`), directive non dupliquée |
521
521
  | Nonce CSP (unicité, imprévisibilité) | W3C CSP Level 3 §6.7.4 | `Context.cspNonce` — 128 bits CSPRNG (`Context.ts:253`) |
522
- | HSTS | RFC 6797 | posé sur TLS uniquement (`http-kernel.ts:839`) |
522
+ | HSTS | RFC 6797 | posé sur TLS uniquement (`http-kernel.ts:969`) |
523
523
  | Champ structuré booléen | RFC 8941 | `Origin-Agent-Cluster: ?1` (`securityHeaders.ts:75`) |
524
- | Referrer-Policy | W3C Referrer Policy (enum fermé) | 8 valeurs validées au boot (`config.ts:239`) |
524
+ | Referrer-Policy | W3C Referrer Policy (enum fermé) | 8 valeurs validées au boot (`config.ts:267`) |
525
525
  | Isolation cross-origin | WHATWG HTML (COOP/COEP/CORP) | `securityHeaders.ts:71` |
526
526
  | Anti-MIME-sniffing | WHATWG Fetch (`nosniff`) | `secContentTypeOptions` (`http-kernel.ts:963`) |
527
- | Durcissement en-têtes | OWASP Secure Headers | `computeSecurityHeaderCaches()` (`http-kernel.ts:330`) |
527
+ | Durcissement en-têtes | OWASP Secure Headers | `computeSecurityHeaderCaches()` (`http-kernel.ts:335`) |
528
528
 
529
529
  ## ⚡ Performance & mémoire
530
530
 
@@ -538,7 +538,7 @@ Le coût est concentré au boot, par construction :
538
538
  court-circuite entièrement ce chemin sinon. La paresse de `Context.cspNonce` (`Context.ts:253`)
539
539
  protège en plus les chemins internes qui n'atteignent jamais le firewall.
540
540
  - **Merge CSP** : jamais dans le chemin chaud. Le fragment d'un module est fusionné à
541
- l'enregistrement (`firewall.ts:1067`) ; celui d'une route ne coûte que sur les routes `@Csp`.
541
+ l'enregistrement (`firewall.ts:1086`) ; celui d'une route ne coûte que sur les routes `@Csp`.
542
542
  - **Socle transport** : trois `setHeader` sur des chaînes précalculées (`http-kernel.ts:958-966`), avec
543
543
  un test `!== null` qui annule le coût des en-têtes désactivés.
544
544
 
@@ -570,7 +570,7 @@ porte sa description, ce qui en fait la référence toujours à jour des défaut
570
570
  | CSP contenant `'nonce-{{nonce}}'` littéral | `cspNonces` désactivé, placeholder laissé | Nodefony purge le résiduel (`securityHeaders.ts:63`) ; retirer le placeholder |
571
571
  | Une extension `script-src` de module sans effet | Second en-tête / directive dupliquée (ignorée, W3C CSP3 §3) | Déclarer un fragment via `registerCspOrigins()`, jamais un `setHeader` |
572
572
  | Assets tiers cassés après activation de l'isolation | `coep: "require-corp"` exige CORP/CORS sur **chaque** ressource | Retirer `coep` ou faire annoncer les ressources |
573
- | HSTS absent en développement | Posé sur TLS uniquement (`http-kernel.ts:839`) | Comportement conforme RFC 6797 — vérifier sur le port HTTPS |
573
+ | HSTS absent en développement | Posé sur TLS uniquement (`http-kernel.ts:969`) | Comportement conforme RFC 6797 — vérifier sur le port HTTPS |
574
574
  | Le CSP est plus large en dev qu'en prod | `@nodefony/frontend` déclare les origines Vite (`FrontendService.ts:695`) | Attendu : le fragment n'existe pas en production |
575
575
  | Aucun en-tête applicatif | Module security absent ou `headers.enabled: false` | Le socle transport reste actif ; réactiver security pour CSP/Referrer/isolation |
576
576
  | Domaine injoignable après activation de `preload` | Inscription à la liste pré-chargée, sortie très lente | Ne jamais activer sans plan HTTPS sur **tous** les sous-domaines |
package/docs/oauth2.md CHANGED
@@ -111,15 +111,15 @@ qui les ferme ici :
111
111
 
112
112
  - **Vol de jeton via XSS** — si un `access_token` transite par le navigateur, tout script injecté
113
113
  peut le lire. _Fermé par construction_ : aucun jeton ne sort du serveur, le résultat est un cookie
114
- de session opaque (`OAuth2Controller.ts:155`).
114
+ de session opaque (`OAuth2Controller.ts:192`).
115
115
  - **Interception du `code`** — un `code` capté (log de proxy, historique, redirection ouverte) est
116
116
  échangeable par l'attaquant. _Fermé par **PKCE**_ : l'échange exige le `code_verifier` resté en
117
117
  session (`OAuth2Service.createAuthorization()`, `oauth2.ts:232-238`).
118
118
  - **CSRF de login** — un tiers force ta victime à terminer **son** flux à lui : elle se retrouve
119
119
  connectée sur le compte de l'attaquant, qui lit ensuite ce qu'elle y dépose. _Fermé par le `state`_
120
- comparé au retour (`OAuth2Controller.callback()`, `OAuth2Controller.ts:137-145`).
120
+ comparé au retour (`OAuth2Controller.callback()`, `OAuth2Controller.ts:150-158`).
121
121
  - **Mix-up d'IdP** — un `code` obtenu chez un fournisseur malveillant est présenté au callback d'un
122
- fournisseur de confiance. _Fermé par la vérification de l'`iss`_ (`oauth2.ts:170-174`) **et** par
122
+ fournisseur de confiance. _Fermé par la vérification de l'`iss`_ (`oauth2.ts:265-273`) **et** par
123
123
  l'exigence « même fournisseur qu'à l'aller » côté controller (`OAuth2Controller.ts:170`).
124
124
 
125
125
  > [!IMPORTANT]
@@ -133,7 +133,7 @@ Trois partis pris, tous vérifiables au code.
133
133
 
134
134
  **Le service ne touche ni HTTP ni session.** `OAuth2Service` rend à l'appelant les éléments à
135
135
  persister (`url`, `state`, `codeVerifier`) et un simple `{ identifier }` en sortie
136
- (`IOAuthAuthorization`, `oauth2.ts:21`). Conséquence pratique : la logique OAuth se teste **sans
136
+ (`IOAuthAuthorization`, `oauth2.ts:82`). Conséquence pratique : la logique OAuth se teste **sans
137
137
  serveur**, comme `AuthFlow`. Le transport (cookies, redirections 302) vit dans le controller BFF.
138
138
 
139
139
  **Le login social finit exactement comme un login classique.** Le callback appelle
@@ -144,11 +144,11 @@ d'audit. Il n'existe **aucun** authenticator `oauth2` dans la chaîne du firewal
144
144
  c'est l'authenticator `session` qui identifie chaque requête, comme après un mot de passe.
145
145
 
146
146
  **Coût nul quand on ne s'en sert pas.** Aucune dépendance tierce : le client OAuth 2.0 est écrit
147
- dans le module (`oauth2Client.ts:207`), et `jose` — seul recours externe, pour lire les claims de
147
+ dans le module (`oauth2Client.ts:321`), et `jose` — seul recours externe, pour lire les claims de
148
148
  l'ID token — est importé **paresseusement**. Les fournisseurs sont construits au premier login puis
149
149
  mémoïsés (`OAuth2Service.#resolveProvider()`, `oauth2.ts:290`) : c'est là, une seule fois par
150
150
  processus, que les points d'entrée d'un émetteur OIDC sont découverts. Les routes ne sont montées
151
- que si le service existe (`framework/index.ts:379`) : sans social login configuré, la surface HTTP
151
+ que si le service existe (`framework/index.ts:460`) : sans social login configuré, la surface HTTP
152
152
  est **404**, pas « désactivée ».
153
153
 
154
154
  Au boot, la config est validée et les fournisseurs configurés sont confrontés au registre : un nom
@@ -215,8 +215,8 @@ export default defineConfig<typeof env>((ctx) => ({
215
215
  ### Les routes sont FOURNIES — tu n'écris aucun controller
216
216
 
217
217
  `mountOAuth2Routes()` (`OAuth2Controller.ts:234`) monte trois routes sous
218
- `/nodefony/security/api/oauth2` (`OAuth2Controller.ts:234`), et **seulement si** le service `oauth2`
219
- est présent (`framework/index.ts:379`) :
218
+ `/nodefony/security/api/oauth2` (`OAuth2Controller.ts:236`), et **seulement si** le service `oauth2`
219
+ est présent (`framework/index.ts:460`) :
220
220
 
221
221
  | Route | Rôle |
222
222
  | ---------------------------- | --------------------------------------------------------------------- |
@@ -274,20 +274,20 @@ Séquence identique prouvée de bout en bout sur serveur réel par `oauth2-flow.
274
274
  2. un **`code_verifier`** — **seulement si** le fournisseur pratique PKCE (`usesPkce`,
275
275
  `oauth2.ts:235-237`) ; `null` sinon (GitHub) ;
276
276
  3. l'**URL d'autorisation** construite par l'adaptateur du fournisseur, avec les scopes effectifs
277
- (ceux de la config, sinon les scopes par défaut du fournisseur, `oauth2.ts:216`).
277
+ (ceux de la config, sinon les scopes par défaut du fournisseur, `oauth2.ts:321`).
278
278
 
279
279
  Le controller pose les trois valeurs en session, **persiste** (`session.save()` — pas seulement en
280
- mémoire, `OAuth2Controller.ts:105-108`), puis redirige en 302.
280
+ mémoire, `OAuth2Controller.ts:145`), puis redirige en 302.
281
281
 
282
282
  ### Étape 2 — le retour, validé avant tout appel réseau
283
283
 
284
284
  `OAuth2Controller.callback()` (`OAuth2Controller.ts:150`) travaille dans cet ordre, et l'ordre est la
285
285
  défense :
286
286
 
287
- 1. **lire l'état de session, puis l'invalider immédiatement** (`OAuth2Controller.ts:126-129`) — le
287
+ 1. **lire l'état de session, puis l'invalider immédiatement** (`OAuth2Controller.ts:158-166`) — le
288
288
  `state` est à **usage unique** : un rejeu du même retour échoue, même avec le bon cookie ;
289
289
  2. **comparer** : `code` et `state` présents, `state` reçu ≡ `state` attendu, **et** fournisseur du
290
- callback ≡ fournisseur démarré (`OAuth2Controller.ts:137-145`). Un seul écart → `302` vers
290
+ callback ≡ fournisseur démarré (`OAuth2Controller.ts:174-182`). Un seul écart → `302` vers
291
291
  `failureRedirect`, **sans jamais contacter le fournisseur** ;
292
292
  3. seulement ensuite, `exchangeAndProvision()`.
293
293
 
@@ -296,15 +296,15 @@ défense :
296
296
  `OAuth2Service.exchangeAndProvision()` (`oauth2.ts:254`) enchaîne :
297
297
 
298
298
  1. **anti-mix-up** — si le fournisseur annonce un émetteur attendu, l'`iss` reçu doit correspondre,
299
- et un `iss` **absent** est un rejet, pas une tolérance (`oauth2.ts:170-174`) ;
299
+ et un `iss` **absent** est un rejet, pas une tolérance (`oauth2.ts:265-273`) ;
300
300
  2. **échange** du `code` sur le canal serveur, avec le `code_verifier`
301
- (`validateAuthorizationCode`, `oauth2.ts:256`), puis lecture du profil (`fetchProfile`,
301
+ (`validateAuthorizationCode`, `oauth2.ts:274`), puis lecture du profil (`fetchProfile`,
302
302
  `oauth2.ts:275`) ;
303
303
  3. **provisionnement** du Shadow User avec la politique effective — rôles par défaut surchargeables
304
304
  **par fournisseur** (`oauth2.ts:279-280`), `allowSignup` global (`oauth2.ts:283`).
305
305
 
306
306
  Toute erreur de cette étape est convertie en **échec uniforme** par le controller (`302
307
- failureRedirect`, `OAuth2Controller.ts:157-160`) : le client ne distingue pas un `iss` invalide d'un
307
+ failureRedirect`, `OAuth2Controller.ts:198-209`) : le client ne distingue pas un `iss` invalide d'un
308
308
  échange refusé ou d'un signup interdit.
309
309
 
310
310
  ## 🧑‍⚖️ Le Shadow User — l'identité locale, et pourquoi OAuth n'accorde aucun droit
@@ -314,13 +314,13 @@ compte externe : le _Shadow User_. C'est cette ligne qui porte l'identifiant, le
314
314
  actif/verrouillé — donc **tout** ce dont l'autorisation a besoin.
315
315
 
316
316
  Le contrat s'appelle `IOAuthUserProvisioner` (`IOAuthUserProvisioner.ts:61`) ; l'implémentation par
317
- défaut est `UserService.provisionOAuthUser()` (`UserService.ts:306`), en **find-or-create** :
317
+ défaut est `UserService.provisionOAuthUser()` (`UserService.ts:363`), en **find-or-create** :
318
318
 
319
319
  | Situation au retour du fournisseur | Comportement |
320
320
  | ---------------------------------------- | ------------------------------------------------------------------------------ |
321
321
  | Lien social déjà connu | Le compte existant est rendu tel quel — rien n'est créé, rien n'est réécrit. |
322
322
  | Lien inconnu, `allowSignup: true` | Création JIT : `password: null`, rôles = `defaultRoles`, lien social persisté. |
323
- | Lien inconnu, `allowSignup: false` | **Échec fail-closed** (`UserService.ts:363`) — un compte lié est exigé. |
323
+ | Lien inconnu, `allowSignup: false` | **Échec fail-closed** (`UserService.ts:374`) — un compte lié est exigé. |
324
324
  | E-mail identique à un compte local | **Aucune liaison automatique** — un compte SÉPARÉ est créé. |
325
325
  | Même `providerId` chez deux fournisseurs | Comptes séparés (le couple `provider` + `providerId` fait la clé). |
326
326
 
@@ -337,14 +337,14 @@ Conséquence assumée : l'utilisateur qui avait un mot de passe et clique « ave
337
337
  faite **utilisateur déjà connecté** — jamais un effet de bord du login.
338
338
 
339
339
  L'identifiant du compte créé dérive de l'e-mail si le fournisseur en donne un, sinon d'une clé
340
- préfixée `provider:providerId` — jamais de collision entre fournisseurs (`UserService.ts:325-326`).
340
+ préfixée `provider:providerId` — jamais de collision entre fournisseurs (`UserService.ts:382-383`).
341
341
 
342
342
  ### Les rôles sont posés à la création, et plus jamais
343
343
 
344
- `defaultRoles` s'applique **au moment du `create`** (`UserService.ts:348`). Un second login
344
+ `defaultRoles` s'applique **au moment du `create`** (`UserService.ts:405`). Un second login
345
345
  n'écrase rien : promouvoir quelqu'un dans ta base reste effectif, et modifier `defaultRoles` en
346
346
  config ne repeint pas les comptes existants. C'est la traduction de la règle « OAuth =
347
- authentification, pas autorisation » (`oauth2.ts:279-283`, `config.ts:1042-1046`).
347
+ authentification, pas autorisation » (`oauth2.ts:279-283`, `config.ts:1048-1053`).
348
348
 
349
349
  > [!TIP]
350
350
  > Un fournisseur social ne doit **jamais** figurer dans le chemin d'obtention d'un rôle privilégié.
@@ -363,9 +363,9 @@ et la charge brute `raw`.
363
363
 
364
364
  ## 🧩 Fournisseurs — catalogue et extension
365
365
 
366
- Un fournisseur est un adaptateur qui implémente `IOAuthProvider` (`IOAuthProvider.ts:21`) : il masque
366
+ Un fournisseur est un adaptateur qui implémente `IOAuthProvider` (`IOAuthProvider.ts:53`) : il masque
367
367
  les divergences (PKCE ou non, profil par ID token ou par appel d'API) derrière un contrat unique.
368
- Quatre sont livrés, résolus par nom via le registre `oauthProviderRegistry.ts:50`.
368
+ Quatre sont livrés, résolus par nom via le registre `oauthProviderRegistry.ts:62`.
369
369
 
370
370
  | Nom | Famille | PKCE | `iss` vérifié | Profil lu depuis | Scopes par défaut |
371
371
  | ---------- | ---------------- | :--: | --------------------- | ----------------- | ---------------------------- |
@@ -376,23 +376,23 @@ Quatre sont livrés, résolus par nom via le registre `oauthProviderRegistry.ts:
376
376
 
377
377
  ### `google` — OIDC, le cas nominal
378
378
 
379
- Construit par le helper générique `createOidcProvider()` (`oidc.ts:103`) : PKCE systématique
380
- (`usesPkce: true`, `oidc.ts:111`), émetteur figé `https://accounts.google.com`
381
- (`oauthProviderRegistry.ts:80`). Ses points d'entrée ne sont **pas** écrits en dur : ils sont
379
+ Construit par le helper générique `createOidcProvider()` (`oidc.ts:106`) : PKCE systématique
380
+ (`usesPkce: true`, `oidc.ts:114`), émetteur figé `https://accounts.google.com`
381
+ (`oauthProviderRegistry.ts:90`). Ses points d'entrée ne sont **pas** écrits en dur : ils sont
382
382
  demandés à l'émetteur (RFC 8414, cf. « Découverte » plus bas). Le profil se lit dans l'**ID token** —
383
- claims standard `sub`, `email`, `email_verified`, `name` (`oidc.ts:134`), après les contrôles
383
+ claims standard `sub`, `email`, `email_verified`, `name` (`oidc.ts:146`), après les contrôles
384
384
  obligatoires d'OpenID Connect Core §3.1.3.7 : `iss`, `aud`, `exp`, et un `sub` non vide
385
- (`assertIdTokenClaims()`, `oidc.ts:132`). Pas d'identifiant stable, pas d'identité.
385
+ (`assertIdTokenClaims()`, `oidc.ts:139`). Pas d'identifiant stable, pas d'identité.
386
386
 
387
387
  ### `keycloak` — OIDC self-hosted, l'émetteur vient de ta config
388
388
 
389
389
  Même helper, mais l'**issuer** (URL du realm) sert à la fois à découvrir les points d'entrée et à
390
- valider l'`iss` (`oauthProviderRegistry.ts:85`). Il est donc **obligatoire** : sans lui, la fabrique
390
+ valider l'`iss` (`oauthProviderRegistry.ts:95-97`). Il est donc **obligatoire** : sans lui, la fabrique
391
391
  lève au premier login avec un message explicite.
392
392
 
393
393
  ### `oidc` — n'importe quel serveur OpenID Connect
394
394
 
395
- La même mécanique, sans nom de marque : l'entrée `oidc` (`oauthProviderRegistry.ts:89`) prend
395
+ La même mécanique, sans nom de marque : l'entrée `oidc` (`oauthProviderRegistry.ts:101`) prend
396
396
  l'émetteur de sa configuration et n'a besoin de rien d'autre. C'est elle qui rend inutile une classe
397
397
  par fournisseur.
398
398
 
@@ -417,7 +417,7 @@ anti-mix-up **principale** est ailleurs — chaque fournisseur a son URL de redi
417
417
  (`…/{provider}/callback`) et le flux vérifie que le fournisseur de retour est celui qui a démarré,
418
418
  ce que la RFC 9700 §4.4.2.2 donne comme la protection de référence. `iss` est la seconde ceinture.
419
419
 
420
- La politique est portée par le fournisseur (`issuerPolicy`, `IOAuthProvider.ts:61`) et remplie par
420
+ La politique est portée par le fournisseur (`issuerPolicy`, `IOAuthProvider.ts:65`) et remplie par
421
421
  la découverte ; elle vaut `null` pour un fournisseur non-OIDC, qui ne relève pas de cette défense.
422
422
 
423
423
  ### Découverte des points d'entrée (RFC 8414)
@@ -429,7 +429,7 @@ points d'entrée sont demandés à l'émetteur, une seule fois par processus, au
429
429
  bien connues (§3.1 : insertion oauth → insertion oidc → ajout oidc) et l'égalité stricte du §3.3
430
430
  vivent dans le cœur (`nodefony` → `src/oauth/authorizationServer.ts`), qui s'en sert aussi pour
431
431
  PUBLIER nos propres métadonnées. `metadata.ts` n'ajoute que le transport : requête bornée, sans
432
- redirection suivie, avec un délai d'attente (`discoverAuthorizationServer()`, `metadata.ts:126`).
432
+ redirection suivie, avec un délai d'attente (`discoverAuthorizationServer()`, `metadata.ts:132`).
433
433
 
434
434
  Deux refus valent d'être connus. Un document dont l'`issuer` diffère de celui demandé est rejeté
435
435
  **sans se rabattre** sur l'URL suivante — se rabattre masquerait un document hostile derrière un 404.
@@ -445,16 +445,16 @@ donnerait l'illusion de PKCE.
445
445
  ### `github` — OAuth simple, l'archétype non-OIDC
446
446
 
447
447
  Pas de PKCE, pas d'ID token, pas d'`iss` (`usesPkce: false`, `issuerPolicy: null`,
448
- `github.ts:43-44`) : ici, la défense anti-CSRF repose **entièrement** sur le `state`. Le profil vient
449
- de l'API REST `/user` (`createGithubProvider()`, `github.ts:34`). Subtilité GitHub : l'e-mail
448
+ `github.ts:68-69`) : ici, la défense anti-CSRF repose **entièrement** sur le `state`. Le profil vient
449
+ de l'API REST `/user` (`createGithubProvider()`, `github.ts:53`). Subtilité GitHub : l'e-mail
450
450
  primaire est souvent privé — l'adaptateur bascule alors sur `/user/emails` et n'accepte
451
- `emailVerified` que si GitHub le certifie (`github.ts:68-77`).
451
+ `emailVerified` que si GitHub le certifie (`github.ts:98-107`).
452
452
 
453
453
  ### Enregistrer le sien — sans éditer le cœur
454
454
 
455
455
  **Tout serveur OpenID Connect conforme est déjà supporté** — Auth0, Okta, Authentik, Entra
456
456
  mono-locataire… — sans une ligne de code propre. Le builtin `oidc` suffit quand il n'y en a qu'un ;
457
- pour en nommer plusieurs, `registerOAuthProvider()` (`oauthProviderRegistry.ts:56`) au chargement de
457
+ pour en nommer plusieurs, `registerOAuthProvider()` (`oauthProviderRegistry.ts:68`) au chargement de
458
458
  ton module (avant le `onBoot` du service) :
459
459
 
460
460
  ```typescript ignore
@@ -475,39 +475,39 @@ callback issus de la config, rien d'autre. Elle peut être **asynchrone** — d
475
475
  une opération de construction, faite une fois par processus.
476
476
 
477
477
  Un fournisseur qui n'est **pas** OIDC (pas de métadonnées, pas d'ID token) demande un adaptateur : le
478
- protocole vient de `OAuth2Client`, la fabrique ne fait que lire le profil. C'est une quarantaine de
478
+ protocole vient de `OAuth2Client`, la fabrique ne fait que lire le profil. C'est une centaine de
479
479
  lignes — `github.ts` en est le modèle.
480
480
 
481
481
  Un fournisseur qui n'est pas OIDC (pas d'ID token, profil lu à son API) s'écrit comme GitHub
482
- (`createGithubProvider()`, `github.ts:40`) : `OAuth2Client` porte le protocole, la fabrique ne fait
482
+ (`createGithubProvider()`, `github.ts:53`) : `OAuth2Client` porte le protocole, la fabrique ne fait
483
483
  que le mapping du profil. Exemple sans réseau dans le dépôt :
484
484
  `src/modules/test/nodefony/secure/oauthTestProvider.ts`.
485
485
 
486
486
  ## ⚙️ Configuration
487
487
 
488
- Section `oauth2` du schéma Zod (`config.ts:1034`), branchée sur la config du module
489
- (`config.ts:1149`). Table dérivée du schéma — les défauts sont ceux du code.
488
+ Section `oauth2` du schéma Zod (`config.ts:1155`), branchée sur la config du module
489
+ (`config.ts:1155`). Table dérivée du schéma — les défauts sont ceux du code.
490
490
 
491
491
  | Option | Type | Défaut | Effet |
492
492
  | ----------------- | -------------------- | --------------- | ---------------------------------------------------------- |
493
493
  | `enabled` | booléen | `true` | Coupe le social login ; les routes ne montent pas. |
494
- | `defaultRoles` | liste de rôles | `["ROLE_USER"]` | Rôles du Shadow User **à la création** (`config.ts:1042`). |
495
- | `allowSignup` | booléen | `true` | `false` = compte préexistant lié exigé (`config.ts:1048`). |
494
+ | `defaultRoles` | liste de rôles | `["ROLE_USER"]` | Rôles du Shadow User **à la création** (`config.ts:1048`). |
495
+ | `allowSignup` | booléen | `true` | `false` = compte préexistant lié exigé (`config.ts:1054`). |
496
496
  | `successRedirect` | chemin | `/` | Où revient l'utilisateur après succès. |
497
497
  | `failureRedirect` | chemin | `/login` | Où il revient après échec (uniforme, sans détail). |
498
- | `providers` | dictionnaire par nom | `{}` | Fournisseurs activés (`config.ts:1064`). |
498
+ | `providers` | dictionnaire par nom | `{}` | Fournisseurs activés (`config.ts:1070`). |
499
499
 
500
- Par fournisseur (`oauthProviderSchema`, `config.ts:948`) :
500
+ Par fournisseur (`oauthProviderSchema`, `config.ts:954`) :
501
501
 
502
502
  <!-- prettier-ignore -->
503
503
  | Option | Requis | Effet |
504
504
  | --- | :---: | --- |
505
505
  | `clientId` / `clientSecret` | ✅ | Identifiants délivrés par l'IdP. Secrets : par `env.ts`, jamais journalisés. |
506
- | `redirectUri` | ✅ | URL de callback **exacte** (`config.ts:958`). |
506
+ | `redirectUri` | ✅ | URL de callback **exacte** (`config.ts:975`). |
507
507
  | `issuer` | OIDC self-hosted | Realm Keycloak ; ignoré par les IdP à endpoints fixes. |
508
508
  | `clientAuthMethod` | | Comment le client s'authentifie au point de jeton (RFC 6749 §2.3). Omis = `client_secret_basic`, ce que la RFC demande de préférer. Poser `client_secret_post` quand le serveur l'EXIGE — il le publie dans `token_endpoint_auth_methods_supported`. |
509
509
  | `scopes` | | Vide = scopes par défaut du fournisseur. |
510
- | `successRedirect` / `failureRedirect` / `defaultRoles` | | Surchargent le global **pour ce fournisseur** (`oauth2.ts:218-222`). |
510
+ | `successRedirect` / `failureRedirect` / `defaultRoles` | | Surchargent le global **pour ce fournisseur** (`oauth2.ts:279-283`). |
511
511
 
512
512
  Les surcharges par fournisseur permettent la cohabitation : un IdP de recette garde ses redirections
513
513
  et ses rôles pendant qu'un IdP de production pointe ailleurs.
@@ -550,12 +550,12 @@ ou détruire les sessions), pas chez le fournisseur.
550
550
  | -------------------------------------------------- | ------------------------------------------------------- | -------------------------------- |
551
551
  | Rejeu du retour (même `code`, même `state`) | `state` consommé + session régénérée à la promotion | `oauth2-attack.test.ts:89` (S5) |
552
552
  | `state` valide présenté au callback d'un autre IdP | Fournisseur attendu conservé en session et comparé | `oauth2-attack.test.ts:115` (S6) |
553
- | `iss` falsifié | Comparaison stricte à l'émetteur de la politique | `oauth2Service.test.ts:167` |
553
+ | `iss` falsifié | Comparaison stricte à l'émetteur de la politique | `oauth2Service.test.ts:170` |
554
554
  | Prise de compte par e-mail collidant un admin | Aucune liaison auto : compte séparé, admin intact | `oauth.attack.test.ts:71` (A1) |
555
555
  | Élévation de privilège par re-login | Rôles posés à la création, jamais réécrits | `oauth.attack.test.ts:123` (A2) |
556
556
  | Collision d'identifiants entre fournisseurs | Clé = `provider` + `providerId` | `oauth.attack.test.ts:155` (A3) |
557
- | Interception du `code` | PKCE : `code_verifier` exigé, refus si absent | `oauthProviders.test.ts:67` |
558
- | Création de compte non voulue | Provisioner absent (`provisionOAuthUser`) → fail-closed | `oauth2Service.test.ts:52` |
557
+ | Interception du `code` | PKCE : `code_verifier` exigé, refus si absent | `oauthProviders.test.ts:79` |
558
+ | Création de compte non voulue | Provisioner absent (`provisionOAuthUser`) → fail-closed | `oauth2Service.test.ts:266` |
559
559
 
560
560
  ## 📜 Normes appliquées
561
561
 
@@ -563,11 +563,11 @@ ou détruire les sessions), pas chez le fournisseur.
563
563
  | --------------------------------- | ------------------------ | --------------------------------------------------------------------- |
564
564
  | Flux Authorization Code | RFC 6749 | `IOAuthProvider.validateAuthorizationCode()` (`IOAuthProvider.ts:85`) |
565
565
  | PKCE | RFC 7636 | `usesPkce` (`IOAuthProvider.ts:58`) · `oidc.ts:104-111` |
566
- | Sécurité OAuth (BCP 2.1) | RFC 9700 | `OAuth2Service` (`oauth2.ts:116`) · `oauth2Schema` (`config.ts:1034`) |
567
- | Anti-mix-up (`iss`) | RFC 9207 | `issuerPolicy` (`IOAuthProvider.ts:61`) · `oauth2.ts:170-181` |
568
- | Callback en correspondance exacte | RFC 9700 §4 | `redirectUri` (`config.ts:958`) |
566
+ | Sécurité OAuth (BCP 2.1) | RFC 9700 | `OAuth2Service` (`oauth2.ts:116`) · `oauth2Schema` (`config.ts:1040`) |
567
+ | Anti-mix-up (`iss`) | RFC 9207 | `issuerPolicy` (`IOAuthProvider.ts:65`) · `oauth2.ts:265-273` |
568
+ | Callback en correspondance exacte | RFC 9700 §4 | `redirectUri` (`config.ts:975`) |
569
569
  | Claims d'identité OIDC | OpenID Connect Core | `fetchProfile()` du helper OIDC (`oidc.ts:127-145`) |
570
- | ID token consommé en code flow | OIDC Core §3.1.3.7 | `assertIdTokenClaims()` (`oidc.ts:132`) |
570
+ | ID token consommé en code flow | OIDC Core §3.1.3.7 | `assertIdTokenClaims()` (`oidc.ts:139`) |
571
571
  | Anti-fixation de session | OWASP Session Management | `session.regenerateId()` au login (`authFlow.ts:388`) |
572
572
 
573
573
  Flux **exclus** par posture 2.1, et donc absents du code : `implicit` (jeton en fragment d'URL) et
@@ -603,7 +603,7 @@ retirer de la configuration.
603
603
  technique brut.
604
604
 
605
605
  Côté suivi, chaque login réussi produit un événement d'audit `auth` / `login.success` via
606
- `AuthFlow.establishSessionFor()` (`authFlow.ts:229-236`), consultable dans l'écran **Audit**. La
606
+ `AuthFlow.establishSessionFor()` (`authFlow.ts:215-222`), consultable dans l'écran **Audit**. La
607
607
  session ouverte apparaît dans l'écran **Sessions** (IP et agent capturés à l'ouverture) ; le compte
608
608
  provisionné dans l'écran **Users**, avec ses rôles réels.
609
609
 
@@ -618,17 +618,17 @@ provisionné dans l'écran **Users**, avec ses rôles réels.
618
618
  | ------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------- |
619
619
  | `404` sur `…/oauth2/…` | Service `oauth2` absent (module non chargé / `enabled: false`) | Charger `@nodefony/security` et activer `oauth2` |
620
620
  | WARNING « inconnu du registre » au boot | Nom configuré sans fabrique (`oauth2.ts:149-155`) | `registerOAuthProvider()` au chargement du module, ou builtin |
621
- | `404` « Unknown provider » sur `authorize` | Le nom n'est pas dans `listProviders()` (`OAuth2Controller.ts:97`) | Vérifier le nom exact **et** la présence des secrets |
621
+ | `404` « Unknown provider » sur `authorize` | Le nom n'est pas dans `listProviders()` (`OAuth2Controller.ts:14`) | Vérifier le nom exact **et** la présence des secrets |
622
622
  | Bouton absent de l'écran de login | Secrets manquants → fournisseur non monté (spread conditionnel) | Renseigner `clientId`/`clientSecret` dans l'env |
623
- | `redirect_uri_mismatch` chez le fournisseur | `redirectUri` ≠ URL enregistrée, au caractère près (`config.ts:958`) | Aligner schéma, hôte, port et chemin `/…/{provider}/callback` |
623
+ | `redirect_uri_mismatch` chez le fournisseur | `redirectUri` ≠ URL enregistrée, au caractère près (`config.ts:975`) | Aligner schéma, hôte, port et chemin `/…/{provider}/callback` |
624
624
  | Retour systématique sur `failureRedirect` | `state`/`verifier` absents (cookie perdu entre les deux requêtes) | Vérifier `SameSite`/domaine du cookie ; un seul hôte en dev |
625
- | Callback échoue au **deuxième** essai | `state` à usage unique, consommé (`OAuth2Controller.ts:126-129`) | Refaire le flux depuis `authorize` — comportement attendu |
626
- | `OAuth issuer mismatch` | `iss` reçu ≠ l'émetteur attendu (`oauth2.ts:170-181`) | Corriger `issuer` (Keycloak : URL exacte du realm) |
627
- | Keycloak : erreur dès le premier login | `issuer` absent en config (`oauthProviderRegistry.ts:89-93`) | Renseigner l'URL du realm |
625
+ | Callback échoue au **deuxième** essai | `state` à usage unique, consommé (`OAuth2Controller.ts:163-165`) | Refaire le flux depuis `authorize` — comportement attendu |
626
+ | `OAuth issuer mismatch` | `iss` reçu ≠ l'émetteur attendu (`oauth2.ts:265-273`) | Corriger `issuer` (Keycloak : URL exacte du realm) |
627
+ | Keycloak : erreur dès le premier login | `issuer` absent en config (`oidc.ts:176-181`) | Renseigner l'URL du realm |
628
628
  | « provisioning indisponible » | `users` n'implémente pas la capability (`oauth2.ts:327-333`) | Implémenter `provisionOAuthUser()` sur le service `users` |
629
- | Profil connu refusé | `allowSignup: false` sans lien préexistant (`UserService.ts:363`) | Activer `allowSignup` ou lier le compte au préalable |
629
+ | Profil connu refusé | `allowSignup: false` sans lien préexistant (`UserService.ts:374`) | Activer `allowSignup` ou lier le compte au préalable |
630
630
  | Doublon de compte pour un utilisateur existant | Aucune liaison auto par e-mail (choix de sécurité) | Rattacher explicitement, utilisateur connecté |
631
- | Rôle attendu absent après re-login | Rôles posés à la **création** seulement (`UserService.ts:348`) | Modifier les rôles en base ; `defaultRoles` ne réécrit rien |
631
+ | Rôle attendu absent après re-login | Rôles posés à la **création** seulement (`UserService.ts:405`) | Modifier les rôles en base ; `defaultRoles` ne réécrit rien |
632
632
  | Jeton du fournisseur introuvable côté application | `IOAuthProfile` n'en porte aucun, par choix (`IOAuthUserProvisioner.ts:8-10`) | Le capturer dans son propre `fetchProfile()` et le stocker soi-même |
633
633
 
634
634
  ## 🧪 Tests & couverture
@@ -136,14 +136,14 @@ Puis `npm run build` — le runtime lit le `dist`, pas la source.
136
136
  | `-j, --json` | — | sortie machine, sans invite |
137
137
 
138
138
  Le plafond de `--ttl` n'est pas décoratif : un jeton posé dans un fichier **est une clé**, et une
139
- clé se remplace (`security-token.ts:52`, `security-token.ts:64-79`).
139
+ clé se remplace (`security-token.ts:67-84`).
140
140
 
141
141
  ### Où `--write` pose la valeur
142
142
 
143
143
  La variable est `NF_MCP_TOKEN` (`src/nodefony/src/cli/aiMcpReport.ts:32`). La commande ne sert que
144
144
  les agents dont la présence est **constatée** dans le projet — on ne crée pas la configuration d'un
145
145
  outil que personne n'utilise ici. La table des cibles vit au cœur
146
- (`src/nodefony/src/cli/agentTargets.ts:239`) : Claude, Gemini, Vibe, Codex.
146
+ (`src/nodefony/src/cli/agentTargets.ts:280`) : Claude, Gemini, Vibe, Codex.
147
147
 
148
148
  Deux garanties qui ne se négocient pas :
149
149