@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.5

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 (165) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +6 -8
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
  4. package/dist/nodefony/command/security-oauth-doctor.js +5 -3
  5. package/dist/nodefony/command/security-secrets.js +10 -4
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +5 -3
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +5 -3
  10. package/dist/nodefony/config/config.js +20 -4
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  14. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  15. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  16. package/dist/nodefony/errors/CsrfError.js +1 -2
  17. package/dist/nodefony/errors/InsecureTransportError.js +1 -2
  18. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  19. package/dist/nodefony/errors/SsrfError.js +1 -2
  20. package/dist/nodefony/errors/ThrottledError.js +1 -2
  21. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  22. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  23. package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
  24. package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
  25. package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
  26. package/dist/nodefony/keycloak/scaffold.js +3 -3
  27. package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
  28. package/dist/nodefony/scaffold/contribute.js +1 -2
  29. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  30. package/dist/nodefony/service/apiKeys.js +45 -5
  31. package/dist/nodefony/service/auditService.js +4 -5
  32. package/dist/nodefony/service/authFlow.js +10 -8
  33. package/dist/nodefony/service/authorization.js +1 -2
  34. package/dist/nodefony/service/cors.js +1 -2
  35. package/dist/nodefony/service/csrf.js +1 -2
  36. package/dist/nodefony/service/firewall.js +5 -4
  37. package/dist/nodefony/service/keycloak.js +1 -2
  38. package/dist/nodefony/service/oauth2.js +29 -7
  39. package/dist/nodefony/service/securityHeaders.js +1 -2
  40. package/dist/nodefony/service/tokenService.js +38 -22
  41. package/dist/nodefony/service/totp.js +4 -5
  42. package/dist/nodefony/service/webAuthn.js +4 -5
  43. package/dist/nodefony/service/webhooks.js +4 -5
  44. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  45. package/dist/nodefony/src/SecuredArea.js +1 -2
  46. package/dist/nodefony/src/accountBinding.js +96 -0
  47. package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
  48. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  49. package/dist/nodefony/src/admin/adminAudit.js +1 -2
  50. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  51. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  52. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  53. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  54. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  55. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  56. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  57. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  58. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  59. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  60. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
  61. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  62. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
  63. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
  64. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  65. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
  66. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
  67. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  68. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  69. package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
  70. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  71. package/dist/nodefony/src/csp.js +1 -2
  72. package/dist/nodefony/src/csrfToken.js +1 -2
  73. package/dist/nodefony/src/entryPoint.js +1 -2
  74. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  75. package/dist/nodefony/src/net/transportFailure.js +57 -0
  76. package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
  77. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  78. package/dist/nodefony/src/oauth/metadata.js +5 -16
  79. package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
  80. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
  81. package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
  82. package/dist/nodefony/src/oauth/providerRoles.js +1 -2
  83. package/dist/nodefony/src/oauth/providers/github.js +16 -6
  84. package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
  85. package/dist/nodefony/src/openToSignup.js +1 -2
  86. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  87. package/dist/nodefony/src/realtime/frameAuthorizer.js +2 -3
  88. package/dist/nodefony/src/rolesWithin.js +57 -0
  89. package/dist/nodefony/src/sessionAuthentication.js +1 -2
  90. package/dist/nodefony/src/sessionIdentity.js +29 -3
  91. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  92. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  93. package/dist/nodefony/src/token/JwtKeystore.js +14 -5
  94. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  95. package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
  96. package/dist/nodefony/src/token/UserToken.js +1 -2
  97. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  98. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  99. package/dist/nodefony/src/token/secretFile.js +1 -2
  100. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  101. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  102. package/dist/nodefony/src/token/tokenSort.js +1 -2
  103. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  106. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  107. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  108. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  110. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
  111. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  112. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  113. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  114. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  115. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  116. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  117. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  118. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  119. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  120. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  124. package/dist/nodefony/testing/index.js +108 -0
  125. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
  126. package/dist/types/nodefony/config/config.d.ts +1 -0
  127. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  128. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  129. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  130. package/dist/types/nodefony/service/oauth2.d.ts +20 -0
  131. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  132. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
  133. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
  134. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  135. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
  136. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  137. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  138. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  139. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  140. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
  141. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  142. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  143. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  144. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  145. package/dist/types/nodefony/testing/index.d.ts +51 -0
  146. package/docs/api-keys.md +181 -174
  147. package/docs/audit.md +140 -140
  148. package/docs/authenticators.md +101 -101
  149. package/docs/authorization.md +116 -69
  150. package/docs/cors.md +64 -64
  151. package/docs/csrf.md +74 -71
  152. package/docs/external-jwt.md +22 -12
  153. package/docs/firewall.md +85 -82
  154. package/docs/headers.md +81 -81
  155. package/docs/index.md +4 -4
  156. package/docs/keycloak.md +95 -78
  157. package/docs/login-page.md +23 -23
  158. package/docs/oauth2.md +157 -146
  159. package/docs/obtenir-un-jeton.md +116 -26
  160. package/docs/tokens.md +128 -122
  161. package/docs/totp.md +136 -136
  162. package/docs/webauthn.md +153 -153
  163. package/docs/webhooks.md +136 -124
  164. package/package.json +19 -13
  165. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -49,7 +49,7 @@ flowchart TD
