@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4

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 (249) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +48 -16
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
  4. package/dist/nodefony/command/security-oauth-doctor.js +68 -0
  5. package/dist/nodefony/command/security-secrets.js +52 -29
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +10 -7
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +9 -14
  10. package/dist/nodefony/config/config.js +99 -8
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/contracts/index.js +1 -2
  14. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  15. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  16. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  17. package/dist/nodefony/errors/CsrfError.js +1 -2
  18. package/dist/nodefony/errors/InsecureTransportError.js +23 -0
  19. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  20. package/dist/nodefony/errors/SsrfError.js +1 -2
  21. package/dist/nodefony/errors/ThrottledError.js +1 -2
  22. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  23. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  24. package/dist/nodefony/errors/index.js +2 -1
  25. package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
  26. package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
  27. package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
  28. package/dist/nodefony/keycloak/scaffold.js +231 -0
  29. package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
  30. package/dist/nodefony/scaffold/contribute.js +19 -0
  31. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  32. package/dist/nodefony/service/apiKeys.js +45 -5
  33. package/dist/nodefony/service/auditService.js +4 -5
  34. package/dist/nodefony/service/authFlow.js +118 -16
  35. package/dist/nodefony/service/authorization.js +1 -2
  36. package/dist/nodefony/service/cors.js +1 -2
  37. package/dist/nodefony/service/csrf.js +1 -2
  38. package/dist/nodefony/service/firewall.js +116 -25
  39. package/dist/nodefony/service/keycloak.js +208 -0
  40. package/dist/nodefony/service/oauth2.js +403 -28
  41. package/dist/nodefony/service/securityHeaders.js +1 -2
  42. package/dist/nodefony/service/tokenService.js +94 -27
  43. package/dist/nodefony/service/totp.js +55 -11
  44. package/dist/nodefony/service/webAuthn.js +24 -7
  45. package/dist/nodefony/service/webhooks.js +4 -5
  46. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  47. package/dist/nodefony/src/SecuredArea.js +15 -5
  48. package/dist/nodefony/src/accountBinding.js +96 -0
  49. package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
  50. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  51. package/dist/nodefony/src/admin/adminAudit.js +1 -3
  52. package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
  53. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  54. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  55. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  56. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  57. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  58. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  59. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  60. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  61. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  62. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
  63. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  64. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
  65. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
  66. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  67. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
  68. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
  69. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  70. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  71. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  72. package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
  73. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  74. package/dist/nodefony/src/csp.js +1 -2
  75. package/dist/nodefony/src/csrfToken.js +1 -2
  76. package/dist/nodefony/src/entryPoint.js +38 -0
  77. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  78. package/dist/nodefony/src/net/transportFailure.js +57 -0
  79. package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
  80. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  81. package/dist/nodefony/src/oauth/metadata.js +28 -12
  82. package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
  83. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
  84. package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
  85. package/dist/nodefony/src/oauth/providerRoles.js +78 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +48 -12
  87. package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
  88. package/dist/nodefony/src/openToSignup.js +46 -0
  89. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  90. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  91. package/dist/nodefony/src/rolesWithin.js +57 -0
  92. package/dist/nodefony/src/sessionAuthentication.js +25 -0
  93. package/dist/nodefony/src/sessionIdentity.js +29 -3
  94. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  95. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  96. package/dist/nodefony/src/token/JwtKeystore.js +89 -48
  97. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  98. package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
  99. package/dist/nodefony/src/token/UserToken.js +1 -2
  100. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  101. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  102. package/dist/nodefony/src/token/secretFile.js +60 -4
  103. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  104. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  105. package/dist/nodefony/src/token/tokenSort.js +1 -2
  106. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  107. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  108. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  109. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  110. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  111. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  112. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  113. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
  114. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  115. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  116. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  117. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  118. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  119. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  120. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  124. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  125. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  126. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  127. package/dist/types/index.d.ts +26 -6
  128. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
  129. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  130. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  131. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  132. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  133. package/dist/types/nodefony/config/config.d.ts +64 -1
  134. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  135. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  136. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  137. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  138. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  139. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  140. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  141. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  142. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  143. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  144. package/dist/types/nodefony/errors/index.d.ts +1 -0
  145. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  146. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  147. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  148. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  149. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  150. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  151. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  152. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  153. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  154. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  155. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  156. package/dist/types/nodefony/service/oauth2.d.ts +145 -9
  157. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  158. package/dist/types/nodefony/service/totp.d.ts +28 -4
  159. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  160. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  161. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  162. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
  163. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
  164. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  165. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
  166. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  167. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  168. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  169. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  170. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  171. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  172. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  173. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  174. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  175. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  176. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  177. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  178. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  179. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  180. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  181. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  182. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  183. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  184. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  185. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  186. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  187. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  188. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  189. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  190. package/docs/api-keys.md +182 -175
  191. package/docs/audit.md +141 -141
  192. package/docs/authenticators.md +129 -96
  193. package/docs/authorization.md +116 -69
  194. package/docs/cors.md +64 -64
  195. package/docs/csrf.md +77 -72
  196. package/docs/external-jwt.md +83 -11
  197. package/docs/firewall.md +119 -83
  198. package/docs/headers.md +82 -82
  199. package/docs/index.md +16 -8
  200. package/docs/keycloak.md +835 -0
  201. package/docs/login-page.md +361 -0
  202. package/docs/oauth2.md +291 -166
  203. package/docs/obtenir-un-jeton.md +131 -34
  204. package/docs/tokens.md +197 -140
  205. package/docs/totp.md +177 -136
  206. package/docs/webauthn.md +157 -154
  207. package/docs/webhooks.md +128 -125
  208. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  209. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  210. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  211. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  212. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  213. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  214. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  215. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  216. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  217. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  218. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  219. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  220. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  221. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  222. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  223. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  224. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  225. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  226. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  227. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  228. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  229. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  230. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  231. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  232. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  233. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  234. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  235. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  236. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  237. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  238. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  239. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  240. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  241. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  242. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  243. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  244. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  245. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  246. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  247. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  248. package/package.json +19 -17
  249. package/dist/_virtual/_@oxc-project_runtime@0.152.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:1128`) 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
 
@@ -67,7 +67,7 @@ pose le 401 et son challenge — l'authenticator, lui, ne touche jamais à la r
67
67
  | EdDSA | Algorithme de signature asymétrique (Ed25519) — le seul accepté par le vérificateur JWT. |
68
68
  | CRC | Somme de contrôle embarquée dans une clé API — filtre les valeurs malformées avant la base. |
69
69
  | ALS | _AsyncLocalStorage_ : le contexte ambiant de la requête où le firewall pose `user` + `token`. |
70
- | Challenge | En-tête `WWW-Authenticate` renvoyé avec un 401 (RFC 7235) indiquant comment s'authentifier. |
70
+ | Challenge | En-tête `WWW-Authenticate` renvoyé avec un 401 (RFC 9110 §11) indiquant comment s'authentifier. |
71
71
  | Zero Trust | Sur une zone protégée, **aucune preuve valide ⇒ 401** ; l'anonymat n'est accepté que s'il est déclaré. |
72
72
 
73
73
  ## Qu'est-ce qu'un authenticator — et quelle faille il ferme
@@ -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 7235) 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:435`) 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:279`).
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:588`) : 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:902`) :
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:37-39`). 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:1200`) |
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,36 +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:850`).
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:68`).
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:780`).
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`).
252
- - Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts:130`).
251
+ attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts`).
252
+ - **En production, un mot de passe reçu en clair est refusé avant d'être décodé** :
253
+ `createToken()` interroge la politique `credentialTransport`
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`).
253
257
  - **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
