@nodefony/security 10.0.0-alpha.1

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 (258) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +182 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +151 -0
  6. package/dist/nodefony/command/security-secrets.js +158 -0
  7. package/dist/nodefony/command/security-token.js +335 -0
  8. package/dist/nodefony/command/security-user-add.js +131 -0
  9. package/dist/nodefony/command/security-user-delete.js +102 -0
  10. package/dist/nodefony/command/security-user-list.js +77 -0
  11. package/dist/nodefony/config/config.js +366 -0
  12. package/dist/nodefony/config/defineModuleConfig.js +35 -0
  13. package/dist/nodefony/contracts/IAccessVoter.js +13 -0
  14. package/dist/nodefony/contracts/IApiKey.js +1 -0
  15. package/dist/nodefony/contracts/IAuditEvent.js +1 -0
  16. package/dist/nodefony/contracts/IAuditStore.js +1 -0
  17. package/dist/nodefony/contracts/IAuthenticator.js +1 -0
  18. package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
  19. package/dist/nodefony/contracts/IFirewall.js +1 -0
  20. package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
  21. package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
  22. package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
  23. package/dist/nodefony/contracts/ISecuredArea.js +1 -0
  24. package/dist/nodefony/contracts/IToken.js +1 -0
  25. package/dist/nodefony/contracts/ITokenStore.js +1 -0
  26. package/dist/nodefony/contracts/ITotpSecret.js +1 -0
  27. package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
  28. package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
  29. package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
  30. package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
  31. package/dist/nodefony/contracts/IWebhookStore.js +1 -0
  32. package/dist/nodefony/contracts/index.js +2 -0
  33. package/dist/nodefony/errors/AccessDeniedError.js +14 -0
  34. package/dist/nodefony/errors/ApiKeyError.js +21 -0
  35. package/dist/nodefony/errors/AuthenticationError.js +14 -0
  36. package/dist/nodefony/errors/CsrfError.js +23 -0
  37. package/dist/nodefony/errors/InvalidTargetError.js +39 -0
  38. package/dist/nodefony/errors/SsrfError.js +17 -0
  39. package/dist/nodefony/errors/ThrottledError.js +21 -0
  40. package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
  41. package/dist/nodefony/errors/WebAuthnError.js +21 -0
  42. package/dist/nodefony/errors/index.js +9 -0
  43. package/dist/nodefony/service/accessTokenVerifier.js +77 -0
  44. package/dist/nodefony/service/apiKeys.js +310 -0
  45. package/dist/nodefony/service/auditService.js +145 -0
  46. package/dist/nodefony/service/authFlow.js +332 -0
  47. package/dist/nodefony/service/authorization.js +95 -0
  48. package/dist/nodefony/service/cors.js +81 -0
  49. package/dist/nodefony/service/csrf.js +97 -0
  50. package/dist/nodefony/service/firewall.js +699 -0
  51. package/dist/nodefony/service/oauth2.js +153 -0
  52. package/dist/nodefony/service/securityHeaders.js +80 -0
  53. package/dist/nodefony/service/tokenService.js +486 -0
  54. package/dist/nodefony/service/totp.js +209 -0
  55. package/dist/nodefony/service/webAuthn.js +343 -0
  56. package/dist/nodefony/service/webhooks.js +539 -0
  57. package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
  58. package/dist/nodefony/src/SecuredArea.js +51 -0
  59. package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
  60. package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
  61. package/dist/nodefony/src/admin/adminAudit.js +37 -0
  62. package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
  63. package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
  64. package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
  65. package/dist/nodefony/src/audit/auditBridge.js +82 -0
  66. package/dist/nodefony/src/audit/auditFilters.js +60 -0
  67. package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
  68. package/dist/nodefony/src/audit/readAuditContext.js +24 -0
  69. package/dist/nodefony/src/audit/recordAudit.js +16 -0
  70. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
  71. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
  72. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
  73. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
  74. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
  75. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
  76. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
  77. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
  78. package/dist/nodefony/src/authenticator/bearer.js +2 -0
  79. package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
  80. package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
  81. package/dist/nodefony/src/crypto/secretCipher.js +79 -0
  82. package/dist/nodefony/src/csp.js +54 -0
  83. package/dist/nodefony/src/csrfToken.js +65 -0
  84. package/dist/nodefony/src/net/ssrfGuard.js +130 -0
  85. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +65 -0
  87. package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
  88. package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
  89. package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
  90. package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
  91. package/dist/nodefony/src/sessionIdentity.js +35 -0
  92. package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
  93. package/dist/nodefony/src/token/AnonymousToken.js +40 -0
  94. package/dist/nodefony/src/token/JwtKeystore.js +160 -0
  95. package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
  96. package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
  97. package/dist/nodefony/src/token/UserToken.js +67 -0
  98. package/dist/nodefony/src/token/jwtRuntime.js +19 -0
  99. package/dist/nodefony/src/token/secretFile.js +134 -0
  100. package/dist/nodefony/src/token/tokenCriteria.js +35 -0
  101. package/dist/nodefony/src/token/tokenFilters.js +72 -0
  102. package/dist/nodefony/src/token/tokenSort.js +40 -0
  103. package/dist/nodefony/src/token/tokenStatus.js +35 -0
  104. package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
  105. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
  106. package/dist/nodefony/src/totp/totpCipher.js +30 -0
  107. package/dist/nodefony/src/totp/totpCrypto.js +226 -0
  108. package/dist/nodefony/src/totp/totpOperations.js +129 -0
  109. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
  110. package/dist/nodefony/src/voter/RoleVoter.js +32 -0
  111. package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
  112. package/dist/nodefony/src/voter/voterRegistry.js +20 -0
  113. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
  114. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
  115. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
  116. package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
  117. package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
  118. package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
  119. package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
  120. package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
  121. package/dist/nodefony/src/webhook/webhookSort.js +48 -0
  122. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
  123. package/dist/types/index.d.ts +157 -0
  124. package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
  125. package/dist/types/nodefony/command/security-token.d.ts +44 -0
  126. package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
  127. package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
  128. package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
  129. package/dist/types/nodefony/config/config.d.ts +295 -0
  130. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  131. package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
  132. package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
  133. package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
  134. package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
  135. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
  136. package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
  137. package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
  138. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
  139. package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
  140. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
  141. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
  142. package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
  143. package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
  144. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
  145. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
  146. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
  147. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
  148. package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
  149. package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
  150. package/dist/types/nodefony/contracts/index.d.ts +9 -0
  151. package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
  152. package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
  153. package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
  154. package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
  155. package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
  156. package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
  157. package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
  158. package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
  159. package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
  160. package/dist/types/nodefony/errors/index.d.ts +8 -0
  161. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
  162. package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
  163. package/dist/types/nodefony/service/auditService.d.ts +30 -0
  164. package/dist/types/nodefony/service/authFlow.d.ts +123 -0
  165. package/dist/types/nodefony/service/authorization.d.ts +33 -0
  166. package/dist/types/nodefony/service/cors.d.ts +48 -0
  167. package/dist/types/nodefony/service/csrf.d.ts +57 -0
  168. package/dist/types/nodefony/service/firewall.d.ts +148 -0
  169. package/dist/types/nodefony/service/oauth2.d.ts +66 -0
  170. package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
  171. package/dist/types/nodefony/service/tokenService.d.ts +103 -0
  172. package/dist/types/nodefony/service/totp.d.ts +58 -0
  173. package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
  174. package/dist/types/nodefony/service/webhooks.d.ts +160 -0
  175. package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
  176. package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
  177. package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
  178. package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
  179. package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
  180. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
  181. package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
  182. package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
  183. package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
  184. package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
  185. package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
  186. package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
  187. package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
  188. package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
  189. package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
  190. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
  191. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
  192. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
  193. package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
  194. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
  195. package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
  196. package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
  197. package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
  198. package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
  199. package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
  200. package/dist/types/nodefony/src/csp.d.ts +39 -0
  201. package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
  202. package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
  203. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
  204. package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
  205. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
  206. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
  207. package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
  208. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
  209. package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
  210. package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
  211. package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
  212. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
  213. package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
  214. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
  215. package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
  216. package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
  217. package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
  218. package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
  219. package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
  220. package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
  221. package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
  222. package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
  223. package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
  224. package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
  225. package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
  226. package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
  227. package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
  228. package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
  229. package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
  230. package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
  231. package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
  232. package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
  233. package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
  234. package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
  235. package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
  236. package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
  237. package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
  238. package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
  239. package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
  240. package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
  241. package/docs/api-keys.md +691 -0
  242. package/docs/audit.md +751 -0
  243. package/docs/authenticators.md +487 -0
  244. package/docs/authorization.md +497 -0
  245. package/docs/cors.md +497 -0
  246. package/docs/csrf.md +392 -0
  247. package/docs/external-jwt.md +181 -0
  248. package/docs/firewall.md +546 -0
  249. package/docs/headers.md +616 -0
  250. package/docs/index.md +207 -0
  251. package/docs/lexique.md +190 -0
  252. package/docs/oauth2.md +575 -0
  253. package/docs/obtenir-un-jeton.md +225 -0
  254. package/docs/tokens.md +520 -0
  255. package/docs/totp.md +804 -0
  256. package/docs/webauthn.md +733 -0
  257. package/docs/webhooks.md +1016 -0
  258. package/package.json +83 -0