49
49
  S --> CTRL["→ autorisation → contrôleur"]
50
50
  ```
51
51
 
52
- C'est `Firewall.#authenticate()` (`firewall.ts:1236`) qui déroule ce cycle pour chaque maillon de la
52
+ C'est `Firewall.#authenticate()` (`firewall.ts#Firewall.#authenticate`) qui déroule ce cycle pour chaque maillon de la
53
53
  zone, dans l'ordre déclaré. Le succès pose l'identité dans l'ALS ; l'échec remonte au firewall qui
54
54
  pose le 401 et son challenge — l'authenticator, lui, ne touche jamais à la réponse.
55
55
 
@@ -90,33 +90,33 @@ classe d'attaque précise — détaillées brique par brique dans le catalogue :
90
90
 
91
91
  ### Le contrat `IAuthenticator`
92
92
 
93
- Tout authenticator implémente le même cycle (`IAuthenticator.ts:23`), ce qui rend le firewall
93
+ Tout authenticator implémente le même cycle (`IAuthenticator.ts#IAuthenticator`), ce qui rend le firewall
94
94
  totalement agnostique de la stratégie :
95
95
 
96
- | Méthode | Rôle |
97
- | --------------------- | ---------------------------------------------------------------------------------------------------------- |
98
- | `supports(ctx)` | Test **bon marché** : le credential de cette stratégie est-il présent ? (sinon, maillon suivant) |
99
- | `createToken(ctx)` | Extrait le credential **brut, non vérifié**, dans un `UserToken`. |
100
- | `authenticate(token)` | **Vérifie** (signature/hash/session), applique la **révocation**, **re-résout le sujet** — ou lève un 401. |
101
- | `onSuccess(ctx,tok)` | Effet de bord au succès (poser l'identité en session, audit). |
102
- | `onFailure(ctx,err)` | Slot d'audit (le 401 + challenge sont posés par le firewall). |
103
- | `challenge()` | **Optionnel** (`IAuthenticator.ts:55`) — la valeur `WWW-Authenticate` (RFC 9110 §11) des 401 de la zone. |
96
+ | Méthode | Rôle |
97
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
98
+ | `supports(ctx)` | Test **bon marché** : le credential de cette stratégie est-il présent ? (sinon, maillon suivant) |
99
+ | `createToken(ctx)` | Extrait le credential **brut, non vérifié**, dans un `UserToken`. |
100
+ | `authenticate(token)` | **Vérifie** (signature/hash/session), applique la **révocation**, **re-résout le sujet** — ou lève un 401. |
101
+ | `onSuccess(ctx,tok)` | Effet de bord au succès (poser l'identité en session, audit). |
102
+ | `onFailure(ctx,err)` | Slot d'audit (le 401 + challenge sont posés par le firewall). |
103
+ | `challenge()` | **Optionnel** (`IAuthenticator.ts#IAuthenticator.challenge`) — la valeur `WWW-Authenticate` (RFC 9110 §11) des 401 de la zone. |
104
104
 
105
105
  ### Le registre pluggable
106
106
 
107
107
  Les authenticators sont résolus par **nom** : `Firewall.#instantiateAuthenticators()`
108
- (`firewall.ts:468`) interroge `getAuthenticatorFactory()` (`authenticatorRegistry.ts:59`) — jamais
108
+ (`firewall.ts#Firewall.#instantiateAuthenticators`) interroge `getAuthenticatorFactory()` (`authenticatorRegistry.ts#getAuthenticatorFactory`) — jamais
109
109
  un `if (name === "jwt")` dans le firewall, qui trahirait la promesse « pluggable ».
110
110
 
111
111
  - Les **six builtins HTTP** (`anonymous`, `userpassword`, `session`, `jwt`, `external-jwt`, `apikey`)
112
112
  s'enregistrent à l'import du module via `registerAuthenticatorFactory()`
113
- (`authenticatorRegistry.ts:73-145`) — donc toujours avant le boot.
113
+ (`authenticatorRegistry.ts#registerAuthenticatorFactory`) — donc toujours avant le boot.
114
114
  - Le septième, `firewall-realtime`, n'est **pas dans le registre** : c'est le firewall qui le câble
115
- lui-même au handshake WS des zones protégées (`Firewall.#wireRealtime()`, `firewall.ts:301`).
115
+ lui-même au handshake WS des zones protégées (`Firewall.#wireRealtime()`, `firewall.ts#Firewall.#wireRealtime`).
116
116
  - La fabrique ne fait que **construire** ; les résolutions de services coûteuses (`users`,
117
117
  `tokenStore`, keystore) restent **lazy** dans l'instance (cold path).
118
118
  - Un nom inconnu en config = boot **fail-closed** — `#configError` posé + log CRITIC
119
- (`firewall.ts:634`) : jamais de zone « protégée » silencieusement ouverte à cause d'une
119
+ (`firewall.ts#Firewall.#configError`) : jamais de zone « protégée » silencieusement ouverte à cause d'une
120
120
  faute de frappe.
121
121
 
122
122
  ## 🚀 Démarrage rapide
@@ -124,7 +124,7 @@ un `if (name === "jwt")` dans le firewall, qui trahirait la promesse « pluggabl
124
124
  ### Une zone, trois preuves — la même API pour le web et les machines
125
125
 
126
126
  Dans une app `nodefony create app`, on déclare quelles preuves une zone accepte — un **objet par
127
- nom**, validé Zod au boot (`areas: z.record(...)`, `config.ts:1358`) :
127
+ nom**, validé Zod au boot (`areas: z.record(...)`, `config.ts#oauth2Schema`) :
128
128
 
129
129
  ```typescript
130
130
  // nodefony.config.ts (extrait)
@@ -144,7 +144,7 @@ use("@nodefony/security", {
144
144
  > [!IMPORTANT]
145
145
  > **Le login est FOURNI** : `POST /nodefony/security/api/auth/login` (body `{ username, password }`
146
146
  > → `Set-Cookie` de session, ID régénéré anti-fixation), avec `logout` et `me`
147
- > (`SessionAuthController.ts:131-148`). Pas de LoginController à écrire — tes routes ne font que
147
+ > (`SessionAuthController.ts#SessionAuthController`). Pas de LoginController à écrire — tes routes ne font que
148
148
  > consommer l'identité.
149
149
 
150
150
  ### Ce que TU écris : le controller qui consomme l'identité
@@ -198,12 +198,12 @@ curl -s -H 'Authorization: Bearer nf_…' \
198
198
 
199
199
  Requête par requête, qui répond :
200
200
 
201
- | Le client envoie… | Maillon (`supports()`) | Résultat |
202
- | ------------------------------------ | ---------------------- | --------------------------------------------------------- |
203
- | le cookie de session | `session` | identifié — rôles frais re-résolus en base |
204
- | `Authorization: Bearer eyJ…` (a.b.c) | `jwt` | identifié — signature EdDSA + claims vérifiés |
205
- | `Authorization: Bearer nf_…` | `apikey` | identifié — clé vérifiée au store, révocable |
206
- | rien | aucun | **401** + `WWW-Authenticate: Bearer` (`firewall.ts:1267`) |
201
+ | Le client envoie… | Maillon (`supports()`) | Résultat |
202
+ | ------------------------------------ | ---------------------- | --------------------------------------------------------------------------- |
203
+ | le cookie de session | `session` | identifié — rôles frais re-résolus en base |
204
+ | `Authorization: Bearer eyJ…` (a.b.c) | `jwt` | identifié — signature EdDSA + claims vérifiés |
205
+ | `Authorization: Bearer nf_…` | `apikey` | identifié — clé vérifiée au store, révocable |
206
+ | rien | aucun | **401** + `WWW-Authenticate: Bearer` (`firewall.ts#Firewall.#authenticate`) |
207
207
 
208
208
  ## 🔐 Les authenticators intégrés
209
209
 
@@ -220,40 +220,40 @@ Requête par requête, qui répond :
220
220
  ### `anonymous` — accepter explicitement l'anonymat
221
221
 
222
222
  Le seul authenticator autorisé à produire un token **non authentifié** sans déclencher le Zero Trust
223
- (`AnonymousAuthenticator.ts:19`).
223
+ (`AnonymousAuthenticator.ts#AnonymousAuthenticator`).
224
224
 
225
- - `supports()` accepte tout (`AnonymousAuthenticator.ts:22`) ; le token porte le **singleton gelé**
226
- `anonymousUser` — zéro allocation d'utilisateur (`AnonymousToken.ts:9`).
225
+ - `supports()` accepte tout (`AnonymousAuthenticator.ts#AnonymousAuthenticator.supports`) ; le token porte le **singleton gelé**
226
+ `anonymousUser` — zéro allocation d'utilisateur (`AnonymousToken.ts#AnonymousToken`).
227
227
  - À ne lister **que volontairement** : `["jwt", "anonymous"]` en mode `first` signifie « identifié
228
228
  si preuve présente, sinon visiteur anonyme accepté ». En mode `all`, utile en **dernier** :
229
229
  « canal prouvé (ex. mTLS), identité utilisateur optionnelle ».
230
230
  - Sans lui, zone protégée + aucune preuve = 401 : la défense en profondeur du firewall n'accepte un
231
- token non authentifié que si `anonymous` est le maillon déclaré (`firewall.ts:929`).
231
+ token non authentifié que si `anonymous` est le maillon déclaré (`firewall.ts#Firewall.handleSecurity`).
232
232
  - **Faille fermée** : l'anonymat _implicite_ — ici il est un choix explicite et auditable, jamais un
233
233
  défaut.
234
234
 
235
235
  ### `userpassword` — HTTP Basic + backoff NIST
236
236
 
237
237
  Schéma **HTTP Basic** (RFC 7617) : `Authorization: Basic base64(id:mdp)`, charset UTF-8, scheme
238
- case-insensitive (`UserPasswordAuthenticator.ts:11`) ; `createToken()` split au **premier** `:` —
239
- le mot de passe peut en contenir (`UserPasswordAuthenticator.ts:81`).
238
+ case-insensitive (`UserPasswordAuthenticator.ts#BASIC_SCHEME`) ; `createToken()` split au **premier** `:` —
239
+ le mot de passe peut en contenir (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`).
240
240
 
241
241
  - **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
242
242
  leurre anti-timing, re-hash transparent — l'authenticator ne voit que le verdict.
243
243
  - **Message uniforme** `INVALID_CREDENTIALS` quelle que soit la cause — identifiant inconnu, compte
244
- verrouillé, mot de passe faux (`UserPasswordAuthenticator.ts:16`) → anti-énumération de comptes.
244
+ verrouillé, mot de passe faux (`UserPasswordAuthenticator.ts#INVALID_CREDENTIALS`) → anti-énumération de comptes.
245
245
  - **Throttling NIST SP 800-63B AVANT le verifier** : `#throttler.check()` sur l'identifiant saisi
246
- (`UserPasswordAuthenticator.ts:101-103`) — un identifiant bloqué ne coûte **aucun hash** → le
246
+ (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) — un identifiant bloqué ne coûte **aucun hash** → le
247
247
  throttle protège aussi le serveur du **DoS argon2**. Échec compté, succès remis à zéro
248
- (`UserPasswordAuthenticator.ts:111-114`). `ThrottledError` → **429 + `Retry-After`**
249
- (`firewall.ts:841`).
248
+ (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`). `ThrottledError` → **429 + `Retry-After`**
249
+ (`firewall.ts#Firewall.handleSecurity`).
250
250
  - **Le throttler est PARTAGÉ** avec le login JSON du BFF — même `loginThrottler` du container : un
251
- attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts:75-79`).
251
+ attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts`).
252
252
  - **En production, un mot de passe reçu en clair est refusé avant d'être décodé** :
253
253
  `createToken()` interroge la politique `credentialTransport`
254
- (`UserPasswordAuthenticator.ts:82`) → **403**, sans challenge — un `Basic` ferait renvoyer le
255
- mot de passe par le même canal (`firewall.ts:876`). Voir [Mot de passe en clair](#mot-de-passe-en-clair--refusé-en-production).
256
- - Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts:130`).
254
+ (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`) → **403**, sans challenge — un `Basic` ferait renvoyer le
255
+ mot de passe par le même canal (`firewall.ts#Firewall.handleSecurity`). Voir [Mot de passe en clair](#mot-de-passe-en-clair--refusé-en-production).
256
+ - Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`).
257
257
  - **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
258
258
  (`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
259
259
 
@@ -263,37 +263,37 @@ Après le login, chaque requête web prouve son identité par la **session serve
263
263
  Credential = l'**identifiant** posé dans le blob de session, jamais un secret.
264
264
 
265
265
  - **N'ouvre jamais la session lui-même** : `supports()` exige une session **déjà reprise** porteuse
266
- d'un utilisateur (`SessionAuthenticator.ts:75-78`) — le pipeline http démarre la session _avant_
266
+ d'un utilisateur (`SessionAuthenticator.ts#SessionAuthenticator.supports`) — le pipeline http démarre la session _avant_
267
267
  le firewall ; c'est `AuthFlow.login()` qui ouvre et régénère l'ID (anti-fixation).
268
268
  - **L'identité est re-résolue à CHAQUE requête** via `resolveSessionIdentity`
269
- (`SessionAuthenticator.ts:91`) → rôles frais, révocation immédiate. Les contrôles d'état sont
270
- partagés avec `AuthFlow.me()` : `isLocked()`/`isActive()` → rejet (`sessionIdentity.ts:40`).
269
+ (`SessionAuthenticator.ts#SessionAuthenticator.authenticate`) → rôles frais, révocation immédiate. Les contrôles d'état sont
270
+ partagés avec `AuthFlow.me()` : `isLocked()`/`isActive()` → rejet (`sessionIdentity.ts#resolveSessionIdentity`).
271
271
  - `onSuccess()` pose l'identifiant sur le contexte — la persistance de session lie le blob au
272
- principal courant (`SessionAuthenticator.ts:110-116`).
272
+ principal courant (`SessionAuthenticator.ts#SessionAuthenticator.onSuccess`).
273
273
  - **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login,
274
- jamais une popup Basic (`SessionAuthenticator.ts:25-27`).
274
+ jamais une popup Basic (`SessionAuthenticator.ts#SessionAuthenticator`).
275
275
 
276
276
  ### `jwt` — Bearer signé pour les API (RFC 6750 + BCP RFC 8725)
277
277
 
278
278
  Réservé aux **API service↔service / agents** (le web reste sur la session). Vérifie un access token
279
279
  **EdDSA** signé par le keystore du serveur ; `supports()` ne réclame que la structure compacte
280
- `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:21`). Les défenses **dures** du JWT BCP, toutes
280
+ `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts#COMPACT_JWS`). Les défenses **dures** du JWT BCP, toutes
281
281
  prouvées en test :
282
282
 
283
- | Défense | Comment | Attaque fermée |
284
- | -------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
285
- | **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts:120`) | `alg=none`, algorithm confusion (§3.1) |
286
- | **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts:174`) | injection de clé / SSRF (§3.5) |
287
- | **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts:119-124`) | refresh présenté comme access, token d'un autre service (§3.8-3.9) |
288
- | **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts:138-148`) | jeton auto-porté volé, logout global |
289
- | **Sujet revérifié** | `loadUserByIdentifier(sub)` → disparu/inactif/verrouillé = rejet (`JwtAuthenticator.ts:190-203`) | compte banni encore « valide » via son token (§3.10) |
283
+ | Défense | Comment | Attaque fermée |
284
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
285
+ | **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | `alg=none`, algorithm confusion (§3.1) |
286
+ | **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts#JwtAuthenticator.#ensureGetKey`) | injection de clé / SSRF (§3.5) |
287
+ | **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts#JwtAuthenticator.verifyForResource`) | refresh présenté comme access, token d'un autre service (§3.8-3.9) |
288
+ | **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | jeton auto-porté volé, logout global |
289
+ | **Sujet revérifié** | `loadUserByIdentifier(sub)` → disparu/inactif/verrouillé = rejet (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | compte banni encore « valide » via son token (§3.10) |
290
290
 
291
- Le **message d'échec est uniforme** (`INVALID_TOKEN`, `JwtAuthenticator.ts:25`) : la cause fine
291
+ Le **message d'échec est uniforme** (`INVALID_TOKEN`, `JwtAuthenticator.ts#INVALID_TOKEN`) : la cause fine
292
292
  (expiré, `aud`, signature, sujet banni) part en **audit**, jamais au client — anti-oracle. Le token
293
- promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts:178-188`). `jose` est
294
- importé **lazy** — dépendance lourde (`JwtAuthenticator.ts:112`).
293
+ promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts#JwtAuthenticator.#verify`). `jose` est
294
+ importé **lazy** — dépendance lourde (`JwtAuthenticator.ts#JwtAuthenticator.#jose`).
295
295
 
