@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,174 @@
1
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { UserRealtimeToken } from "../realtime/UserRealtimeToken.js";
3
+ import { Nodefony, RequestContext } from "nodefony";
4
+ import { anonymousUser } from "@nodefony/user";
5
+ //#region nodefony/src/authenticator/FirewallRealtimeAuthenticator.ts
6
+ /**
7
+ * Authenticator realtime des identités résolues par le **firewall** — équivalent
8
+ * WS de tout ce que le pipeline HTTP sait authentifier.
9
+ *
10
+ * ── Pourquoi il NE re-lit PAS la base ──────────────────────────────────────
11
+ * Un handshake WebSocket est une requête upgrade HTTP qui traverse le MÊME
12
+ * pipeline : `startSession` (reprise L1 du cookie) **puis** `firewall.handleSecurity`
13
+ * tournent AVANT que le `RealtimeController` ne fasse son handshake. Sur une zone
14
+ * data plane, le firewall a donc DÉJÀ : (1) authentifié (session, JWT, clé API…),
15
+ * (2) re-résolu l'identité via le provider `users` (rôles frais), (3) posé
16
+ * l'`IUser` **et le jeton** dans l'ALS et appliqué le Zero Trust (un anonyme est
17
+ * fermé AVANT d'arriver ici). Re-décoder le credential ici referait des lectures
18
+ * base **redondantes** par connexion — sur le différenciateur temps réel, un coût
19
+ * évitable. → on **réutilise** l'identité déjà en ALS.
20
+ *
21
+ * Le `RealtimeController.onHandshake` s'exécute dans la même bulle ALS que le
22
+ * firewall (un seul `RequestContext.run` enveloppe handshake + frames) → la
23
+ * lecture est sûre et synchrone.
24
+ *
25
+ * ── Il n'est PAS l'authenticator « de la session » ──────────────────────────
26
+ * Son nom d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode
27
+ * branché, pas son rôle : il promeut **toute** identité que le firewall a posée,
28
+ * y compris un agent authentifié par jeton porteur, sans cookie ni session. La
29
+ * confusion a coûté cher — un durcissement pensé pour la session a été appliqué
30
+ * à toutes les identités, et une connexion JWT parfaitement valide se faisait
31
+ * révoquer au motif qu'elle n'avait pas de session. D'où le nom actuel : il dit
32
+ * d'où vient l'identité (le firewall), pas comment elle a été prouvée.
33
+ *
34
+ * ── Révocation : un invariant, deux preuves ────────────────────────────────
35
+ * L'invariant est unique — **une socket ne survit pas à l'identité qui l'a
36
+ * ouverte** — mais la preuve dépend du mode, parce que ce sont deux mécanismes
37
+ * de révocation différents :
38
+ *
39
+ * | Mode | Ce qui rend l'identité morte |
40
+ * | -------------------------- | ------------------------------------------------ |
41
+ * | session BFF (`session`) | session détruite, expirée, ou passée à un autre |
42
+ * | jeton porteur (JWT, clé…) | `exp` atteint · `jti` denylisté · `invalidBefore` |
43
+ *
44
+ * Le jeton est figé au handshake (les frames lisent un cache O(1), jamais la
45
+ * base) ; la re-validation tourne sur le tick du hub (`REVOCATION_REVALIDATE_MS`)
46
+ * et devant chaque `api.request`. Une révocation prend donc effet en une fenêtre,
47
+ * pas à la frame suivante — c'est l'état de l'art (Socket.IO/Phoenix figent aussi
48
+ * l'identité au handshake).
49
+ */
50
+ var FirewallRealtimeAuthenticator = class {
51
+ name = "firewall-realtime";
52
+ /**
53
+ * Accès **paresseux** au store de révocation des jetons porteurs. Résolu au
54
+ * plus tôt à la première re-validation d'une socket à jeton — jamais au boot,
55
+ * jamais pour une session. Absent (`null`) → seule la borne `exp` du jeton
56
+ * fait foi.
57
+ */
58
+ #resolveStore;
59
+ /**
60
+ * @param resolveStore - fournit le store de révocation des jetons (le firewall
61
+ * passe une closure sur son container). Omis → mode dégradé documenté :
62
+ * seules les bornes portées par le jeton lui-même sont vérifiables.
63
+ */
64
+ constructor(resolveStore = null) {
65
+ this.#resolveStore = resolveStore;
66
+ }
67
+ /** Une identité authentifiée a-t-elle été résolue (par le firewall) au handshake ? */
68
+ supports(_handshake) {
69
+ return isAuthenticatedUser(RequestContext.getUser());
70
+ }
71
+ /**
72
+ * Promeut l'identité déjà résolue (ALS) en jeton realtime — 0 lecture base.
73
+ *
74
+ * @throws AuthenticationError — aucune identité authentifiée en ALS (ne devrait
75
+ * pas arriver sur une zone data plane : le firewall ferme l'anonyme en amont ;
76
+ * filet défensif fail-closed → le hub ferme la socket en 4001).
77
+ */
78
+ async authenticate(_handshake) {
79
+ const user = RequestContext.getUser();
80
+ if (!isAuthenticatedUser(user)) throw new AuthenticationError("Invalid realtime session");
81
+ const issued = readFirewallToken();
82
+ const type = typeof issued?.type === "string" && issued.type ? issued.type : "session";
83
+ const revalidate = type === "session" ? buildSessionRevalidator(user.identifier) : this.#buildBearerRevalidator(issued, user.identifier, type);
84
+ const scopes = issued ? issued.getScopes() : void 0;
85
+ return new UserRealtimeToken(user, revalidate, type, scopes);
86
+ }
87
+ /**
88
+ * Re-validation d'une identité portée par un **jeton** (JWT, clé API, OAuth) :
89
+ * rejoue les trois conditions qui tuent un jeton, sans jamais parler de session.
90
+ *
91
+ * `exp` est vérifié **sans personne** (la borne voyage dans le jeton) ; le store
92
+ * n'ajoute que la révocation *avant terme*. C'est pourquoi un store en panne ne
93
+ * coupe pas les sockets d'agents : ce serait une panne de disponibilité déguisée
94
+ * en mesure de sécurité, alors que la borne du jeton tient toujours.
95
+ */
96
+ #buildBearerRevalidator(issued, identifier, type) {
97
+ const claims = issued.getAttribute("claims") ?? EMPTY_CLAIMS;
98
+ const expMs = secondsToMs(claims.exp);
99
+ const iatMs = secondsToMs(claims.iat);
100
+ const jti = issued.getAttribute("jti") ?? (typeof claims.jti === "string" ? claims.jti : null);
101
+ const resolveStore = this.#resolveStore;
102
+ if (expMs === null && (resolveStore === null || jti === null && iatMs === null)) {
103
+ Nodefony.getKernel()?.log?.(`Realtime ${type} token "${identifier}" carries neither an expiry nor any revocation handle ("jti"/"iat") — connection will be revoked (fail-closed). Ensure the authenticator forwards the token claims.`, "WARNING");
104
+ return () => Promise.resolve(false);
105
+ }
106
+ return async (nowMs = Date.now()) => {
107
+ if (expMs !== null && nowMs >= expMs) return false;
108
+ if (!resolveStore) return true;
109
+ let store;
110
+ try {
111
+ store = resolveStore();
112
+ } catch {
113
+ return expMs !== null;
114
+ }
115
+ if (!store) return expMs !== null;
116
+ try {
117
+ if (jti !== null && await store.isJtiDenied(jti)) return false;
118
+ const invalidBefore = await store.getInvalidBefore(identifier);
119
+ if (invalidBefore !== null && iatMs !== null && iatMs < invalidBefore) return false;
120
+ return true;
121
+ } catch {
122
+ return expMs !== null;
123
+ }
124
+ };
125
+ }
126
+ };
127
+ const EMPTY_CLAIMS = Object.freeze({});
128
+ /** Claim temporel JWT (secondes, RFC 7519) → epoch ms, ou `null` si absent/invalide. */
129
+ function secondsToMs(value) {
130
+ return typeof value === "number" && Number.isFinite(value) ? value * 1e3 : null;
131
+ }
132
+ /** Le jeton posé dans l'ALS par `firewall.handleSecurity`, s'il y en a un. */
133
+ function readFirewallToken() {
134
+ return RequestContext.get()?.token;
135
+ }
136
+ /**
137
+ * Re-validation d'une identité portée par la **session BFF** : re-lit la session
138
+ * (par son id, capturé au handshake) et vérifie qu'elle est TOUJOURS vivante et
139
+ * TOUJOURS celle de `identifier` — ce qui détecte la déconnexion comme le
140
+ * changement de compte sur un navigateur partagé.
141
+ *
142
+ * Renvoie TOUJOURS un revalidateur (jamais `null`) : une identité authentifiée
143
+ * par session DOIT rester révocable. Une lecture qui throw (store down, session
144
+ * détruite) → invalide ; une session non re-lisible au handshake → invalide
145
+ * aussi (fail-closed, cf ci-dessous).
146
+ */
147
+ function buildSessionRevalidator(identifier) {
148
+ const session = RequestContext.getContext()?.session;
149
+ const id = session?.id;
150
+ const storage = session?.storage;
151
+ if (typeof id !== "string" || !storage || typeof storage.read !== "function") {
152
+ Nodefony.getKernel()?.log?.(`Realtime session token "${identifier}" has no revalidatable session at handshake — connection will be revoked (fail-closed). Ensure the realtime zone runs after startSession.`, "WARNING");
153
+ return () => Promise.resolve(false);
154
+ }
155
+ return async () => {
156
+ try {
157
+ const serialized = await storage.read(id);
158
+ return !!serialized && serialized.user === identifier;
159
+ } catch {
160
+ return false;
161
+ }
162
+ };
163
+ }
164
+ /**
165
+ * Vrai si `user` est un utilisateur AUTHENTIFIÉ (≠ anonyme). Le firewall pose
166
+ * `token.getUser()` dans l'ALS : soit l'`IUser` réel, soit `anonymousUser`
167
+ * (singleton). Tout ce qui n'est pas l'anonyme et porte un `identifier` est
168
+ * une identité valide.
169
+ */
170
+ function isAuthenticatedUser(user) {
171
+ return !!user && user !== anonymousUser && typeof user.identifier === "string";
172
+ }
173
+ //#endregion
174
+ export { FirewallRealtimeAuthenticator, FirewallRealtimeAuthenticator as default };
@@ -0,0 +1,176 @@
1
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { UserToken } from "../token/UserToken.js";
3
+ import { bearerToken } from "./bearer.js";
4
+ import peekIssuer from "./peekIssuer.js";
5
+ //#region nodefony/src/authenticator/JwtAuthenticator.ts
6
+ const COMPACT_JWS = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
7
+ const INVALID_TOKEN = "Invalid token";
8
+ /**
9
+ * Authentification par **JWT Bearer** (RFC 6750) — réservée API service↔service /
10
+ * agents (le web utilise la session BFF). Vérifie un access token EdDSA signé par
11
+ * le {@link IJwtKeystore} du serveur.
12
+ *
13
+ * Défenses **dures** (RFC 8725 JWT BCP, prouvées en test) :
14
+ * - **allowlist d'algorithmes** côté serveur (`["EdDSA"]`) — l'algo n'est JAMAIS
15
+ * choisi d'après l'en-tête du token (§3.1) ; `alg=none` jamais accepté par jose.
16
+ * - **clé par `kid` depuis le keyset LOCAL** (`createLocalJWKSet`) — jamais
17
+ * `jku`/`jwk` de l'en-tête (injection de clé / SSRF, §3.5).
18
+ * - **`aud` (§3.9) + `iss` (§3.8) obligatoires** + `typ:"at+jwt"` (§3.11, sépare
19
+ * access et refresh) + `exp`/`nbf` (jose).
20
+ * - **révocation** : denylist `jti` + seuil `invalidBefore` par porteur (le JWT
21
+ * est auto-porté et non révocable sans état serveur).
22
+ * - **sujet revérifié** (§3.10) : `loadUserByIdentifier(sub)` → compte disparu,
23
+ * inactif ou verrouillé = rejet.
24
+ *
25
+ * Dépendances (keystore, store, userProvider) résolues **paresseusement** du
26
+ * container au premier usage (cold path) ; jose importé **lazy** (dep lourde).
27
+ */
28
+ var JwtAuthenticator = class {
29
+ name = "jwt";
30
+ #container;
31
+ #runtime;
32
+ #jose = null;
33
+ #getKey = null;
34
+ #keystore = null;
35
+ #store = null;
36
+ #userProvider = null;
37
+ /**
38
+ * @param container - container DI (résolution lazy de `jwtKeystore`/`tokenStore`/`users`).
39
+ * @param runtime - paramètres JWT effectifs (iss/aud/ttl) partagés avec l'émetteur.
40
+ */
41
+ constructor(container, runtime) {
42
+ this.#container = container;
43
+ this.#runtime = runtime;
44
+ }
45
+ /**
46
+ * La requête porte-t-elle un `Authorization: Bearer <jws>` émis par NOUS ?
47
+ *
48
+ * L'émetteur revendiqué est lu sans être vérifié ({@link peekIssuer}) et sert
49
+ * uniquement à AIGUILLER : `ExternalJwtAuthenticator` reconnaît la même forme
50
+ * de credential pour les jetons d'un serveur d'autorisation tiers. Sans ce
51
+ * discriminant, en mode `first`, le premier des deux listés dans la zone
52
+ * capturerait les deux familles et refuserait la moitié des jetons — l'ordre
53
+ * de la configuration deviendrait une décision de sécurité, dont l'erreur ne
54
+ * se verrait qu'en production.
55
+ *
56
+ * Un jeton dont l'émetteur est illisible reste pris en charge ici : c'est un
57
+ * jeton maison malformé, que la vérification refusera en le disant, plutôt
58
+ * qu'un credential qui disparaîtrait sans laisser de trace.
59
+ */
60
+ supports(context) {
61
+ const auth = context.request?.headers?.authorization;
62
+ if (typeof auth !== "string") return false;
63
+ const token = bearerToken(auth);
64
+ if (token === null || !COMPACT_JWS.test(token)) return false;
65
+ const issuer = peekIssuer(token);
66
+ return issuer === null || issuer === this.#runtime.issuer;
67
+ }
68
+ /** Extrait le token brut (non vérifié) → porté par un `UserToken` type `"jwt"`. */
69
+ createToken(context) {
70
+ const auth = context.request?.headers?.authorization;
71
+ return Promise.resolve(new UserToken("jwt", bearerToken(auth) ?? ""));
72
+ }
73
+ /**
74
+ * Vérifie la signature + les claims du JWT, applique la révocation et résout le
75
+ * sujet — ou lève un 401 au message uniforme.
76
+ *
77
+ * @throws AuthenticationError (401) — token absent/invalide/expiré/révoqué, ou
78
+ * sujet disparu/banni.
79
+ * @throws Error (câblage : keystore/store/users absents) — logguée ERROR par le
80
+ * firewall puis 401 fail-closed (rien ne fuite au client).
81
+ */
82
+ async authenticate(token) {
83
+ const raw = token.getCredentials();
84
+ if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
85
+ const jose = this.#jose ??= await import("jose");
86
+ const getKey = await this.#ensureGetKey(jose);
87
+ let payload;
88
+ try {
89
+ payload = (await jose.jwtVerify(raw, getKey, {
90
+ algorithms: ["EdDSA"],
91
+ issuer: this.#runtime.issuer,
92
+ audience: this.#runtime.audiences,
93
+ typ: "at+jwt"
94
+ })).payload;
95
+ } catch {
96
+ throw new AuthenticationError(INVALID_TOKEN);
97
+ }
98
+ const sub = payload.sub;
99
+ const jti = payload.jti;
100
+ if (typeof sub !== "string" || typeof jti !== "string") throw new AuthenticationError(INVALID_TOKEN);
101
+ const store = this.#resolveStore();
102
+ if (await store.isJtiDenied(jti)) throw new AuthenticationError(INVALID_TOKEN);
103
+ const invalidBefore = await store.getInvalidBefore(sub);
104
+ if (invalidBefore !== null) {
105
+ if ((typeof payload.iat === "number" ? payload.iat * 1e3 : 0) < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
106
+ }
107
+ const user = await this.#resolveUserOrReject(sub);
108
+ return this.#promote(token, user, payload);
109
+ }
110
+ /** Slot audit (J4b). */
111
+ onSuccess(_context, _token) {
112
+ return Promise.resolve();
113
+ }
114
+ /** Slot audit (J4b) — le 401 + challenge sont posés par le firewall. */
115
+ onFailure(_context, _error) {
116
+ return Promise.resolve();
117
+ }
118
+ /** Challenge RFC 6750/7235 posé par le firewall sur les 401 de la zone. */
119
+ challenge() {
120
+ return "Bearer";
121
+ }
122
+ /** Construit (une fois) le résolveur de clé par `kid` depuis le JWKS public. */
123
+ async #ensureGetKey(jose) {
124
+ if (this.#getKey !== null) return this.#getKey;
125
+ const jwks = await this.#resolveKeystore().getPublicJWKS();
126
+ return this.#getKey = jose.createLocalJWKSet(jwks);
127
+ }
128
+ /** Promeut le token : utilisateur vérifié + scopes/claims posés en attributs. */
129
+ #promote(token, user, payload) {
130
+ const ut = token;
131
+ ut.promote(user);
132
+ const scope = payload.scope;
133
+ const scopes = typeof scope === "string" && scope.length > 0 ? scope.split(" ") : [];
134
+ ut.setAttribute("scopes", scopes);
135
+ ut.setAttribute("jti", payload.jti);
136
+ ut.setAttribute("claims", payload);
137
+ return ut;
138
+ }
139
+ async #resolveUserOrReject(sub) {
140
+ const provider = this.#resolveUserProvider();
141
+ let user;
142
+ try {
143
+ user = await provider.loadUserByIdentifier(sub);
144
+ } catch {
145
+ throw new AuthenticationError(INVALID_TOKEN);
146
+ }
147
+ if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
148
+ return user;
149
+ }
150
+ #resolveKeystore() {
151
+ if (this.#keystore === null) {
152
+ const ks = this.#container.get("jwtKeystore");
153
+ if (!ks) throw new Error("JwtAuthenticator: service 'jwtKeystore' absent du container — le TokenService de @nodefony/security doit être chargé.");
154
+ this.#keystore = ks;
155
+ }
156
+ return this.#keystore;
157
+ }
158
+ #resolveStore() {
159
+ if (this.#store === null) {
160
+ const store = this.#container.get("tokenStore");
161
+ if (!store) throw new Error("JwtAuthenticator: service 'tokenStore' absent du container — le TokenService de @nodefony/security doit être chargé.");
162
+ this.#store = store;
163
+ }
164
+ return this.#store;
165
+ }
166
+ #resolveUserProvider() {
167
+ if (this.#userProvider === null) {
168
+ const provider = this.#container.get("users");
169
+ if (!provider) throw new Error("JwtAuthenticator: aucun service 'users' (IUserProvider) dans le container — enregistrer un UserService au boot de l'application.");
170
+ this.#userProvider = provider;
171
+ }
172
+ return this.#userProvider;
173
+ }
174
+ };
175
+ //#endregion
176
+ export { JwtAuthenticator, JwtAuthenticator as default };
@@ -0,0 +1,92 @@
1
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { resolveSessionIdentity } from "../sessionIdentity.js";
3
+ import { UserToken } from "../token/UserToken.js";
4
+ //#region nodefony/src/authenticator/SessionAuthenticator.ts
5
+ /**
6
+ * Authentification par **session serveur** (cookie opaque, modèle BFF) — la
7
+ * preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
8
+ * l'identifiant dans le blob et régénéré l'ID anti-fixation).
9
+ *
10
+ * `supports()` exige une session REPRISE porteuse d'un utilisateur : le
11
+ * pipeline http démarre la session AVANT le firewall (point d'activation
12
+ * unique, lazy — cookie entrant ou intent de route), cet authenticator ne
13
+ * démarre jamais rien lui-même. L'identité est re-résolue à CHAQUE requête
14
+ * via {@link resolveSessionIdentity} (rôles frais, révocation immédiate).
15
+ *
16
+ * Pas de `challenge()` : une session absente/expirée donne un 401 nu — le
17
+ * client web redirige vers son écran de login, jamais de popup Basic. Si la
18
+ * zone liste aussi `userpassword`, le firewall pose SON challenge (RFC 7235).
19
+ */
20
+ var SessionAuthenticator = class {
21
+ name = "session";
22
+ #provider = null;
23
+ #resolveProvider;
24
+ /**
25
+ * @param resolveProvider - résolution lazy de la source d'identité
26
+ * (typiquement `container.get("users")`) — appelée à la première requête.
27
+ */
28
+ constructor(resolveProvider) {
29
+ this.#resolveProvider = resolveProvider;
30
+ }
31
+ /**
32
+ * Refuse une zone déclarée SANS REGISTRE — au boot, pas à la première requête.
33
+ *
34
+ * `stateless: true` annonce que l'identité tient tout entière dans la preuve
35
+ * portée par chaque requête, et que la session est ignorée « même si un
36
+ * cookie est présent ». Lister `session` dans une telle zone dit exactement
37
+ * l'inverse : {@link supports} y rendrait vrai dès qu'un cookie ramène une
38
+ * session porteuse d'un utilisateur, et la zone authentifierait par le
39
+ * registre qu'elle déclare ne pas tenir.
40
+ *
41
+ * Cette contradiction ne se voyait NULLE PART : l'application démarrait, la
42
+ * console d'administration affichait « aucun registre serveur », et le
43
+ * cookie authentifiait quand même. Elle se refuse donc au démarrage — le
44
+ * firewall en fait une erreur de configuration fail-closed, plutôt qu'une
45
+ * requête sur deux qui se comporte autrement que ce qui est écrit.
46
+ *
47
+ * @param area - la zone qui liste cet authenticator.
48
+ * @throws Error si la zone est `stateless` — le message la NOMME.
49
+ */
50
+ validateArea(area) {
51
+ if (area.stateless) throw new Error(`area "${area.name}": l'authenticator "${this.name}" est incompatible avec \`stateless: true\` — une zone sans registre ne peut pas tirer son identité d'une session serveur. Retirer "session" de cette zone (l'appelant porte sa preuve : \`apikey\`, \`jwt\`, \`external-jwt\`), ou passer la zone à \`stateless: false\` si elle sert un navigateur.`);
52
+ }
53
+ /** La requête porte-t-elle une session reprise avec un utilisateur ? */
54
+ supports(context) {
55
+ const user = context.session?.user;
56
+ return typeof user === "string" && user.length > 0;
57
+ }
58
+ /** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
59
+ createToken(context) {
60
+ const credentials = { identifier: context.session?.user ?? "" };
61
+ return Promise.resolve(new UserToken(this.name, credentials));
62
+ }
63
+ /**
64
+ * Re-résout l'identifiant de session en utilisateur vivant et promeut le
65
+ * token. Les contrôles d'état (existe, actif, non verrouillé) vivent dans
66
+ * {@link resolveSessionIdentity} — partagés avec `AuthFlow.me()`.
67
+ *
68
+ * @throws AuthenticationError (401, message uniforme) — session orpheline,
69
+ * compte verrouillé ou désactivé.
70
+ */
71
+ async authenticate(token) {
72
+ const credentials = token.getCredentials();
73
+ if (!credentials?.identifier) throw new AuthenticationError("Invalid session");
74
+ const provider = this.#provider ??= this.#resolveProvider();
75
+ const user = await resolveSessionIdentity(provider, credentials.identifier);
76
+ return token.promote(user);
77
+ }
78
+ /**
79
+ * Pose l'identifiant sur le contexte : la persistance de session du pipeline
80
+ * (`saveSession`) lie le blob au principal courant (string attendu).
81
+ */
82
+ onSuccess(context, token) {
83
+ context.user = token.getUser().identifier;
84
+ return Promise.resolve();
85
+ }
86
+ /** Slot audit (P6.14). Le 401 est posé par le firewall. */
87
+ onFailure(_context, _error) {
88
+ return Promise.resolve();
89
+ }
90
+ };
91
+ //#endregion
92
+ export { SessionAuthenticator, SessionAuthenticator as default };
@@ -0,0 +1,95 @@
1
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { ThrottledError } from "../../errors/ThrottledError.js";
3
+ import { UserToken } from "../token/UserToken.js";
4
+ //#region nodefony/src/authenticator/UserPasswordAuthenticator.ts
5
+ const BASIC_SCHEME = /^basic\s+/i;
6
+ const INVALID_CREDENTIALS = "Invalid credentials";
7
+ /**
8
+ * Authentification par identifiant + mot de passe — schéma **HTTP Basic**
9
+ * (RFC 7617) : `Authorization: Basic base64(identifiant:motdepasse)`, charset
10
+ * UTF-8, split au PREMIER `:` (le mot de passe peut en contenir).
11
+ *
12
+ * La vérification est déléguée au {@link IPasswordVerifier} (`UserService` par
13
+ * défaut) : hash, comparaison, leurre anti-timing et re-hash transparent restent
14
+ * derrière la frontière user — cet authenticator ne voit que le verdict.
15
+ *
16
+ * Le verifier est résolu **paresseusement** au premier login (cold path) : le
17
+ * boot ne paie rien et l'ordre de chargement des modules est indifférent.
18
+ *
19
+ * @remarks Le login par formulaire (body JSON) n'est PAS ici : il arrive avec la
20
+ * session BFF (`AuthController`, J3) qui appelle le verifier directement.
21
+ */
22
+ var UserPasswordAuthenticator = class {
23
+ name = "userpassword";
24
+ #verifier = null;
25
+ #resolveVerifier;
26
+ #throttler;
27
+ /**
28
+ * @param resolveVerifier - résolution lazy de la source de vérification
29
+ * (typiquement `container.get("users")`) — appelée au premier login.
30
+ * @param throttler - limiteur de tentatives (backoff NIST), `null` = désactivé.
31
+ */
32
+ constructor(resolveVerifier, throttler = null) {
33
+ this.#resolveVerifier = resolveVerifier;
34
+ this.#throttler = throttler;
35
+ }
36
+ /** La requête porte-t-elle un en-tête `Authorization: Basic ...` ? */
37
+ supports(context) {
38
+ const auth = context.request?.headers?.authorization;
39
+ return typeof auth === "string" && BASIC_SCHEME.test(auth);
40
+ }
41
+ /** Décode l'enveloppe Basic — un contenu malformé donne un credential vide (échec uniforme). */
42
+ createToken(context) {
43
+ const auth = context.request?.headers?.authorization;
44
+ const decoded = Buffer.from(auth.replace(BASIC_SCHEME, ""), "base64").toString("utf8");
45
+ const sep = decoded.indexOf(":");
46
+ const credentials = sep > 0 ? {
47
+ identifier: decoded.slice(0, sep),
48
+ password: decoded.slice(sep + 1)
49
+ } : {
50
+ identifier: "",
51
+ password: ""
52
+ };
53
+ return Promise.resolve(new UserToken(this.name, credentials));
54
+ }
55
+ /**
56
+ * Vérifie le credential via le verifier ou lève un 401 au message uniforme.
57
+ * Au succès le token est promu : utilisateur posé, credential effacé.
58
+ *
59
+ * Throttling NIST (si activé) : l'identifiant SAISI est vérifié AVANT le
60
+ * verifier (un identifiant bloqué ne coûte aucun hash → le throttle protège
61
+ * aussi le serveur du DoS argon2), échec compté, succès remis à zéro.
62
+ *
63
+ * @throws ThrottledError (429 + `Retry-After`) — backoff encore actif.
64
+ * @throws AuthenticationError (401) — credential absent ou invalide.
65
+ */
66
+ async authenticate(token) {
67
+ const credentials = token.getCredentials();
68
+ if (!credentials?.identifier || !credentials.password) throw new AuthenticationError(INVALID_CREDENTIALS);
69
+ if (this.#throttler !== null) {
70
+ const retryAfterS = this.#throttler.check(credentials.identifier);
71
+ if (retryAfterS > 0) throw new ThrottledError(retryAfterS);
72
+ }
73
+ const user = await (this.#verifier ??= this.#resolveVerifier()).authenticate(credentials.identifier, credentials.password);
74
+ if (user === null) {
75
+ this.#throttler?.recordFailure(credentials.identifier);
76
+ throw new AuthenticationError(INVALID_CREDENTIALS);
77
+ }
78
+ this.#throttler?.recordSuccess(credentials.identifier);
79
+ return token.promote(user);
80
+ }
81
+ /** Slot J3 (session BFF au login) — rien à poser pour du Basic pur. */
82
+ onSuccess(_context, _token) {
83
+ return Promise.resolve();
84
+ }
85
+ /** Slot J3+ (audit events). Le throttling vit dans `authenticate` (clé = identifiant) ; le 401 + challenge sont posés par le firewall. */
86
+ onFailure(_context, _error) {
87
+ return Promise.resolve();
88
+ }
89
+ /** Challenge RFC 7235 posé par le firewall sur les 401 de la zone. */
90
+ challenge() {
91
+ return "Basic realm=\"nodefony\", charset=\"UTF-8\"";
92
+ }
93
+ };
94
+ //#endregion
95
+ export { UserPasswordAuthenticator, UserPasswordAuthenticator as default };
@@ -0,0 +1,63 @@
1
+ import { AnonymousAuthenticator } from "./AnonymousAuthenticator.js";
2
+ import { SessionAuthenticator } from "./SessionAuthenticator.js";
3
+ import { UserPasswordAuthenticator } from "./UserPasswordAuthenticator.js";
4
+ import { JwtAuthenticator } from "./JwtAuthenticator.js";
5
+ import { ApiKeyAuthenticator } from "./ApiKeyAuthenticator.js";
6
+ import { ExternalJwtAuthenticator } from "./ExternalJwtAuthenticator.js";
7
+ import { resolveJwtRuntime } from "../token/jwtRuntime.js";
8
+ //#region nodefony/src/authenticator/authenticatorRegistry.ts
9
+ const factories = /* @__PURE__ */ new Map();
10
+ /**
11
+ * Enregistre (ou remplace) la fabrique d'un authenticator. Appelé par les
12
+ * builtins au chargement du module, et par les plugins pour les leurs
13
+ * (LDAP, SSO maison...).
14
+ */
15
+ function registerAuthenticatorFactory(name, factory) {
16
+ factories.set(name, factory);
17
+ }
18
+ /** Fabrique d'un authenticator par nom, ou `undefined` si inconnu. */
19
+ function getAuthenticatorFactory(name) {
20
+ return factories.get(name);
21
+ }
22
+ /** Noms enregistrés (validation boot, introspection Studio, tests). */
23
+ function listAuthenticatorFactories() {
24
+ return [...factories.keys()];
25
+ }
26
+ registerAuthenticatorFactory("anonymous", () => new AnonymousAuthenticator());
27
+ registerAuthenticatorFactory("userpassword", ({ container }) => {
28
+ const throttler = container.get("loginThrottler") ?? null;
29
+ return new UserPasswordAuthenticator(() => {
30
+ const verifier = container.get("users");
31
+ if (!verifier) throw new Error("UserPasswordAuthenticator: aucun service \"users\" (IPasswordVerifier) dans le container — enregistrer un UserService au boot de l'application.");
32
+ return verifier;
33
+ }, throttler);
34
+ });
35
+ registerAuthenticatorFactory("session", ({ container }) => {
36
+ return new SessionAuthenticator(() => {
37
+ const provider = container.get("users");
38
+ if (!provider) throw new Error("SessionAuthenticator: aucun service \"users\" (IUserProvider) dans le container — enregistrer un UserService au boot de l'application.");
39
+ return provider;
40
+ });
41
+ });
42
+ registerAuthenticatorFactory("jwt", ({ container, config }) => {
43
+ return new JwtAuthenticator(container, resolveJwtRuntime(config.jwt));
44
+ });
45
+ registerAuthenticatorFactory("external-jwt", ({ container, config }) => {
46
+ const rs = config.resourceServer;
47
+ return new ExternalJwtAuthenticator(container, {
48
+ issuers: rs.issuers.map((i) => ({
49
+ issuer: i.issuer,
50
+ subjectMapping: i.subjectMapping
51
+ })),
52
+ subjectPolicy: rs.subjectPolicy,
53
+ ephemeralRoles: rs.ephemeralRoles
54
+ });
55
+ });
56
+ registerAuthenticatorFactory("apikey", ({ container, config }) => {
57
+ return new ApiKeyAuthenticator(container, {
58
+ prefix: config.apiKeys.prefix,
59
+ lastUsedThrottleS: config.apiKeys.lastUsedThrottleS
60
+ });
61
+ });
62
+ //#endregion
63
+ export { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthenticatorFactory };
@@ -0,0 +1,2 @@
1
+ import { bearerToken } from "nodefony";
2
+ export { bearerToken };
@@ -0,0 +1,36 @@
1
+ //#region nodefony/src/authenticator/externalSubject.ts
2
+ /**
3
+ * Séparateur entre l'émetteur et le sujet.
4
+ *
5
+ * `#` est choisi parce qu'un identifiant d'émetteur ne peut PAS en contenir :
6
+ * la RFC 8414 §2 interdit le fragment dans un `issuer`. La composition est donc
7
+ * **injective** — deux paires `(iss, sub)` distinctes ne peuvent jamais produire
8
+ * le même identifiant local, et c'est précisément la propriété qui empêche un
9
+ * émetteur d'usurper le sujet d'un autre.
10
+ */
11
+ const SEPARATOR = "#";
12
+ /**
13
+ * Compose l'identifiant local qui désigne le sujet d'un émetteur externe.
14
+ *
15
+ * 🔴 **Un `sub` seul ne désigne personne.** OpenID Connect Core §2 ne garantit
16
+ * son unicité et sa non-réattribution que *dans l'espace de son émetteur*.
17
+ * Chercher un compte local directement par `sub` verse donc des identifiants
18
+ * étrangers dans l'espace local : il suffit d'un annuaire où l'utilisateur
19
+ * choisit son identifiant — beaucoup le permettent — pour présenter
20
+ * `sub: "admin"` et se voir rattacher au compte local du même nom.
21
+ *
22
+ * C'est pour cela que `prefixed` est le défaut et que `subject` se déclare :
23
+ * le mode sûr ne doit rien demander, le mode qui fait confiance doit être écrit.
24
+ *
25
+ * @param issuer - émetteur VÉRIFIÉ, sous sa forme canonique (jamais la valeur
26
+ * brute lue dans le jeton — elle est choisie par le porteur)
27
+ * @param subject - sujet du jeton (`sub`)
28
+ * @param mapping - politique déclarée pour CET émetteur
29
+ * @returns l'identifiant à chercher dans l'annuaire local
30
+ */
31
+ function localIdentifierFor(issuer, subject, mapping) {
32
+ if (mapping === "subject") return subject;
33
+ return `${issuer}${SEPARATOR}${subject}`;
34
+ }
35
+ //#endregion
36
+ export { localIdentifierFor };