254
258
  (`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
255
259
 
@@ -259,37 +263,37 @@ Après le login, chaque requête web prouve son identité par la **session serve
259
263
  Credential = l'**identifiant** posé dans le blob de session, jamais un secret.
260
264
 
261
265
  - **N'ouvre jamais la session lui-même** : `supports()` exige une session **déjà reprise** porteuse
262
- 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_
263
267
  le firewall ; c'est `AuthFlow.login()` qui ouvre et régénère l'ID (anti-fixation).
264
268
  - **L'identité est re-résolue à CHAQUE requête** via `resolveSessionIdentity`
265
- (`SessionAuthenticator.ts:91`) → rôles frais, révocation immédiate. Les contrôles d'état sont
266
- 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`).
267
271
  - `onSuccess()` pose l'identifiant sur le contexte — la persistance de session lie le blob au
268
- principal courant (`SessionAuthenticator.ts:110-116`).
272
+ principal courant (`SessionAuthenticator.ts#SessionAuthenticator.onSuccess`).
269
273
  - **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login,
270
- jamais une popup Basic (`SessionAuthenticator.ts:25-27`).
274
+ jamais une popup Basic (`SessionAuthenticator.ts#SessionAuthenticator`).
271
275
 
272
276
  ### `jwt` — Bearer signé pour les API (RFC 6750 + BCP RFC 8725)