296
- Les six premiers se déclarent dans une zone (registre `authenticatorRegistry.ts:73`) ;
296
+ Les six premiers se déclarent dans une zone (registre `authenticatorRegistry.ts`) ;
297
297
  `firewall-realtime` ne se déclare pas — le firewall le câble lui-même au handshake WebSocket.
298
298
 
299
299
  ### `external-jwt` — un jeton émis par un serveur d'autorisation TIERS
@@ -310,17 +310,17 @@ Clé API personnelle en `Authorization: Bearer nf_…` (préfixe `apiKeys.prefix
310
310
  Contrairement au JWT (auto-porté), un PAT est un **bearer opaque** dont la vérité vit **côté
311
311
  serveur** (`ITokenStore`) → **révocable immédiatement**. Défenses :
312
312
 
313
- - **Forme + CRC validés AVANT tout accès au store** (`parseApiKey`, `ApiKeyAuthenticator.ts:103-106`)
313
+ - **Forme + CRC validés AVANT tout accès au store** (`parseApiKey`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`)
314
314
  → une valeur malformée n'atteint jamais la base (**anti-DoS**).
315
- - **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts:109`) — le secret n'existe
315
+ - **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) — le secret n'existe
316
316
  **nulle part au repos**.
317
- - **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts:113-116`) +
318
- **ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts:121-122`).
319
- - **Sujet revérifié** à chaque requête → rôles frais (`ApiKeyAuthenticator.ts:127`).
317
+ - **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) +
318
+ **ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
319
+ - **Sujet revérifié** à chaque requête → rôles frais (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
320
320
  - **`lastUsedAt` throttlé** : aucune écriture sur le hot path tant que la fenêtre
321
- `apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts:136-143`).
321
+ `apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts#IApiKeyAuthenticatorRuntime.lastUsedThrottleS`).
322
322
 
