@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,546 @@
1
+ ---
2
+ title: "Firewall — le pare-feu applicatif"
3
+ navTitle: Firewall
4
+ lang: fr
5
+ module: "@nodefony/security"
6
+ topic: firewall
7
+ coverageModule: security
8
+ coverageFiles: "firewall"
9
+ section: "Sécurité"
10
+ audience: [developer]
11
+ tags:
12
+ [
13
+ firewall,
14
+ securite,
15
+ authentification,
16
+ authenticators,
17
+ zones,
18
+ zero-trust,
19
+ csrf,
20
+ cors,
21
+ csp,
22
+ ]
23
+ version: "doc"
24
+ status: stable
25
+ updated: 2026-07-19
26
+ source: "src/packages/@nodefony/security/docs/firewall.md"
27
+ ---
28
+
29
+ # Firewall — le pare-feu applicatif
30
+
31
+ > Pour **chaque** requête (HTTP comme WebSocket), le firewall répond à trois questions dans l'ordre :
32
+ > est-ce une zone protégée ? qui es-tu ? as-tu le droit ? La politique par défaut est **Zero Trust** :
33
+ > sur une zone protégée, pas de preuve d'identité valide = 401. Ancré sur
34
+ > `src/packages/@nodefony/security/nodefony/service/firewall.ts` et les authenticators de
35
+ > `nodefony/src/authenticator/`.
36
+
37
+ 📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Firewall**
38
+
39
+ ## 🧠 Le modèle mental — chemin chaud, chemin froid
40
+
41
+ Le firewall sépare **détecter** (chaud, sur chaque requête) et **décider** (froid, seulement zone
42
+ protégée) — pour ne pas payer l'authentification sur les routes publiques.
43
+
44
+ ```mermaid
45
+ flowchart TD
46
+ R["Requête HTTP / WS"] --> IS{"isSecure()<br/>zone protégée ?"}
47
+ IS -->|non| PASS["passe (public)"]
48
+ IS -->|oui| AU["#authenticate()<br/>authenticators de la zone, dans l'ordre"]
49
+ AU -->|ThrottledError| T429["429 + Retry-After"]
50
+ AU -->|credential invalide| C401["401 + challenge"]
51
+ AU -->|aucune preuve| Z401["401 (Zero Trust)"]
52
+ AU -->|succès| OK["user + token dans l'ALS → contrôleur"]
53
+ ```
54
+
55
+ `Firewall.isSecure()` (`firewall.ts:705`) rattache la requête à une **zone** via
56
+ `Firewall.matchPath()` (`firewall.ts:696`) ; `Firewall.handleSecurity()` (`firewall.ts:738`) décide.
57
+ Les zones sont triées par **spécificité** dans `#build()` — `list.sort` par longueur de motif :
58
+ le plus long gagne, pas le premier déclaré (`firewall.ts:191`).
59
+
60
+ ## 📖 Lexique
61
+
62
+ | Terme | Sens |
63
+ | ------------- | ------------------------------------------------------------------------------- |
64
+ | Zone | Un motif d'URL (+ host) avec sa politique (`config.areas`, un objet par nom). |
65
+ | Authenticator | Une stratégie d'identification (session, userpassword, jwt, apikey, anonymous). |
66
+ | Zero Trust | Sans preuve valide sur une zone protégée → 401. |
67
+ | Challenge | En-tête `WWW-Authenticate` (RFC 7235) qui dit comment s'authentifier. |
68
+ | BFF | Backend-For-Frontend : le serveur gère session/jetons pour le front web. |
69
+ | PAT | Personal Access Token : une clé d'API opaque, révocable côté serveur. |
70
+ | Bearer | Schéma `Authorization: Bearer <token>` (RFC 6750). |
71
+
72
+ ## 🚀 Démarrage rapide
73
+
74
+ ### Dans une app `nodefony create app`, le firewall est DÉJÀ actif
75
+
76
+ Le scaffold déclare deux zones dans `nodefony.config.ts` — c'est la forme canonique (un **objet par
77
+ nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts:902`) :
78
+
79
+ ```typescript
80
+ // nodefony.config.ts (extrait généré par `nodefony create app`)
81
+ use("@nodefony/security", {
82
+ areas: {
83
+ // Zone de TES routes : `session` PUIS `anonymous` → identifié si cookie,
84
+ // sinon visiteur accepté. Hors zone, l'identité n'est JAMAIS résolue.
85
+ main: {
86
+ pattern: "^/api",
87
+ authenticators: ["session", "anonymous"],
88
+ },
89
+ // Zone PROTÉGÉE — pattern PLUS SPÉCIFIQUE que ^/api : le firewall trie
90
+ // par longueur → /api/secure/* tombe ICI. Pas d'`anonymous` : sans
91
+ // session → 401 AVANT ton controller (Zero Trust).
92
+ secure: {
93
+ pattern: "^/api/secure",
94
+ authenticators: ["session"],
95
+ },
96
+ },
97
+ roleHierarchy: {
98
+ ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
99
+ ROLE_ADMIN: ["ROLE_USER"],
100
+ },
101
+ });
102
+ ```
103
+
104
+ > [!IMPORTANT]
105
+ > **Hors zone, l'identité n'est JAMAIS résolue** — même connecté, une route non couverte par une
106
+ > zone ne sait pas qui tu es. Une route « publique » qui veut connaître l'utilisateur se couvre
107
+ > par `["session", "anonymous"]`.
108
+
109
+ **Le login est FOURNI** : le module security expose le BFF `POST /nodefony/security/api/auth/login`
110
+ (body `{ username, password }` → `Set-Cookie` de session ; `AuthFlow.login` régénère l'ID de session
111
+ — anti-fixation OWASP). Pas de LoginController à écrire.
112
+
113
+ ### Ce que TU écris : le controller protégé
114
+
115
+ ```typescript
116
+ // nodefony/controllers/AccountController.ts — complet, compile tel quel
117
+ import {
118
+ controller,
119
+ Controller,
120
+ Get,
121
+ IsGranted,
122
+ CurrentUser,
123
+ } from "@nodefony/framework";
124
+ import type { ContextType } from "@nodefony/http";
125
+ import type { IUser } from "@nodefony/user";
126
+
127
+ @controller("/api/secure/account")
128
+ class AccountController extends Controller {
129
+ // Zone `secure` : context.user est GARANTI ici (le firewall a authentifié).
130
+ // @IsGranted ajoute l'AUTORISATION : il faut aussi le rôle.
131
+ @IsGranted(["ROLE_USER"])
132
+ @Get("/me")
133
+ async me(@CurrentUser() user: IUser) {
134
+ // identité ré-résolue à chaque requête → rôles frais, révocation immédiate
135
+ return this.renderJson({ identifier: user.identifier, roles: user.roles });
136
+ }
137
+ }
138
+
139
+ export default AccountController;
140
+ ```
141
+
142
+ (Wiring : `@controllers([AccountController])` dans le module de l'app — `nodefony create controller`
143
+ le fait pour toi.)
144
+
145
+ ### Ce qu'on observe
146
+
147
+ ```bash
148
+ # 1) Sans session : Zero Trust → 401 (aucun code à toi n'a tourné)
149
+ curl -si http://localhost:5151/api/secure/account/me | head -1
150
+ # HTTP/1.1 401 Unauthorized
151
+
152
+ # 2) Login BFF (compte dev seedé admin/admin) → cookie de session
153
+ curl -si -c /tmp/jar -H 'Content-Type: application/json' \
154
+ -d '{"username":"admin","password":"admin"}' \
155
+ http://localhost:5151/nodefony/security/api/auth/login | head -1
156
+ # HTTP/1.1 200 OK
157
+
158
+ # 3) Rejouer avec le cookie → 200, identité résolue
159
+ curl -s -b /tmp/jar http://localhost:5151/api/secure/account/me
160
+ # {"identifier":"admin","roles":["ROLE_NODEFONY_ADMIN", …]}
161
+ ```
162
+
163
+ ### Protéger une API machine (jwt et/ou apikey)
164
+
165
+ ```typescript
166
+ use("@nodefony/security", {
167
+ areas: {
168
+ // jwt et apikey cohabitent : discriminés par la FORME du bearer (voir plus bas)
169
+ api: {
170
+ pattern: "^/api/v1",
171
+ authenticators: ["jwt", "apikey"],
172
+ mode: "first",
173
+ },
174
+ },
175
+ });
176
+ ```
177
+
178
+ Le client envoie l'un ou l'autre :
179
+
180
+ ```
181
+ Authorization: Bearer eyJhbGciOiJFZERTQS␣…␣.␣…␣.␣… # un JWT (structure a.b.c)
182
+ Authorization: Bearer nf_9a2c… # une clé API (préfixe nf_)
183
+ ```
184
+
185
+ ## 🔐 Les authenticators intégrés
186
+
187
+ Tous respectent le **même contrat** (`IAuthenticator`) : `supports(context)` (test bon marché : la
188
+ requête porte-t-elle ce type de credential ?), `createToken()` (extrait le credential brut),
189
+ `authenticate(token)` (valide + promeut, ou lève un 401), `challenge()` (l'en-tête `WWW-Authenticate`).
190
+ Point commun de sécurité : **message d'échec uniforme** (`"Invalid token"` / `"Invalid credentials"`)
191
+ — la cause fine (expiré, révoqué, sujet banni…) part dans l'audit, jamais au client (anti-énumération).
192
+
193
+ | Nom | Credential | Vérité | Révocable | Pour… |
194
+ | -------------- | --------------------------------------- | ---------- | :-------: | --------------------------------- |
195
+ | `session` | cookie de session (identifiant en blob) | serveur | immédiate | le **web** après login (BFF) |
196
+ | `userpassword` | `Authorization: Basic base64(id:mdp)` | verifier | n/a | outils/scripts, brique login |
197
+ | `jwt` | `Authorization: Bearer <a.b.c>` | auto-porté | via état | API service↔service, agents |
198
+ | `apikey` | `Authorization: Bearer <prefix>_…` | serveur | immédiate | API/CI/scripts d'un user |
199
+ | `anonymous` | (aucun) | — | — | accepter l'anonymat explicitement |
200
+
201
+ ### `session` — la preuve du web après login
202
+
203
+ Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
204
+
205
+ - **N'ouvre jamais la session lui-même** : il exige une session reprise portant un user
206
+ (`supports()`, `SessionAuthenticator.ts:43`). C'est `AuthFlow.login()` (BFF) qui ouvre et
207
+ régénère l'ID (anti-fixation).
208
+ - **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts:70`) → rôles frais,
209
+ révocation et verrouillage effectifs immédiatement.
210
+ - **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login
211
+ (pas de popup Basic).
212
+
213
+ ### `userpassword` — HTTP Basic, avec throttle NIST
214
+
215
+ Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617, split au **premier**
216
+ `:` (`UserPasswordAuthenticator.ts:74`).
217
+
218
+ - **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
219
+ leurre anti-timing, re-hash. L'authenticator ne voit que le verdict.
220
+ - **Le throttle NIST SP 800-63B passe AVANT le verifier** (`UserPasswordAuthenticator.ts:101`) :
221
+ un identifiant bloqué ne coûte **aucun hash argon2** — protège d'un DoS par hachage.
222
+ Échec → backoff ; `ThrottledError` → **429 + `Retry-After`**.
223
+ - Challenge : `Basic realm="nodefony"`.
224
+ - **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
225
+ (`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
226
+
227
+ ### `jwt` — Bearer JWT signé, durci RFC 8725
228
+
229
+ Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts:14`).
230
+ Réservé API service↔service / agents (le web reste sur la session). Access token **EdDSA** signé par
231
+ le keystore du serveur. Défenses **dures**, prouvées en test (RFC 8725 JWT BCP), toutes dans
232
+ `JwtAuthenticator.authenticate()` :
233
+
234
+ - **allowlist d'algorithmes** `["EdDSA"]` — l'algo n'est **jamais** choisi d'après l'en-tête du
235
+ token ; `alg=none` rejeté (`JwtAuthenticator.ts:120`).
236
+ - **clé par `kid` depuis le JWKS LOCAL** (`createLocalJWKSet`) — jamais `jku`/`jwk` de l'en-tête
237
+ (anti-injection de clé / SSRF, `JwtAuthenticator.ts:155`).
238
+ - **`aud` + `iss` obligatoires** + `typ:"at+jwt"` (un refresh présenté comme access est rejeté) +
239
+ exp/nbf (`JwtAuthenticator.ts:105-108`).
240
+ - **révocation** malgré l'auto-portage : denylist `jti` + `invalidBefore` par sujet
241
+ (`JwtAuthenticator.ts:122-132`).
242
+ - **sujet revérifié** à réception (`loadUserByIdentifier(sub)`) : compte disparu/inactif/verrouillé
243
+ = rejet (`JwtAuthenticator.ts:174-187`).
244
+
245
+ Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts:162-172`).
246
+
247
+ > [!WARNING]
248
+ > Un JWT est **auto-porté** : sans état serveur il n'est **pas** révocable. C'est la denylist
249
+ > `jti` + `invalidBefore` (état serveur) qui le rend révocable — vérifie que ton `tokenStore`
250
+ > les porte.
251
+
252
+ ### `apikey` — clé d'API opaque (PAT), révocable
253
+
254
+ Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts:67`). Contrairement au JWT,
255
+ c'est un **bearer opaque** : sa vérité vit côté serveur (`ITokenStore`) → **révocable immédiatement**.
256
+
257
+ Défenses de `ApiKeyAuthenticator.authenticate()` :
258
+
259
+ - **forme + CRC validés AVANT tout accès au store** — anti-DoS (`parseApiKey()`,
260
+ `ApiKeyAuthenticator.ts:98`) ;
261
+ - lookup par **hash sha256** : le secret n'existe nulle part au repos (`:105`) ;
262
+ - révocation (`revokedAt`), expiration (`expiresAt`), **ban en masse** du porteur
263
+ (`invalidBefore` vs `createdAt`, `:117-120`) ;
264
+ - **sujet revérifié** à chaque requête — rôles frais (`:122`) ;
265
+ - `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`:127-134`).
266
+
267
+ Le token porte `scopes`, `apiKeyId`, `tenantId` (`:138-140`). `jwt` et `apikey` **cohabitent** dans
268
+ une zone : ils se discriminent par la forme (JWT = `a.b.c`, PAT = `prefix_…`).
269
+
270
+ ### `anonymous` — accepter l'anonymat, explicitement
271
+
272
+ Le **seul** authenticator qui produit un token non authentifié **sans** déclencher le Zero Trust
273
+ (`AnonymousAuthenticator.ts:6-18`). À lister **volontairement** : `["jwt", "anonymous"]` en mode
274
+ `first` = « identifié si preuve présente, sinon **visiteur anonyme accepté** ». En mode `all`, utile
275
+ en dernier : « le canal doit être prouvé (ex. mTLS), l'identité utilisateur est optionnelle ». Coût
276
+ nul : `supports()` accepte tout, le token porte le singleton gelé `anonymousUser` (0 allocation).
277
+ Sans lui, zone protégée + aucune preuve = 401.
278
+
279
+ ### `firewall-realtime` — l'identité du firewall, côté WebSocket (câblé auto)
280
+
281
+ Il promeut en jeton realtime **toute** identité que le firewall a résolue — session BFF comme jeton
282
+ porteur (JWT, clé d'API). **Enregistré automatiquement** par `Firewall.#wireRealtime()` au
283
+ handshake des zones protégées `realtime` (`firewall.ts:289`).
284
+
285
+ - **Perf : il ne relit pas la base.** Handshake et frames tournent dans la même bulle ALS —
286
+ l'identité déjà posée est réutilisée, 2 lectures base économisées par connexion
287
+ (`FirewallRealtimeAuthenticator.ts:24-30`).
288
+ - **Asymétrie HTTP↔WS assumée** : le jeton est **figé au handshake** (les frames lisent un cache
289
+ O(1)) → une révocation prend effet **en une fenêtre** (tick du hub, et devant chaque
290
+ `api.request`), pas à la frame suivante (`FirewallRealtimeAuthenticator.ts:51-55`). C'est l'état
291
+ de l'art (Socket.IO et Phoenix figent aussi).
292
+ - **Filet** : un revalidator re-lit la session avant chaque action data plane ; fail-closed →
293
+ fermeture 4001.
294
+
295
+ ## 🗝️ `stateless` — la zone tient-elle un registre ?
296
+
297
+ Une zone dit, par ce drapeau, **où vit l'identité** — et ce n'est pas la même chose que la liste de
298
+ ses authentificateurs.
299
+
300
+ | Valeur | Ce que la zone fait | Pour qui |
301
+ | ---------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
302
+ | `false` (défaut) | la zone **peut** tenir un registre serveur : session créée au login, cookie opaque révocable | un **navigateur** (modèle BFF) |
303
+ | `true` | la zone n'ouvre **ni ne reprend** de session — le cookie entrant est ignoré, aucun `Set-Cookie` n'est renvoyé | un **porteur de preuve** (clé, jeton) |
304
+
305
+ Ce que `true` évite concrètement : sans lui, un appelant qui envoie un cookie inconnu — un client
306
+ qui recycle un en-tête, un navigateur qui traîne une vieille session, un attaquant qui en fabrique
307
+ un — fait **reprendre puis réécrire une session serveur** et repartir un `Set-Cookie`, y compris
308
+ quand la réponse est un 401. Un registre pour quelqu'un qui ne le relira jamais.
309
+
310
+ > 🔴 **`stateless: true` et `"session"` dans la même zone est une contradiction, et l'application
311
+ > REFUSE de démarrer** en la nommant (`SessionAuthenticator.validateArea`). Une zone sert un
312
+ > navigateur **ou** un porteur de preuve. Si les deux publics doivent atteindre la même
313
+ > fonctionnalité, ce sont **deux zones** — le firewall trie par longueur de motif, donc la plus
314
+ > spécifique gagne.
315
+
316
+ Une **route** qui demande une session (`@UseSession`) sous une zone stateless ne l'emporte pas : la
317
+ zone est la déclaration de sécurité, elle gagne, et le journal le dit une fois par zone au lieu de
318
+ laisser chercher pourquoi `context.session` est nul.
319
+
320
+ ## ⚙️ Ordre et modes (`mode: "first"` vs `"all"`)
321
+
322
+ La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
323
+ (`firewall.ts:1112`). Le `mode` dit comment la parcourir. Trois situations concrètes :
324
+
325
+ ### Situation 1 — humains ET machines sur la même API (`first`, le mode courant)
326
+
327
+ Ton back-office est appelé par le **navigateur** des utilisateurs connectés ET par un **script CI**.
328
+ Deux preuves différentes, mêmes routes :
329
+
330
+ ```typescript
331
+ back: {
332
+ pattern: "^/api/back",
333
+ authenticators: ["session", "apikey"],
334
+ mode: "first", // (défaut) le PREMIER qui reconnaît la requête authentifie
335
+ },
336
+ ```
337
+
338
+ Ce qui se passe, requête par requête :
339
+
340
+ <!-- prettier-ignore -->
341
+ | Le client envoie… | `supports()` vrai pour… | Résultat |
342
+ | --- | --- | --- |
343
+ | le cookie de session | `session` | identifié, `apikey` jamais consulté |
344
+ | `Authorization: Bearer nf_…` | `apikey` | identifié (session ne matche pas, on passe) |
345
+ | une clé **révoquée** `nf_…` | `apikey` | **401 direct** — l'échec d'`authenticate()` remonte, pas de fallback (`firewall.ts:1112`) |
346
+ | rien | aucun | **401** (Zero Trust) |
347
+
348
+ ### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
349
+
350
+ Tu veux « identifié si connecté, sinon visiteur » :
351
+
352
+ ```typescript
353
+ authenticators: ["session", "anonymous"], // ✅ session d'abord
354
+ authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le monde
355
+ ```
356
+
357
+ `AnonymousAuthenticator.supports()` accepte **toutes** les requêtes — placé en premier en mode
358
+ `first`, il court-circuite la liste : **personne n'est jamais identifié**, même avec un cookie
359
+ valide. L'ordre est ta politique.
360
+
361
+ ### Situation 3 — le « sudo mode » (`all` : empiler les preuves)
362
+
363
+ Une action destructrice (suppression de compte, rotation des clés) doit exiger la session **ET**
364
+ une re-saisie du mot de passe — même logique que GitHub avant une action sensible :
365
+
366
+ ```typescript
367
+ danger: {
368
+ pattern: "^/api/back/danger",
369
+ authenticators: ["session", "userpassword"],
370
+ mode: "all", // CHAQUE maillon est obligatoire
371
+ },
372
+ ```
373
+
374
+ Le client doit présenter **les deux preuves** dans la même requête (cookie + `Authorization:
375
+ Basic …`). Une seule manque → 401. Le **dernier** token de la chaîne porte l'identité
376
+ (`firewall.ts:936-939`) — ici la preuve mot de passe, la plus fraîche.
377
+
378
+ > [!TIP]
379
+ > Un nom d'authenticator inconnu en config **fait échouer le boot** —
380
+ > `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts:402`) : jamais de zone
381
+ > « protégée » silencieusement ouverte à cause d'une faute de frappe.
382
+
383
+ ## 🧑‍⚖️ Autorisation — rôles, scopes, voters (« as-tu le droit ? »)
384
+
385
+ L'authentification dit **qui** tu es ; l'autorisation dit **ce que tu peux faire**. On déclare
386
+ l'exigence sur l'action, un **jury de voters** tranche. La garde s'applique **avant l'instanciation
387
+ du contrôleur** (seam Resolver) — une action protégée ne s'exécute jamais pour un non-autorisé.
388
+
389
+ ```typescript
390
+ @IsGranted(["ROLE_ADMIN"]) // rôle — OR interne : un seul attribut suffit
391
+ @Post("/users") async create() {}
392
+
393
+ @RequireScope("users:write") // scope — pour une clé/JWT délégué
394
+ @Delete("/users/{id}") async remove(@Param("id") id: string) {}
395
+
396
+ @IsGranted("doc.edit", { subject: "id" }) // règle métier — le param de route `id` est passé au voter
397
+ @Put("/docs/{id}") async edit() {}
398
+ ```
399
+
400
+ ### Le jury et sa stratégie
401
+
402
+ `Authorization.decide(token, attribut, subject?)` (`service/authorization.ts:70`) applique une
403
+ stratégie **affirmative + DENY veto**, fermée par défaut (**Zero Trust**) :
404
+
405
+ - un seul **`DENY`** bloque (veto, court-circuit — inutile de finir le jury,
406
+ `authorization.ts:94-97`) ;
407
+ - sinon un **`GRANT`** suffit ;
408
+ - **silence total** (tous `ABSTAIN`, ou aucun voter compétent) → **`DENY`**
409
+ (`authorization.ts:100-108`) ;
410
+ - un voter qui **throw** → **`DENY`** + log ERROR (fail-closed : jamais 500, jamais octroi,
411
+ `authorization.ts:85-93`).
412
+
413
+ Tout refus est audité (WARNING + `recordAudit`, `authorization.ts:113-142`) ; les octrois restent
414
+ muets (volume, pas un signal). Les voters sont instanciés **une fois au boot** via le registre
415
+ (aucun nom en dur, `authorization.ts:55-64`).
416
+
417
+ ### Les voters intégrés — deux axes
418
+
419
+ - **RoleVoter** (`role`, attributs `ROLE_*`) — `GRANT` si l'utilisateur a le rôle, **hiérarchie
420
+ résolue** ; **`ABSTAIN` sinon**, jamais `DENY` (`RoleVoter.vote()`, `RoleVoter.ts:25-39`).
421
+ Constat : l'absence d'un rôle ne doit pas opposer son veto aux autres axes — c'est le
422
+ **default-DENY du jury** qui ferme la porte, pas ce voter. C'est ce qui rend une clause OR
423
+ (`@IsGranted(["A","B"])`) possible.
424
+ - **ScopeVoter** (`scope`, attributs `api:action`) — un scope **ne bride jamais un humain**
425
+ (`ScopeVoter.ts:17-62`) :
426
+ - jeton humain (`session`/`userpassword`/`anonymous`) → `GRANT` no-op : l'autorisation d'un
427
+ humain passe par ses **rôles** ;
428
+ - jeton **machine délégué** (`apikey`/`jwt`/`oauth2`) → `GRANT` si le scope exact est présent,
429
+ `ABSTAIN` sinon ;
430
+ - **fail-closed côté machine** : tout type de jeton hors de la liste « non scopable » — présent
431
+ ou futur (`mtls`, `agent`…) — est traité comme scopable, donc **bridé par défaut**.
432
+ - En une ligne : rôles = qui tu es ; scopes = ce qu'une **clé** a le droit de faire.
433
+
434
+ ### La hiérarchie de rôles
435
+
436
+ `RoleHierarchyWalker` (`src/RoleHierarchyWalker.ts`) : `ROLE_ADMIN` hérite `ROLE_USER`, etc.
437
+ **Aplatissement précalculé au boot** → `hasRole()` est O(1) sur le hot path
438
+ (`RoleHierarchyWalker.ts:23-30`), et les **cycles sont détectés au boot** (throw avec le chemin
439
+ complet, pas de fail-silent, `RoleHierarchyWalker.ts:69-95`). La hiérarchie est posée au container
440
+ par le firewall au boot ; le `RoleVoter` la lit en lazy.
441
+
442
+ ### Voters métier (le vrai pouvoir applicatif)
443
+
444
+ Pour une règle qui dépend des **données** (ownership, tenant, état), on enregistre une fabrique :
445
+
446
+ ```typescript
447
+ import { registerVoterFactory } from "@nodefony/security";
448
+
449
+ registerVoterFactory(
450
+ "projectVoter",
451
+ ({ container }) => new ProjectVoter(container),
452
+ );
453
+ // ProjectVoter.supports("doc.edit") → true ; vote(token, "doc.edit", subject) → lookup DB async :
454
+ // l'utilisateur est-il propriétaire/membre du `subject` ? GRANT / DENY / ABSTAIN.
455
+ ```
456
+
457
+ Le voter est **découvert automatiquement** par l'`AuthorizationService` — aucun changement dans le
458
+ cœur (`registerVoterFactory()`, `voterRegistry.ts:40`). Pourquoi un registre et pas un scan DI des
459
+ `@injectable` : les interfaces TS sont **effacées à la compilation** — rien à scanner ; le registre
460
+ **est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts:6-16`). Trois axes (rôles,
461
+ scopes, métier), un même jury, combinables.
462
+
463
+ ## 🔌 HTTP et WebSocket — le même firewall
464
+
465
+ `Firewall.#wireRealtime()` (`firewall.ts:268`) câble, pour toute zone protégée `realtime !== false`
466
+ (opt-out, `firewall.ts:277`), le `FirewallRealtimeAuthenticator` au handshake (`firewall.ts:289`)
467
+ **et** un `frameAuthorizer` (RBAC par canal, `firewall.ts:337`). Même résolution de zone que HTTP.
468
+ Sur une socket, un refus n'a pas d'en-tête `WWW-Authenticate` (`Firewall.#setChallenge()`,
469
+ `firewall.ts:1191`) : le **code de fermeture** suffit.
470
+
471
+ ## 🛡️ En-têtes de sécurité, CSRF, CORS
472
+
473
+ - **`Firewall.applySecurityHeaders()`** (`firewall.ts:1029`) : CSP, Referrer-Policy, COOP/COEP/CORP
474
+ au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts:855`) :
475
+ alloué seulement si une directive en a besoin.
476
+ - **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts:932`) : Fetch Metadata
477
+ (`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts:764`), puis double-submit `x-csrf-token` ≡
478
+ cookie + HMAC (`firewall.ts:778`).
479
+ - **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts:991`).
480
+
481
+ ## 📜 Normes appliquées
482
+
483
+ | Domaine | Norme | Ancrage |
484
+ | ---------------------- | --------------- | ------------------------------------------------------ |
485
+ | Challenge d'auth (401) | RFC 7235 | `Firewall.#setChallenge()` (`firewall.ts:1191`) |
486
+ | Bearer | RFC 6750 | `JwtAuthenticator.ts:13` · `ApiKeyAuthenticator.ts:11` |
487
+ | JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts:33-44,104-108` |
488
+ | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts:10-28` |
489
+ | Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts:764`) |
490
+ | Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts:43-46,101-104` |
491
+ | CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts:932`) |
492
+ | Modèle | Zero Trust | `firewall.ts:611` (aucune preuve → 401) |
493
+
494
+ ## ⚡ Performance & mémoire
495
+
496
+ Le découpage chaud/froid EST l'optimisation : `isSecure()` (chaque requête) ne fait qu'un
497
+ `matchPath` ; `handleSecurity()` (throttler, authenticators, nonce CSP, `securityTrace`) n'est payé
498
+ que sur zone protégée. Les dépendances des authenticators (keystore, tokenStore, userProvider,
499
+ verifier, jose) sont résolues **paresseusement** au premier usage (cold path) ; `jose` est importé
500
+ lazy (dep lourde). Le nonce CSP et le `securityTrace` sont alloués à la demande. Une route publique
501
+ ne paie quasiment rien.
502
+
503
+ ## ⚠️ Pièges (symptôme → cause → correction)
504
+
505
+ | Symptôme | Cause (dans le code) | Correction |
506
+ | ---------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------- |
507
+ | Boot rejette la config (`areas`) | `areas` déclaré en **tableau** — c'est un objet par nom | `areas: { monNom: { pattern, authenticators } }` |
508
+ | Boot « authenticator inconnu » | Nom absent du registre (fail-closed) | Corriger le nom / enregistrer l'authenticator |
509
+ | 401 alors qu'un credential est envoyé | Mode `first` : credential invalide échoue sans fallback | Vérifier le format/authenticator attendu |
510
+ | Route « publique » ne voit jamais l'user | Hors zone, l'identité n'est **jamais** résolue | Couvrir la route par une zone `["session", "anonymous"]` |
511
+ | API : JWT et clé API se marchent dessus | — | Rien à faire : discriminés par la forme (`a.b.c` vs `prefix_…`) |
512
+ | JWT révoqué encore accepté | Auto-portage : révocation = état serveur | S'assurer que `tokenStore` porte la denylist/`invalidBefore` |
513
+ | WS : révocation pas immédiate | Jeton figé au handshake (asymétrie assumée) | Effet à la reconnexion ; pour l'immédiat, canal JWT (J4) |
514
+ | 429 au login | Throttle NIST (backoff par identifiant) | Respecter `Retry-After` ; attendu sous attaque |
515
+
516
+ ## 📡 Observabilité — Studio
517
+
518
+ Écran **Firewall** (`/nodefony/firewall`) : zones, authenticators, décisions (`securityTrace`).
519
+ Écran **Roles** (`/nodefony/roles`) : hiérarchie de rôles consommée par les voters. Écran
520
+ **ApiKeys** (`/nodefony/api-keys`) : gestion et révocation des PAT.
521
+
522
+ ## 🧪 Tests & couverture
523
+
524
+ Quatre familles couvrent la brique — les **chiffres exacts vivent dans la carte de l'aperçu**
525
+ (régénérée par `gen-counters.mjs` depuis vitest, jamais figée ici) :
526
+
527
+ - **unit** : `firewallChain` (la chaîne d'authenticators + modes first/all), `securedArea` (match
528
+ pattern/host), `firewallIntrospection` (l'écran Studio), `firewallSecurityTrace` (la radiographie
529
+ de décision) ;
530
+ - **intégration** : `firewall-auth` (serveur réel : zones + login BFF), `securityGuard` (la garde
531
+ `@IsGranted` au Resolver) ;
532
+ - **e2e** : `realtimeFirewallWiring` (le câblage WS réel) ;
533
+ - **attaque** : les bancs transverses (csrf, cors, authorization, frames WS) exercent le firewall en
534
+ conditions hostiles — voir [authenticators](./authenticators.md) et
535
+ [authorization](./authorization.md).
536
+
537
+ Couverture : `npm run coverage` dans `@nodefony/security`.
538
+
539
+ ## 🔗 Pour aller plus loin
540
+
541
+ - ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
542
+ - 🧭 **Pages sœurs** : [Authenticators](authenticators.md) · [Autorisation](authorization.md)
543
+
544
+ - Vue du module → [index](./index.md) · Autorisation (voters, rôles, scopes) → [authorization](./authorization.md)
545
+ - JWT/OAuth2/WebAuthn/TOTP/API keys en détail → pages dédiées du module
546
+ - Où le firewall s'insère → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)