273
277
 
274
278
  Réservé aux **API service↔service / agents** (le web reste sur la session). Vérifie un access token
275
279
  **EdDSA** signé par le keystore du serveur ; `supports()` ne réclame que la structure compacte
276
- `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
277
281
  prouvées en test :
278
282
 
279
- | Défense | Comment | Attaque fermée |
280
- | -------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
281
- | **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts:120`) | `alg=none`, algorithm confusion (§3.1) |
282
- | **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts:174`) | injection de clé / SSRF (§3.5) |
283
- | **`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) |
284
- | **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts:138-148`) | jeton auto-porté volé, logout global |
285
- | **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) |
286
290
 
287
- 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
288
292
  (expiré, `aud`, signature, sujet banni) part en **audit**, jamais au client — anti-oracle. Le token
289
- promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts:178-188`). `jose` est
290
- 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`).
291
295
 
292
- 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`) ;
293
297
  `firewall-realtime` ne se déclare pas — le firewall le câble lui-même au handshake WebSocket.
294
298
 
295
299
  ### `external-jwt` — un jeton émis par un serveur d'autorisation TIERS
@@ -306,17 +310,17 @@ Clé API personnelle en `Authorization: Bearer nf_…` (préfixe `apiKeys.prefix
306
310
  Contrairement au JWT (auto-porté), un PAT est un **bearer opaque** dont la vérité vit **côté
307
311
  serveur** (`ITokenStore`) → **révocable immédiatement**. Défenses :
308
312
 
309
- - **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`)
310
314
  → une valeur malformée n'atteint jamais la base (**anti-DoS**).
311
- - **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
312
316
  **nulle part au repos**.
313
- - **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts:113-116`) +
314
- **ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts:121-122`).
315
- - **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`).
316
320
  - **`lastUsedAt` throttlé** : aucune écriture sur le hot path tant que la fenêtre
317
- `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`).
318
322
 
319
- Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:146-149`).
323
+ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
320
324
 
321
325
  ### `firewall-realtime` — la promotion, en WebSocket, de l'identité déjà posée
322
326
 
@@ -325,27 +329,27 @@ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:1
325
329
  > a résolue — y compris un agent authentifié par jeton porteur, sans cookie ni session. Son nom
326
330
  > d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode branché, pas son rôle ; la
327
331
  > confusion a coûté un durcissement pensé pour la session appliqué à toutes les identités
328
- > (`FirewallRealtimeAuthenticator.ts:32-39`).
332
+ > (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
329
333
 
330
334
  Sur un handshake WS (une requête upgrade HTTP qui traverse **le même pipeline**), `startSession`
331
335
  puis `handleSecurity` ont **déjà** tourné : session chargée, identité re-résolue, `IUser` posé dans
332
336
  l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
333
- (`FirewallRealtimeAuthenticator.ts:80`).
337
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.supports`).
334
338
 
335
339
  - **Perf : il ne relit pas la base.** `authenticate()` réutilise l'identité de l'ALS
336
- (`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 —
337
341
  un coût évitable sur le différenciateur temps réel.
338
- - **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts:279`) pour toute zone
339
- protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts:289`).
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`).
340
344
  - **Le mode de preuve suit le jeton du firewall**, il n'est pas deviné : absent (zone historique),
341
- 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`).
342
346
  - **Filet Zero Trust** : il câble un revalidateur appelé avant chaque action data plane
343
- (`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
344
348
  ouverte (logout, changement de compte, `jti` denylisté). En mode session,
345
349
  `buildSessionRevalidator()` re-lit `storage.read(id)` et compare l'identifiant ; toute erreur de
346
- lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts:227`). En mode jeton porteur, la
350
+ lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts#SessionLike.storage`). En mode jeton porteur, la
347
351
  preuve est autre : `exp`, `jti` denylisté, `invalidBefore`
348
- (`FirewallRealtimeAuthenticator.ts:179`).
352
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.#buildBearerRevalidator`).
349
353
 
350
354
  > [!NOTE]