323
- Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:146-149`).
323
+ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
324
324
 
325
325
  ### `firewall-realtime` — la promotion, en WebSocket, de l'identité déjà posée
326
326
 
@@ -329,27 +329,27 @@ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:1
329
329
  > a résolue — y compris un agent authentifié par jeton porteur, sans cookie ni session. Son nom
330
330
  > d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode branché, pas son rôle ; la
331
331
  > confusion a coûté un durcissement pensé pour la session appliqué à toutes les identités
332
- > (`FirewallRealtimeAuthenticator.ts:32-39`).
332
+ > (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
333
333
 
334
334
  Sur un handshake WS (une requête upgrade HTTP qui traverse **le même pipeline**), `startSession`
335
335
  puis `handleSecurity` ont **déjà** tourné : session chargée, identité re-résolue, `IUser` posé dans
336
336
  l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
337
- (`FirewallRealtimeAuthenticator.ts:80`).
337
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.supports`).
338
338
 
339
339
  - **Perf : il ne relit pas la base.** `authenticate()` réutilise l'identité de l'ALS
340
- (`FirewallRealtimeAuthenticator.ts:91`) au lieu de refaire deux lectures base par connexion —
340
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`) au lieu de refaire deux lectures base par connexion —
341
341
  un coût évitable sur le différenciateur temps réel.
342
- - **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts:301`) pour toute zone
343
- protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts:290`).
342
+ - **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) pour toute zone
343
+ protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts#Firewall.#build`).
344
344
  - **Le mode de preuve suit le jeton du firewall**, il n'est pas deviné : absent (zone historique),
