@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/command/security-user-add.js +2 -18
- package/dist/nodefony/command/security-user-password.js +2 -2
- package/dist/nodefony/config/config.js +2 -2
- package/dist/nodefony/service/auditService.js +3 -2
- package/dist/nodefony/service/tokenService.js +3 -2
- package/dist/nodefony/service/totp.js +5 -3
- package/dist/nodefony/service/webAuthn.js +3 -2
- package/dist/nodefony/service/webhooks.js +5 -3
- package/dist/nodefony/src/token/MemoryTokenStore.js +18 -5
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +15 -3
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +24 -6
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +18 -10
- package/dist/types/nodefony/command/security-user-add.d.ts +0 -16
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +3 -3
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +0 -9
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +0 -8
- package/docs/api-keys.md +62 -60
- package/docs/audit.md +77 -77
- package/docs/authenticators.md +55 -43
- package/docs/authorization.md +13 -13
- package/docs/cors.md +29 -29
- package/docs/csrf.md +26 -26
- package/docs/external-jwt.md +4 -4
- package/docs/firewall.md +29 -29
- package/docs/headers.md +26 -26
- package/docs/oauth2.md +60 -60
- package/docs/obtenir-un-jeton.md +2 -2
- package/docs/tokens.md +55 -55
- package/docs/totp.md +73 -72
- package/docs/webauthn.md +81 -81
- package/docs/webhooks.md +43 -37
- 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:
|
|
126
|
-
boot ; `onHttpRequest` (`http-kernel.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
279
|
-
(`http/nodefony/config/config.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
400
|
-
**éditables à chaud** (`runtimeMutable`) : `HttpKernel.onConfigChanged()` (`http-kernel.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
219
|
-
est présent (`framework/index.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
380
|
-
(`usesPkce: true`, `oidc.ts:
|
|
381
|
-
(`oauthProviderRegistry.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
449
|
-
de l'API REST `/user` (`createGithubProvider()`, `github.ts:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
489
|
-
(`config.ts:
|
|
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:
|
|
495
|
-
| `allowSignup` | booléen | `true` | `false` = compte préexistant lié exigé (`config.ts:
|
|
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:
|
|
498
|
+
| `providers` | dictionnaire par nom | `{}` | Fournisseurs activés (`config.ts:1070`). |
|
|
499
499
|
|
|
500
|
-
Par fournisseur (`oauthProviderSchema`, `config.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
558
|
-
| Création de compte non voulue | Provisioner absent (`provisionOAuthUser`) → fail-closed | `oauth2Service.test.ts:
|
|
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:
|
|
567
|
-
| Anti-mix-up (`iss`) | RFC 9207 | `issuerPolicy` (`IOAuthProvider.ts:
|
|
568
|
-
| Callback en correspondance exacte | RFC 9700 §4 | `redirectUri` (`config.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
626
|
-
| `OAuth issuer mismatch` | `iss` reçu ≠ l'émetteur attendu (`oauth2.ts:
|
|
627
|
-
| Keycloak : erreur dès le premier login | `issuer` absent en config (`
|
|
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:
|
|
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:
|
|
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
|
package/docs/obtenir-un-jeton.md
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|