351
355
  > **Asymétrie de révocation HTTP↔WS (assumée)** : le jeton realtime est figé au handshake (les
@@ -356,7 +360,7 @@ l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
356
360
  ## ⚙️ Composer une zone — ordre, mode, cohabitation
357
361
 
358
362
  La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
359
- (`firewall.ts:1128`) 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`).
360
364
 
361
365
  ### Situation 1 — humains ET machines sur la même API (`first`)
362
366
 
@@ -365,12 +369,12 @@ Deux preuves différentes, mêmes routes — c'est la config du Démarrage rapid
365
369
  de lecture :
366
370
 
367
371
  - un maillon dont `supports()` est faux est simplement **sauté** en mode `first`
368
- (`firewall.ts:1114`) ;
372
+ (`firewall.ts#Firewall.#rebuildSecurityHeaders`) ;
369
373
  - un credential **présenté mais invalide échoue immédiatement** — l'échec d'`authenticate()`
370
- remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts:1128`). Une clé
374
+ remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts#Firewall.#authenticate`). Une clé
371
375
  API révoquée donne un 401 direct, même si un autre maillon aurait pu réussir.
372
376
  - aucune preuve présentée sur toute la chaîne → `handleSecurity()` lève l'`AuthenticationError`
373
- Zero Trust (`firewall.ts:754`).
377
+ Zero Trust (`firewall.ts#Firewall.handleSecurity`).
374
378
 
375
379
  ### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
376
380
 
@@ -389,15 +393,15 @@ authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le mon
389
393
  ### Situation 3 — empiler les preuves (`all`)
390
394
 
391
395
  En mode `all`, **chaque** maillon est obligatoire : `supports()` faux = 401 immédiat
392
- (`firewall.ts:1144-1149`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts:1198-1199`) —
396
+ (`firewall.ts#Firewall.applySecurityHeaders`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts#Firewall.unregisterCspOrigins`) —
393
397
  utile pour exiger une preuve de canal (mTLS) **et** une identité, ou un « sudo mode » session +
394
398
  re-saisie du mot de passe. Scénario complet côté zones : [firewall](./firewall.md).
395
399
 
396
400
  ### Cohabitation JWT + clé API dans une même zone
397
401
 
398
402
  Les deux sont des `Bearer`, mais Nodefony les **discrimine sur la forme** — un JWT a la structure
399
- compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:21`), un PAT porte le préfixe `nf_` sans
400
- 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_
401
405
  format → aucun conflit, aucune double vérification.
402
406
 
403
407
  ## Le fil rouge : le message d'échec uniforme
@@ -409,7 +413,35 @@ paresse : c'est une **défense anti-énumération / anti-oracle**.
409
413
  Distinguer « compte inconnu » de « mot de passe faux », ou « token expiré » de « signature
410
414
  invalide », donnerait à un attaquant une sonde. La cause fine part **toujours** en log d'audit ; le
411
415
  client n'obtient qu'un 401 + son challenge — posé par le firewall, premier maillon de la zone qui
412
- en déclare un (`Firewall.#setChallenge()`, `firewall.ts:1216`).
416
+ en déclare un (`Firewall.#setChallenge()`, `firewall.ts#Firewall.#setChallenge`).
417
+
418
+ ## Mot de passe en clair — refusé en production
419
+
420
+ Un mot de passe, ou un code de second facteur, qui arrive sur une requête HTTP en clair a déjà
421
+ traversé le réseau en lisible : le vérifier ne le protège plus. En production, toutes les
422
+ portes qui reçoivent un secret le refusent donc **avant toute vérification** — formulaire de
423
+ session, second facteur, Basic, émission de jeton (`/nodefony/security/api/token`), et les
424
+ endpoints d'administration marqués `credentials: true` (création d'un compte, changement de mot
425
+ de passe — cf la doc du plan d'administration de `@nodefony/framework`) :
426
+
427
+ - **403**, message constant `Credentials must be sent over HTTPS`
428
+ (`INSECURE_TRANSPORT_MESSAGE`, exporté par `nodefony`) ; la page de connexion le reconnaît et
429
+ n'affiche pas « mot de passe incorrect ».
430
+ - Le refus précède le throttle et le verifier : aucun hash calculé, aucun échec compté. Il est
431
+ audité (`reason: "insecure_transport"`).
432
+ - Le **premier** refus du process part en WARNING, les suivants non : un pod mal configuré
433
+ refuserait chaque connexion (`#warned`, `CredentialTransportPolicy.ts#CredentialTransportPolicy.#warned`).
434
+ - **La décision se prend sur le scheme EFFECTIF** (`context.scheme`), pas sur le transport du
435
+ pod. Derrière un proxy qui termine TLS — le cas de la plupart des déploiements — il suffit de
436
+ déclarer ce proxy de confiance : `trustProxy` dans la config de `@nodefony/http`, pour que
437
+ `X-Forwarded-Proto: https` soit lu.
438
+ - Une seule décision, posée au conteneur par le firewall au boot (`credentialTransport`,
439
+ `firewall.ts#Firewall.#provisionSharedServices`) : production ⇒ refus, sauf échappement **écrit**
440
+ `security.allowInsecureCredentials: true` (`CredentialTransportPolicy.ts#shouldEnforceCredentialTransport`). Hors
441
+ production, rien n'est refusé.
442
+
443
+ Pourquoi un 403 et pas un 401 : aucune preuve n'a été jugée. Le client ne corrigera rien dans
444
+ son secret — c'est le canal qu'il doit changer.
413
445
 
414
446
  ## 🧩 Ajouter un authenticator maison
415
447
 
@@ -424,38 +456,38 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
424
456
 
425
457
  À faire au chargement du module (avant le boot). Trois règles, calquées sur les builtins :
426
458
 
427
- - implémenter le contrat `IAuthenticator` (`IAuthenticator.ts:23`) — `challenge()` seulement si
459
+ - implémenter le contrat `IAuthenticator` (`IAuthenticator.ts#IAuthenticator`) — `challenge()` seulement si
428
460
  un en-tête `WWW-Authenticate` a du sens pour la stratégie ;
429
461
  - renvoyer le **message uniforme** en cas d'échec (la cause fine part en audit) ;
430
462
  - laisser les résolutions de services **lazy** dans l'instance — la fabrique ne fait que
431
- construire (`authenticatorRegistry.ts:29-31`).
463
+ construire (`authenticatorRegistry.ts#IAuthenticatorFactoryContext`).
432
464
 
433
465
  ## 📜 Normes appliquées
434
466
 
435
467
  <!-- prettier-ignore -->
436
468
  | Domaine | Norme | Ancrage |
437
469
  | --- | --- | --- |
438
- | Challenge d'auth (401) | RFC 7235 | `Firewall.#setChallenge()` (`firewall.ts:1216`) |
439
- | Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts:68`, cœur) — une porte UNIQUE au cœur, plus une constante par authenticator |
440
- | JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts:103-107`) |
441
- | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts:25-27`) |
442
- | Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts:101-103`) |
443
- | Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts:780`) |
444
- | 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`) |
445
477
 