345
- on retombe sur le mode le plus strict, la session (`FirewallRealtimeAuthenticator.ts:101-103`).
345
+ on retombe sur le mode le plus strict, la session (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`).
346
346
  - **Filet Zero Trust** : il câble un revalidateur appelé avant chaque action data plane
347
- (`FirewallRealtimeAuthenticator.ts:105-107`) — la socket peut survivre à l'identité qui l'a
347
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`) — la socket peut survivre à l'identité qui l'a
348
348
  ouverte (logout, changement de compte, `jti` denylisté). En mode session,
349
349
  `buildSessionRevalidator()` re-lit `storage.read(id)` et compare l'identifiant ; toute erreur de
350
- lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts:260`). En mode jeton porteur, la
350
+ lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts#SessionLike.storage`). En mode jeton porteur, la
351
351
  preuve est autre : `exp`, `jti` denylisté, `invalidBefore`
352
- (`FirewallRealtimeAuthenticator.ts:204`).
352
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.#buildBearerRevalidator`).
353
353
 
354
354
  > [!NOTE]
355
355
  > **Asymétrie de révocation HTTP↔WS (assumée)** : le jeton realtime est figé au handshake (les
@@ -360,7 +360,7 @@ l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
360
360
  ## ⚙️ Composer une zone — ordre, mode, cohabitation
361
361
 
362
362
  La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
363
- (`firewall.ts:1236`) selon le `mode` de la zone (`first` par défaut, `config.ts:87-92`).
363
+ (`firewall.ts#Firewall.#authenticate`) selon le `mode` de la zone (`first` par défaut, `config.ts#areaSchema`).
364
364
 
365
365
  ### Situation 1 — humains ET machines sur la même API (`first`)
366
366
 
@@ -369,12 +369,12 @@ Deux preuves différentes, mêmes routes — c'est la config du Démarrage rapid
369
369
  de lecture :
370
370
 
371
371
  - un maillon dont `supports()` est faux est simplement **sauté** en mode `first`
372
- (`firewall.ts:1211`) ;
372
+ (`firewall.ts#Firewall.#rebuildSecurityHeaders`) ;
373
373
  - un credential **présenté mais invalide échoue immédiatement** — l'échec d'`authenticate()`
374
- remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts:1236`). Une clé
374
+ remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts#Firewall.#authenticate`). Une clé
375
375
  API révoquée donne un 401 direct, même si un autre maillon aurait pu réussir.