@@ -0,0 +1,487 @@
1
+ ---
2
+ title: "Authenticators — prouver l'identité (session, mot de passe, JWT, clé API)"
3
+ navTitle: Authenticators
4
+ lang: fr
5
+ module: "@nodefony/security"
6
+ topic: authenticators
7
+ coverageModule: security
8
+ section: "Sécurité"
9
+ audience: [developer]
10
+ tags:
11
+ [
12
+ security,
13
+ authentication,
14
+ jwt,
15
+ apikey,
16
+ session,
17
+ basic,
18
+ bearer,
19
+ rfc6750,
20
+ rfc8725,
21
+ nist,
22
+ ]
23
+ version: "doc"
24
+ status: stable
25
+ updated: 2026-07-19
26
+ source: "src/packages/@nodefony/security/docs/authenticators.md"
27
+ ---
28
+
29
+ # Authenticators — prouver l'identité
30
+
31
+ > Un **authenticator** répond à une seule question : _« qui es-tu, et peux-tu le prouver ? »_. Il ne
32
+ > décide **pas** des droits (ça, c'est l'autorisation / les voters) — il établit une **identité**.
33
+ > Le firewall enchaîne les authenticators déclarés par une zone jusqu'à obtenir une preuve valide,
34
+ > sinon il ferme en 401 (Zero Trust). Nodefony en fournit **six** intégrés, tous ancrés ici sur le
35
+ > code (`src/packages/@nodefony/security/nodefony/src/authenticator/`).
36
+
37
+ 📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Authenticators**
38
+
39
+ ## 🧠 Le cycle d'un authenticator
40
+
41
+ ```mermaid
42
+ flowchart TD
43
+ REQ["Requête (HTTP ou WS)"] --> SUP{"supports(ctx) ?<br/>credential présent ?"}
44
+ SUP -->|non| NEXT["maillon suivant<br/>(ou 401 Zero Trust)"]
45
+ SUP -->|oui| CT["createToken()<br/>credential brut, non vérifié"]
46
+ CT --> AU["authenticate(token)<br/>vérifie · révocation · sujet"]
47
+ AU -->|échec| F["onFailure → 401 + challenge<br/>(message UNIFORME)"]
48
+ AU -->|succès| S["onSuccess → user + token dans l'ALS"]
49
+ S --> CTRL["→ autorisation → contrôleur"]
50
+ ```
51
+
52
+ C'est `Firewall.#authenticate()` (`firewall.ts:1112`) qui déroule ce cycle pour chaque maillon de la
53
+ zone, dans l'ordre déclaré. Le succès pose l'identité dans l'ALS ; l'échec remonte au firewall qui
54
+ pose le 401 et son challenge — l'authenticator, lui, ne touche jamais à la réponse.
55
+
56
+ ## 📖 Lexique
57
+
58
+ | Terme | Sens |
59
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------ |
60
+ | Authentification | Établir **qui** est l'appelant (≠ autorisation, qui établit ce qu'il a le **droit** de faire). |
61
+ | Authenticator | Une stratégie de preuve d'identité (`session`, `jwt`…) implémentant `IAuthenticator`. |
62
+ | BFF | _Backend For Frontend_ : le web s'authentifie par **session serveur** (cookie opaque), pas par jeton exposé au JS. |
63
+ | Bearer | Schéma `Authorization: Bearer <jeton>` (RFC 6750) — porté par les API. |
64
+ | PAT | _Personal Access Token_ : une clé API personnelle, bearer **opaque** révocable. |
65
+ | JWS/JWT | Jeton signé auto-porté (structure compacte `a.b.c`). |
66
+ | JWKS | _JSON Web Key Set_ : le trousseau de clés publiques qui vérifie les signatures JWT. |
67
+ | EdDSA | Algorithme de signature asymétrique (Ed25519) — le seul accepté par le vérificateur JWT. |
68
+ | CRC | Somme de contrôle embarquée dans une clé API — filtre les valeurs malformées avant la base. |
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. |
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
+
73
+ ## Qu'est-ce qu'un authenticator — et quelle faille il ferme
74
+
75
+ Un serveur qui expose des données doit distinguer un appelant légitime d'un inconnu. Le faire « à la
76
+ main » dans chaque contrôleur, c'est garantir qu'un endpoint finira par être oublié — la faille la
77
+ plus banale et la plus grave.
78
+
79
+ Nodefony **centralise** la preuve d'identité dans le firewall : une zone déclare _quelles preuves
80
+ elle accepte_, et rien n'atteint le contrôleur sans être passé par là. Chaque authenticator ferme une
81
+ classe d'attaque précise — détaillées brique par brique dans le catalogue :
82
+
83
+ - **énumération de comptes** (messages d'échec uniformes) ;
84
+ - **brute-force et DoS par hachage** (backoff NIST avant tout hash) ;
85
+ - **algorithm confusion / injection de clé JWT** (allowlist + JWKS local) ;
86
+ - **jeton volé non révocable** (denylist `jti`, PAT opaque révocable) ;
87
+ - **identité périmée** (sujet re-vérifié à chaque requête).
88
+
89
+ ## La vision Nodefony — un contrat, un registre, un firewall agnostique
90
+
91
+ ### Le contrat `IAuthenticator`
92
+
93
+ Tout authenticator implémente le même cycle (`IAuthenticator.ts:18`), ce qui rend le firewall
94
+ totalement agnostique de la stratégie :
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:42`) — la valeur `WWW-Authenticate` (RFC 7235) des 401 de la zone. |
104
+
105
+ ### Le registre pluggable
106
+
107
+ Les authenticators sont résolus par **nom** : `Firewall.#instantiateAuthenticators()`
108
+ (`firewall.ts:402`) interroge `getAuthenticatorFactory()` (`authenticatorRegistry.ts:59`) — jamais
109
+ un `if (name === "jwt")` dans le firewall, qui trahirait la promesse « pluggable ».
110
+
111
+ - Les **cinq builtins HTTP** (`anonymous`, `userpassword`, `session`, `jwt`, `apikey`)
112
+ s'enregistrent à l'import du module via `registerAuthenticatorFactory()`
113
+ (`authenticatorRegistry.ts:72-125`) — donc toujours avant le boot.
114
+ - Le sixiè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:268`).
116
+ - La fabrique ne fait que **construire** ; les résolutions de services coûteuses (`users`,
117
+ `tokenStore`, keystore) restent **lazy** dans l'instance (cold path).
118
+ - Un nom inconnu en config = boot **fail-closed** — `#configError` posé + log CRITIC
119
+ (`firewall.ts:419`) : jamais de zone « protégée » silencieusement ouverte à cause d'une
120
+ faute de frappe.
121
+
122
+ ## 🚀 Démarrage rapide
123
+
124
+ ### Une zone, trois preuves — la même API pour le web et les machines
125
+
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`) :
128
+
129
+ ```typescript
130
+ // nodefony.config.ts (extrait)
131
+ use("@nodefony/security", {
132
+ areas: {
133
+ // Une seule zone, trois preuves : le navigateur (cookie de session),
134
+ // un service (JWT), un script CI (clé API). `mode: "first"` (défaut) :
135
+ // le premier maillon qui reconnaît la requête authentifie.
136
+ api: {
137
+ pattern: "^/api",
138
+ authenticators: ["session", "jwt", "apikey"],
139
+ },
140
+ },
141
+ });
142
+ ```
143
+
144
+ > [!IMPORTANT]
145
+ > **Le login est FOURNI** : `POST /nodefony/security/api/auth/login` (body `{ username, password }`
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
148
+ > consommer l'identité.
149
+
150
+ ### Ce que TU écris : le controller qui consomme l'identité
151
+
152
+ ```typescript
153
+ // nodefony/controllers/WhoAmIController.ts — complet, compile tel quel
154
+ import {
155
+ controller,
156
+ Controller,
157
+ Get,
158
+ IsGranted,
159
+ CurrentUser,
160
+ } from "@nodefony/framework";
161
+ import type { IUser } from "@nodefony/user";
162
+
163
+ @controller("/api/v1")
164
+ class WhoAmIController extends Controller {
165
+ // Zone `api` : le firewall a DÉJÀ validé une des trois preuves (session,
166
+ // JWT ou clé API) — sinon 401 avant ce code. @IsGranted ajoute le rôle.
167
+ @IsGranted(["ROLE_USER"])
168
+ @Get("/whoami")
169
+ async whoami(@CurrentUser() user: IUser) {
170
+ // La même réponse quelle que soit la preuve présentée par le client.
171
+ return this.renderJson({ identifier: user.identifier, roles: user.roles });
172
+ }
173
+ }
174
+
175
+ export default WhoAmIController;
176
+ ```
177
+
178
+ ### Ce qu'on observe
179
+
180
+ ```bash
181
+ # 1) Sans preuve : Zero Trust → 401 + challenge du premier maillon qui en déclare
182
+ curl -si http://localhost:5151/api/v1/whoami | grep -E "^(HTTP|WWW)"
183
+ # HTTP/1.1 401 Unauthorized
184
+ # WWW-Authenticate: Bearer
185
+
186
+ # 2) Web — login BFF (compte dev seedé admin/admin) → cookie de session
187
+ curl -si -c /tmp/jar -H 'Content-Type: application/json' \
188
+ -d '{"username":"admin","password":"admin"}' \
189
+ http://localhost:5151/nodefony/security/api/auth/login | head -1
190
+ # HTTP/1.1 200 OK
191
+
192
+ # 3) La même route, deux preuves différentes → la même identité
193
+ curl -s -b /tmp/jar http://localhost:5151/api/v1/whoami # session (web)
194
+ curl -s -H 'Authorization: Bearer nf_…' \
195
+ http://localhost:5151/api/v1/whoami # clé API (CI)
196
+ # {"identifier":"admin","roles":["ROLE_NODEFONY_ADMIN", …]}
197
+ ```
198
+
199
+ Requête par requête, qui répond :
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`) |
207
+
208
+ ## 🔐 Les six authenticators intégrés
209
+
210
+ | Nom | Credential | Vérité | Révocable | Pour… |
211
+ | ------------------- | ---------------------------------------- | ---------- | :-------: | ---------------------------------- |
212
+ | `anonymous` | (aucun) | — | — | accepter l'anonymat explicitement |
213
+ | `userpassword` | `Authorization: Basic base64(id:mdp)` | verifier | n/a | outils/scripts, brique login |
214
+ | `session` | cookie de session (identifiant en blob) | serveur | immédiate | le **web** après login (BFF) |
215
+ | `jwt` | `Authorization: Bearer <a.b.c>` | auto-porté | via état | API service↔service, agents |
216
+ | `apikey` | `Authorization: Bearer nf_…` | serveur | immédiate | API/CI/scripts d'un user |
217
+ | `firewall-realtime` | identité déjà résolue au handshake (ALS) | serveur | 1 fenêtre | le **WebSocket** de toute identité |
218
+
219
+ ### `anonymous` — accepter explicitement l'anonymat
220
+
221
+ Le seul authenticator autorisé à produire un token **non authentifié** sans déclencher le Zero Trust
222
+ (`AnonymousAuthenticator.ts:19`).
223
+
224
+ - `supports()` accepte tout (`AnonymousAuthenticator.ts:22`) ; le token porte le **singleton gelé**
225
+ `anonymousUser` — zéro allocation d'utilisateur (`AnonymousToken.ts:9`).
226
+ - À ne lister **que volontairement** : `["jwt", "anonymous"]` en mode `first` signifie « identifié
227
+ si preuve présente, sinon visiteur anonyme accepté ». En mode `all`, utile en **dernier** :
228
+ « canal prouvé (ex. mTLS), identité utilisateur optionnelle ».
229
+ - Sans lui, zone protégée + aucune preuve = 401 : la défense en profondeur du firewall n'accepte un
230
+ token non authentifié que si `anonymous` est le maillon déclaré (`firewall.ts:827`).
231
+ - **Faille fermée** : l'anonymat _implicite_ — ici il est un choix explicite et auditable, jamais un
232
+ défaut.
233
+
234
+ ### `userpassword` — HTTP Basic + backoff NIST
235
+
236
+ Schéma **HTTP Basic** (RFC 7617) : `Authorization: Basic base64(id:mdp)`, charset UTF-8, scheme
237
+ case-insensitive (`UserPasswordAuthenticator.ts:11`) ; `createToken()` split au **premier** `:` —
238
+ le mot de passe peut en contenir (`UserPasswordAuthenticator.ts:74`).
239
+
240
+ - **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
241
+ leurre anti-timing, re-hash transparent — l'authenticator ne voit que le verdict.
242
+ - **Message uniforme** `INVALID_CREDENTIALS` quelle que soit la cause — identifiant inconnu, compte
243
+ verrouillé, mot de passe faux (`UserPasswordAuthenticator.ts:16`) → anti-énumération de comptes.
244
+ - **Throttling NIST SP 800-63B AVANT le verifier** : `#throttler.check()` sur l'identifiant saisi
245
+ (`UserPasswordAuthenticator.ts:101-103`) — un identifiant bloqué ne coûte **aucun hash** → le
246
+ throttle protège aussi le serveur du **DoS argon2**. Échec compté, succès remis à zéro
247
+ (`UserPasswordAuthenticator.ts:111-114`). `ThrottledError` → **429 + `Retry-After`**
248
+ (`firewall.ts:764`).
249
+ - **Le throttler est PARTAGÉ** avec le login JSON du BFF — même `loginThrottler` du container : un
250
+ attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts:75-79`).
251
+ - Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts:130`).
252
+ - **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
253
+ (`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
254
+
255
+ ### `session` — la preuve du web (BFF)
256
+
257
+ Après le login, chaque requête web prouve son identité par la **session serveur** (cookie opaque).
258
+ Credential = l'**identifiant** posé dans le blob de session, jamais un secret.
259
+
260
+ - **N'ouvre jamais la session lui-même** : `supports()` exige une session **déjà reprise** porteuse
261
+ d'un utilisateur (`SessionAuthenticator.ts:43-46`) — le pipeline http démarre la session _avant_
262
+ le firewall ; c'est `AuthFlow.login()` qui ouvre et régénère l'ID (anti-fixation).
263
+ - **L'identité est re-résolue à CHAQUE requête** via `resolveSessionIdentity`
264
+ (`SessionAuthenticator.ts:70`) → rôles frais, révocation immédiate. Les contrôles d'état sont
265
+ partagés avec `AuthFlow.me()` : `isLocked()`/`isActive()` → rejet (`sessionIdentity.ts:40`).
266
+ - `onSuccess()` pose l'identifiant sur le contexte — la persistance de session lie le blob au
267
+ principal courant (`SessionAuthenticator.ts:78-80`).
268
+ - **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login,
269
+ jamais une popup Basic (`SessionAuthenticator.ts:25-27`).
270
+
271
+ ### `jwt` — Bearer signé pour les API (RFC 6750 + BCP RFC 8725)
272
+
273
+ Réservé aux **API service↔service / agents** (le web reste sur la session). Vérifie un access token
274
+ **EdDSA** signé par le keystore du serveur ; `supports()` ne réclame que la structure compacte
275
+ `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:20`). Les défenses **dures** du JWT BCP, toutes
276
+ prouvées en test :
277
+
278
+ | Défense | Comment | Attaque fermée |
279
+ | -------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
280
+ | **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts:120`) | `alg=none`, algorithm confusion (§3.1) |
281
+ | **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts:158`) | injection de clé / SSRF (§3.5) |
282
+ | **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts:105-107`) | refresh présenté comme access, token d'un autre service (§3.8-3.9) |
283
+ | **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts:123-131`) | jeton auto-porté volé, logout global |
284
+ | **Sujet revérifié** | `loadUserByIdentifier(sub)` → disparu/inactif/verrouillé = rejet (`JwtAuthenticator.ts:174-186`) | compte banni encore « valide » via son token (§3.10) |
285
+
286
+ Le **message d'échec est uniforme** (`INVALID_TOKEN`, `JwtAuthenticator.ts:24`) : la cause fine
287
+ (expiré, `aud`, signature, sujet banni) part en **audit**, jamais au client — anti-oracle. Le token
288
+ promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts:162-171`). `jose` est
289
+ importé **lazy** — dépendance lourde (`JwtAuthenticator.ts:96`).
290
+
291
+ ### `apikey` — PAT opaque révocable
292
+
293
+ Clé API personnelle en `Authorization: Bearer nf_…` (préfixe `apiKeys.prefix`, défaut `nf`).
294
+ Contrairement au JWT (auto-porté), un PAT est un **bearer opaque** dont la vérité vit **côté
295
+ serveur** (`ITokenStore`) → **révocable immédiatement**. Défenses :
296
+
297
+ - **Forme + CRC validés AVANT tout accès au store** (`parseApiKey`, `ApiKeyAuthenticator.ts:99-101`)
298
+ → une valeur malformée n'atteint jamais la base (**anti-DoS**).
299
+ - **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts:105`) — le secret n'existe
300
+ **nulle part au repos**.
301
+ - **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts:109-111`) +
302
+ **ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts:117-118`).
303
+ - **Sujet revérifié** à chaque requête → rôles frais (`ApiKeyAuthenticator.ts:123`).
304
+ - **`lastUsedAt` throttlé** : aucune écriture sur le hot path tant que la fenêtre
305
+ `apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts:127-133`).
306
+
307
+ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:138-140`).
308
+
309
+ ### `firewall-realtime` — la promotion, en WebSocket, de l'identité déjà posée
310
+
311
+ > [!IMPORTANT]
312
+ > Ce n'est **pas** « l'authenticator de la session ». Il promeut **toute** identité que le firewall
313
+ > a résolue — y compris un agent authentifié par jeton porteur, sans cookie ni session. Son nom
314
+ > d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode branché, pas son rôle ; la
315
+ > confusion a coûté un durcissement pensé pour la session appliqué à toutes les identités
316
+ > (`FirewallRealtimeAuthenticator.ts:32-39`).
317
+
318
+ Sur un handshake WS (une requête upgrade HTTP qui traverse **le même pipeline**), `startSession`
319
+ puis `handleSecurity` ont **déjà** tourné : session chargée, identité re-résolue, `IUser` posé dans
320
+ l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
321
+ (`FirewallRealtimeAuthenticator.ts:80`).
322
+
323
+ - **Perf : il ne relit pas la base.** `authenticate()` réutilise l'identité de l'ALS
324
+ (`FirewallRealtimeAuthenticator.ts:91`) au lieu de refaire deux lectures base par connexion —
325
+ un coût évitable sur le différenciateur temps réel.
326
+ - **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts:268`) pour toute zone
327
+ protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts:289`).
328
+ - **Le mode de preuve suit le jeton du firewall**, il n'est pas deviné : absent (zone historique),
329
+ on retombe sur le mode le plus strict, la session (`FirewallRealtimeAuthenticator.ts:101-103`).
330
+ - **Filet Zero Trust** : il câble un revalidateur appelé avant chaque action data plane
331
+ (`FirewallRealtimeAuthenticator.ts:105-107`) — la socket peut survivre à l'identité qui l'a
332
+ ouverte (logout, changement de compte, `jti` denylisté). En mode session,
333
+ `buildSessionRevalidator()` re-lit `storage.read(id)` et compare l'identifiant ; toute erreur de
334
+ lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts:227`). En mode jeton porteur, la
335
+ preuve est autre : `exp`, `jti` denylisté, `invalidBefore`
336
+ (`FirewallRealtimeAuthenticator.ts:127`).
337
+
338
+ > [!NOTE]
339
+ > **Asymétrie de révocation HTTP↔WS (assumée)** : le jeton realtime est figé au handshake (les
340
+ > frames lisent un cache O(1)) → une révocation prend effet **à la reconnexion**, pas à la frame
341
+ > suivante. C'est l'état de l'art (Socket.IO/Phoenix figent aussi au handshake) ; la révocation
342
+ > immédiate forte passe par le JWT + un canal « token révoqué ».
343
+
344
+ ## ⚙️ Composer une zone — ordre, mode, cohabitation
345
+
346
+ La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
347
+ (`firewall.ts:1112`) selon le `mode` de la zone (`first` par défaut, `config.ts:87-92`).
348
+
349
+ ### Situation 1 — humains ET machines sur la même API (`first`)
350
+
351
+ Ton back-office est appelé par le **navigateur** des utilisateurs connectés ET par un **script CI**.
352
+ Deux preuves différentes, mêmes routes — c'est la config du Démarrage rapide ci-dessus. Deux règles
353
+ de lecture :
354
+
355
+ - un maillon dont `supports()` est faux est simplement **sauté** en mode `first`
356
+ (`firewall.ts:1128`) ;
357
+ - un credential **présenté mais invalide échoue immédiatement** — l'échec d'`authenticate()`
358
+ remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts:1112`). Une clé
359
+ API révoquée donne un 401 direct, même si un autre maillon aurait pu réussir.
360
+ - aucune preuve présentée sur toute la chaîne → `handleSecurity()` lève l'`AuthenticationError`
361
+ Zero Trust (`firewall.ts:738`).
362
+
363
+ ### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
364
+
365
+ Tu veux « identifié si connecté, sinon visiteur » :
366
+
367
+ ```typescript
368
+ authenticators: ["session", "anonymous"], // ✅ session d'abord
369
+ authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le monde
370
+ ```
371
+
372
+ > [!WARNING]
373
+ > `AnonymousAuthenticator.supports()` accepte **toutes** les requêtes — placé en premier en mode
374
+ > `first`, il court-circuite la liste : **personne n'est jamais identifié**, même avec un cookie
375
+ > valide. L'ordre est ta politique.
376
+
377
+ ### Situation 3 — empiler les preuves (`all`)
378
+
379
+ En mode `all`, **chaque** maillon est obligatoire : `supports()` faux = 401 immédiat
380
+ (`firewall.ts:937`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts:973`) —
381
+ utile pour exiger une preuve de canal (mTLS) **et** une identité, ou un « sudo mode » session +
382
+ re-saisie du mot de passe. Scénario complet côté zones : [firewall](./firewall.md).
383
+
384
+ ### Cohabitation JWT + clé API dans une même zone
385
+
386
+ Les deux sont des `Bearer`, mais Nodefony les **discrimine sur la forme** — un JWT a la structure
387
+ compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:20`), un PAT porte le préfixe `nf_` sans
388
+ point (`looksLikeApiKey`, `ApiKeyAuthenticator.ts:72`). Chaque `supports()` ne réclame que _son_
389
+ format → aucun conflit, aucune double vérification.
390
+
391
+ ## Le fil rouge : le message d'échec uniforme
392
+
393
+ Les cinq authenticators vérifiants renvoient **le même message** (`"Invalid credentials"` /
394
+ `"Invalid token"` / `"Invalid session"`) quelle que soit la cause réelle. Ce n'est pas de la
395
+ paresse : c'est une **défense anti-énumération / anti-oracle**.
396
+
397
+ Distinguer « compte inconnu » de « mot de passe faux », ou « token expiré » de « signature
398
+ invalide », donnerait à un attaquant une sonde. La cause fine part **toujours** en log d'audit ; le
399
+ client n'obtient qu'un 401 + son challenge — posé par le firewall, premier maillon de la zone qui
400
+ en déclare un (`Firewall.#setChallenge()`, `firewall.ts:1191`).
401
+
402
+ ## 🧩 Ajouter un authenticator maison
403
+
404
+ ```typescript
405
+ import { registerAuthenticatorFactory } from "@nodefony/security";
406
+
407
+ registerAuthenticatorFactory("ldap", ({ container, config }) => {
408
+ return new LdapAuthenticator(() => container.get("ldapClient"));
409
+ });
410
+ // puis en config : areas.<zone>.authenticators = ["ldap", "anonymous"]
411
+ ```
412
+
413
+ À faire au chargement du module (avant le boot). Trois règles, calquées sur les builtins :
414
+
415
+ - implémenter le contrat `IAuthenticator` (`IAuthenticator.ts:23`) — `challenge()` seulement si
416
+ un en-tête `WWW-Authenticate` a du sens pour la stratégie ;
417
+ - renvoyer le **message uniforme** en cas d'échec (la cause fine part en audit) ;
418
+ - laisser les résolutions de services **lazy** dans l'instance — la fabrique ne fait que
419
+ construire (`authenticatorRegistry.ts:29-31`).
420
+
421
+ ## 📜 Normes appliquées
422
+
423
+ <!-- prettier-ignore -->
424
+ | Domaine | Norme | Ancrage |
425
+ | --- | --- | --- |
426
+ | Challenge d'auth (401) | RFC 7235 | `Firewall.#setChallenge()` (`firewall.ts:1191`) |
427
+ | Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts:68`, cœur) — une porte UNIQUE au cœur, plus une constante par authenticator |
428
+ | JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts:103-107`) |
429
+ | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts:25-27`) |
430
+ | Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts:101-103`) |
431
+ | Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts:764`) |
432
+ | Anti-énumération | OWASP | `INVALID_TOKEN` (`JwtAuthenticator.ts:24`) · `INVALID_CREDENTIALS` (`UserPasswordAuthenticator.ts:16`) |
433
+
434
+ ## ⚡ Performance & mémoire
435
+
436
+ - `supports()` est un test **bon marché** (en-tête + regex) — et n'est payé que sur zone protégée
437
+ (le chemin chaud/froid vit dans le [firewall](./firewall.md)).
438
+ - Résolutions **lazy** : le verifier `#resolveVerifier` est mémoïsé au premier login
439
+ (`UserPasswordAuthenticator.ts:105`) ; keystore, `tokenStore` et `users` sont résolus du
440
+ container au premier usage ; `jose` est importé lazy (`JwtAuthenticator.ts:96`).
441
+ - `anonymous` : singleton gelé `anonymousUser`, zéro allocation d'utilisateur
442
+ (`AnonymousToken.ts:9`).
443
+ - `apikey` : écriture `lastUsedAt` **coalescée** — pas une écriture par requête
444
+ (`ApiKeyAuthenticator.ts:127-133`).
445
+ - `firewall-realtime` : **zéro lecture base** au handshake — réutilise l'ALS
446
+ (`FirewallRealtimeAuthenticator.ts:91`).
447
+ - Le throttle NIST **avant** le hash : un 429 ne coûte aucun argon2.
448
+
449
+ ## ⚠️ Pièges (symptôme → cause → correction)
450
+
451
+ <!-- prettier-ignore -->
452
+ | Symptôme | Cause (dans le code) | Correction |
453
+ | --- | --- | --- |
454
+ | 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts:827`) | Ajouter `anonymous` en dernier si l'anonymat est voulu |
455
+ | 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 |
456
+ | 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) |
457
+ | 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` |
458
+ | 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 |
459
+ | Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts:617`) | Configurer `rateLimit` pour poser le throttler |
460
+
461
+ ## 🧪 Tests & couverture
462
+
463
+ Trois familles couvrent la brique — les **chiffres exacts vivent dans la carte de l'aperçu**
464
+ (régénérée par `gen-counters.mjs` depuis vitest, jamais figée ici) :
465
+
466
+ - **unit** (`security/tests/unit/`) : la chaîne + les modes (`authenticators`), les défenses JWT
467
+ RFC 8725 (`jwt.attack`) et le pipeline JWT bout en bout (`jwtPipeline`), la clé API — flux,
468
+ service et forme/CRC (`apiKeyAuthenticator`, `apiKeyService`, `apiKeyFormat`), la session
469
+ (`sessionAuthenticator`), le backoff NIST (`loginThrottler`), le step-up 2FA (`mfaStepUp`) ;
470
+ - **intégration** (serveur réel, `@nodefony/http`) : le flux clé API de bout en bout
471
+ (`apikey-flow`), un JWT autorisé sur WebSocket (`ws-isgranted-jwt`) ;
472
+ - l'**émission** des jetons (keystore, tokenStore) est couverte sur la page
473
+ [tokens](./tokens.md) ; les bancs d'attaque transverses (csrf, cors) sur leurs pages.
474
+
475
+ Couverture : `npm run coverage` dans `@nodefony/security`.
476
+
477
+ ## 🔗 Pour aller plus loin
478
+
479
+ - ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
480
+ - 🧭 **Pages sœurs** : [Firewall](firewall.md) · [Jetons](tokens.md)
481
+
482
+ - Le firewall qui enchaîne les authenticators (zones, modes, en-têtes) → [firewall](./firewall.md)
483
+ - L'autorisation (voters, rôles, scopes) après l'authentification → [authorization](./authorization.md)
484
+ - Émission/révocation des jetons (keystore, tokenStore) → [tokens](./tokens.md)
485
+ - Les autres preuves — flux BFF, pas des authenticators de zone : [oauth2](./oauth2.md) ·
486
+ [webauthn](./webauthn.md) · [totp](./totp.md)
487
+ - Vue d'ensemble sécurité → [index](./index.md)