446
478
  ## ⚡ Performance & mémoire
447
479
 
448
480
  - `supports()` est un test **bon marché** (en-tête + regex) — et n'est payé que sur zone protégée
449
481
  (le chemin chaud/froid vit dans le [firewall](./firewall.md)).
450
482
  - Résolutions **lazy** : le verifier `#resolveVerifier` est mémoïsé au premier login
451
- (`UserPasswordAuthenticator.ts:105`) ; keystore, `tokenStore` et `users` sont résolus du
452
- 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`).
453
485
  - `anonymous` : singleton gelé `anonymousUser`, zéro allocation d'utilisateur
454
- (`AnonymousToken.ts:9`).
486
+ (`AnonymousToken.ts#AnonymousToken`).
455
487
  - `apikey` : écriture `lastUsedAt` **coalescée** — pas une écriture par requête
456
- (`ApiKeyAuthenticator.ts:127-133`).
488
+ (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
457
489
  - `firewall-realtime` : **zéro lecture base** au handshake — réutilise l'ALS
458
- (`FirewallRealtimeAuthenticator.ts:91`).
490
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`).
459
491
  - Le throttle NIST **avant** le hash : un 429 ne coûte aucun argon2.
460
492
 
461
493
  ## ⚠️ Pièges (symptôme → cause → correction)
@@ -463,12 +495,13 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
463
495
  <!-- prettier-ignore -->
464
496
  | Symptôme | Cause (dans le code) | Correction |
465
497
  | --- | --- | --- |
466
- | 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts:850`) | Ajouter `anonymous` en dernier si l'anonymat est voulu |
467
- | 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 |
468
- | 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) |
469
- | 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` |
470
- | 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 |
471
- | Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts:613`) | Configurer `rateLimit` pour poser le throttler |
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é |
472
505
 
473
506
  ## 🧪 Tests & couverture
474
507