376
376
  - aucune preuve présentée sur toute la chaîne → `handleSecurity()` lève l'`AuthenticationError`
377
- Zero Trust (`firewall.ts:825`).
377
+ Zero Trust (`firewall.ts#Firewall.handleSecurity`).
378
378
 
379
379
  ### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
380
380
 
@@ -393,15 +393,15 @@ authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le mon
393
393
  ### Situation 3 — empiler les preuves (`all`)
394
394
 
395
395
  En mode `all`, **chaque** maillon est obligatoire : `supports()` faux = 401 immédiat
396
- (`firewall.ts:1150-1155`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts:1204-1205`) —
396
+ (`firewall.ts#Firewall.applySecurityHeaders`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts#Firewall.unregisterCspOrigins`) —
397
397
  utile pour exiger une preuve de canal (mTLS) **et** une identité, ou un « sudo mode » session +
398
398
  re-saisie du mot de passe. Scénario complet côté zones : [firewall](./firewall.md).
399
399
 
400
400
  ### Cohabitation JWT + clé API dans une même zone
401
401
 
402
402
  Les deux sont des `Bearer`, mais Nodefony les **discrimine sur la forme** — un JWT a la structure
403
- compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:21`), un PAT porte le préfixe `nf_` sans
404
- point (`looksLikeApiKey`, `ApiKeyAuthenticator.ts:72`). Chaque `supports()` ne réclame que _son_
403
+ compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts#COMPACT_JWS`), un PAT porte le préfixe `nf_` sans
404
+ point (`looksLikeApiKey`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.constructor`). Chaque `supports()` ne réclame que _son_
405
405
  format → aucun conflit, aucune double vérification.
406
406
 
407
407
  ## Le fil rouge : le message d'échec uniforme
@@ -413,7 +413,7 @@ paresse : c'est une **défense anti-énumération / anti-oracle**.
413
413
  Distinguer « compte inconnu » de « mot de passe faux », ou « token expiré » de « signature
414
414
  invalide », donnerait à un attaquant une sonde. La cause fine part **toujours** en log d'audit ; le
415
415
  client n'obtient qu'un 401 + son challenge — posé par le firewall, premier maillon de la zone qui
416
- en déclare un (`Firewall.#setChallenge()`, `firewall.ts:1345`).
416
+ en déclare un (`Firewall.#setChallenge()`, `firewall.ts#Firewall.#setChallenge`).
417
417
 
418
418
  ## Mot de passe en clair — refusé en production
419
419
 
@@ -430,14 +430,14 @@ de passe — cf la doc du plan d'administration de `@nodefony/framework`) :
430
430
  - Le refus précède le throttle et le verifier : aucun hash calculé, aucun échec compté. Il est
431
431
  audité (`reason: "insecure_transport"`).
432
432
  - Le **premier** refus du process part en WARNING, les suivants non : un pod mal configuré
433
- refuserait chaque connexion (`#warned`, `CredentialTransportPolicy.ts:69`).
433
+ refuserait chaque connexion (`#warned`, `CredentialTransportPolicy.ts#CredentialTransportPolicy.#warned`).
434
434
  - **La décision se prend sur le scheme EFFECTIF** (`context.scheme`), pas sur le transport du
435
435
  pod. Derrière un proxy qui termine TLS — le cas de la plupart des déploiements — il suffit de
436
436
  déclarer ce proxy de confiance : `trustProxy` dans la config de `@nodefony/http`, pour que
437
437
  `X-Forwarded-Proto: https` soit lu.
438
438
  - Une seule décision, posée au conteneur par le firewall au boot (`credentialTransport`,
439
- `firewall.ts:438`) : production ⇒ refus, sauf échappement **écrit**
440
- `security.allowInsecureCredentials: true` (`CredentialTransportPolicy.ts:31`). Hors
439
+ `firewall.ts#Firewall.#provisionSharedServices`) : production ⇒ refus, sauf échappement **écrit**
440
+ `security.allowInsecureCredentials: true` (`CredentialTransportPolicy.ts#shouldEnforceCredentialTransport`). Hors
441
441
  production, rien n'est refusé.
442
442
 
443
443
  Pourquoi un 403 et pas un 401 : aucune preuve n'a été jugée. Le client ne corrigera rien dans
@@ -456,38 +456,38 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
456
456
 
457
457
  À faire au chargement du module (avant le boot). Trois règles, calquées sur les builtins :
458
458
 
459
- - implémenter le contrat `IAuthenticator` (`IAuthenticator.ts:23`) — `challenge()` seulement si
459
+ - implémenter le contrat `IAuthenticator` (`IAuthenticator.ts#IAuthenticator`) — `challenge()` seulement si
460
460
  un en-tête `WWW-Authenticate` a du sens pour la stratégie ;
461
461
  - renvoyer le **message uniforme** en cas d'échec (la cause fine part en audit) ;
462
462
  - laisser les résolutions de services **lazy** dans l'instance — la fabrique ne fait que
463
- construire (`authenticatorRegistry.ts:29-31`).
463
+ construire (`authenticatorRegistry.ts#IAuthenticatorFactoryContext`).
464
464
 
465
465
  ## 📜 Normes appliquées
466
466
 
467
467
  <!-- prettier-ignore -->
468
468
  | Domaine | Norme | Ancrage |
469
469
  | --- | --- | --- |
470
- | Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts:1345`) |
471
- | Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts:68`, cœur) — une porte UNIQUE au cœur, plus une constante par authenticator |
472
- | JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts:103-107`) |
473
- | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts:25-27`) |
474
- | Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts:101-103`) |
475
- | Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts:841`) |
476
- | Anti-énumération | OWASP | `INVALID_TOKEN` (`JwtAuthenticator.ts:25`) · `INVALID_CREDENTIALS` (`UserPasswordAuthenticator.ts:16`) |
470
+ | Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts#Firewall.#setChallenge`) |
471
+ | Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts#readBearerHeader`, cœur) — une porte UNIQUE au cœur, plus une constante par authenticator |
472
+ | JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) |
473
+ | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator`) |
474
+ | Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) |
475
+ | Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts#Firewall.handleSecurity`) |
476
+ | Anti-énumération | OWASP | `INVALID_TOKEN` (`JwtAuthenticator.ts#INVALID_TOKEN`) · `INVALID_CREDENTIALS` (`UserPasswordAuthenticator.ts#INVALID_CREDENTIALS`) |
477
477
 
478
478
  ## ⚡ Performance & mémoire
479
479
 
480
480
  - `supports()` est un test **bon marché** (en-tête + regex) — et n'est payé que sur zone protégée
481
481
  (le chemin chaud/froid vit dans le [firewall](./firewall.md)).
482
482
  - Résolutions **lazy** : le verifier `#resolveVerifier` est mémoïsé au premier login
483
- (`UserPasswordAuthenticator.ts:59`) ; keystore, `tokenStore` et `users` sont résolus du
484
- container au premier usage ; `jose` est importé lazy (`JwtAuthenticator.ts:112`).
483
+ (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.#resolveVerifier`) ; keystore, `tokenStore` et `users` sont résolus du
484
+ container au premier usage ; `jose` est importé lazy (`JwtAuthenticator.ts#JwtAuthenticator.#jose`).
485
485
  - `anonymous` : singleton gelé `anonymousUser`, zéro allocation d'utilisateur
486
- (`AnonymousToken.ts:9`).
486
+ (`AnonymousToken.ts#AnonymousToken`).
487
487
  - `apikey` : écriture `lastUsedAt` **coalescée** — pas une écriture par requête
488
- (`ApiKeyAuthenticator.ts:127-133`).
488
+ (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
489
489
  - `firewall-realtime` : **zéro lecture base** au handshake — réutilise l'ALS
490
- (`FirewallRealtimeAuthenticator.ts:91`).
490
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`).
491
491
  - Le throttle NIST **avant** le hash : un 429 ne coûte aucun argon2.
492
492
 
493
493
  ## ⚠️ Pièges (symptôme → cause → correction)
@@ -495,13 +495,13 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
495
495
  <!-- prettier-ignore -->
496
496
  | Symptôme | Cause (dans le code) | Correction |
497
497
  | --- | --- | --- |
498
- | 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts:929`) | Ajouter `anonymous` en dernier si l'anonymat est voulu |
499
- | 401 générique + log ERROR « service `users` » | Câblage : pas de `UserService` au container (`authenticatorRegistry.ts:85-88`) | Enregistrer un `UserService` au boot de l'app |
500
- | JWT rejeté alors qu'il « semble » valide | `aud`/`iss`/`typ` non conformes, ou `alg` ≠ EdDSA (`JwtAuthenticator.ts:103-107`) | Émettre via le `TokenService` (mêmes iss/aud/typ) |
501
- | Clé API révoquée encore acceptée quelques secondes | Confusion avec un JWT (auto-porté) — `revokedAt` est lu à chaque requête (`ApiKeyAuthenticator.ts:110`) | Un PAT est révoqué immédiatement ; vérifier `revokedAt` |
502
- | Révocation WS pas immédiate | Identité figée au handshake — asymétrie assumée du `FirewallRealtimeAuthenticator` (`FirewallRealtimeAuthenticator.ts:51-55`) | Attendre la fenêtre de re-validation, ou utiliser JWT + canal révocation |
503
- | Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts:646`) | Configurer `rateLimit` pour poser le throttler |
504
- | 403 `Credentials must be sent over HTTPS` sur TOUTES les connexions en production | Pod en HTTP derrière un proxy TLS non déclaré de confiance : `context.scheme` reste `http` (`CredentialTransportPolicy.ts:31`) | Déclarer le proxy dans `trustProxy` (`@nodefony/http`) ; `allowInsecureCredentials` seulement si le clair est un choix assumé |
498
+ | 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts#Firewall.handleSecurity`) | Ajouter `anonymous` en dernier si l'anonymat est voulu |
499
+ | 401 générique + log ERROR « service `users` » | Câblage : pas de `UserService` au container (`authenticatorRegistry.ts`) | Enregistrer un `UserService` au boot de l'app |
500
+ | JWT rejeté alors qu'il « semble » valide | `aud`/`iss`/`typ` non conformes, ou `alg` ≠ EdDSA (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | Émettre via le `TokenService` (mêmes iss/aud/typ) |
501
+ | Clé API révoquée encore acceptée quelques secondes | Confusion avec un JWT (auto-porté) — `revokedAt` est lu à chaque requête (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) | Un PAT est révoqué immédiatement ; vérifier `revokedAt` |
502
+ | Révocation WS pas immédiate | Identité figée au handshake — asymétrie assumée du `FirewallRealtimeAuthenticator` (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`) | Attendre la fenêtre de re-validation, ou utiliser JWT + canal révocation |
503
+ | Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts#Firewall.#describeDefenses`) | Configurer `rateLimit` pour poser le throttler |
504
+ | 403 `Credentials must be sent over HTTPS` sur TOUTES les connexions en production | Pod en HTTP derrière un proxy TLS non déclaré de confiance : `context.scheme` reste `http` (`CredentialTransportPolicy.ts#shouldEnforceCredentialTransport`) | Déclarer le proxy dans `trustProxy` (`@nodefony/http`) ; `allowInsecureCredentials` seulement si le clair est un choix assumé |
505
505
 
506
506
  ## 🧪 Tests & couverture
507
507