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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/index.js +48 -16
  3. package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
  4. package/dist/nodefony/command/security-oauth-doctor.js +68 -0
  5. package/dist/nodefony/command/security-secrets.js +52 -29
  6. package/dist/nodefony/command/security-user-add.js +9 -3
  7. package/dist/nodefony/command/security-user-delete.js +10 -7
  8. package/dist/nodefony/command/security-user-list.js +5 -3
  9. package/dist/nodefony/command/security-user-password.js +9 -14
  10. package/dist/nodefony/config/config.js +99 -8
  11. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  12. package/dist/nodefony/contracts/IAccessVoter.js +1 -2
  13. package/dist/nodefony/contracts/index.js +1 -2
  14. package/dist/nodefony/errors/AccessDeniedError.js +1 -2
  15. package/dist/nodefony/errors/ApiKeyError.js +3 -2
  16. package/dist/nodefony/errors/AuthenticationError.js +1 -2
  17. package/dist/nodefony/errors/CsrfError.js +1 -2
  18. package/dist/nodefony/errors/InsecureTransportError.js +23 -0
  19. package/dist/nodefony/errors/InvalidTargetError.js +1 -2
  20. package/dist/nodefony/errors/SsrfError.js +1 -2
  21. package/dist/nodefony/errors/ThrottledError.js +1 -2
  22. package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
  23. package/dist/nodefony/errors/WebAuthnError.js +1 -2
  24. package/dist/nodefony/errors/index.js +2 -1
  25. package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
  26. package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
  27. package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
  28. package/dist/nodefony/keycloak/scaffold.js +231 -0
  29. package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
  30. package/dist/nodefony/scaffold/contribute.js +19 -0
  31. package/dist/nodefony/service/accessTokenVerifier.js +101 -34
  32. package/dist/nodefony/service/apiKeys.js +45 -5
  33. package/dist/nodefony/service/auditService.js +4 -5
  34. package/dist/nodefony/service/authFlow.js +118 -16
  35. package/dist/nodefony/service/authorization.js +1 -2
  36. package/dist/nodefony/service/cors.js +1 -2
  37. package/dist/nodefony/service/csrf.js +1 -2
  38. package/dist/nodefony/service/firewall.js +116 -25
  39. package/dist/nodefony/service/keycloak.js +208 -0
  40. package/dist/nodefony/service/oauth2.js +403 -28
  41. package/dist/nodefony/service/securityHeaders.js +1 -2
  42. package/dist/nodefony/service/tokenService.js +94 -27
  43. package/dist/nodefony/service/totp.js +55 -11
  44. package/dist/nodefony/service/webAuthn.js +24 -7
  45. package/dist/nodefony/service/webhooks.js +4 -5
  46. package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
  47. package/dist/nodefony/src/SecuredArea.js +15 -5
  48. package/dist/nodefony/src/accountBinding.js +96 -0
  49. package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
  50. package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
  51. package/dist/nodefony/src/admin/adminAudit.js +1 -3
  52. package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
  53. package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
  54. package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
  55. package/dist/nodefony/src/audit/auditBridge.js +1 -2
  56. package/dist/nodefony/src/audit/auditFilters.js +1 -2
  57. package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
  58. package/dist/nodefony/src/audit/readAuditContext.js +1 -2
  59. package/dist/nodefony/src/audit/recordAudit.js +1 -2
  60. package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
  61. package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
  62. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
  63. package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
  64. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
  65. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
  66. package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
  67. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
  68. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
  69. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  70. package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
  71. package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
  72. package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
  73. package/dist/nodefony/src/crypto/secretCipher.js +1 -2
  74. package/dist/nodefony/src/csp.js +1 -2
  75. package/dist/nodefony/src/csrfToken.js +1 -2
  76. package/dist/nodefony/src/entryPoint.js +38 -0
  77. package/dist/nodefony/src/net/ssrfGuard.js +1 -2
  78. package/dist/nodefony/src/net/transportFailure.js +57 -0
  79. package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
  80. package/dist/nodefony/src/oauth/httpJson.js +3 -3
  81. package/dist/nodefony/src/oauth/metadata.js +28 -12
  82. package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
  83. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
  84. package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
  85. package/dist/nodefony/src/oauth/providerRoles.js +78 -0
  86. package/dist/nodefony/src/oauth/providers/github.js +48 -12
  87. package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
  88. package/dist/nodefony/src/openToSignup.js +46 -0
  89. package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
  90. package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
  91. package/dist/nodefony/src/rolesWithin.js +57 -0
  92. package/dist/nodefony/src/sessionAuthentication.js +25 -0
  93. package/dist/nodefony/src/sessionIdentity.js +29 -3
  94. package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
  95. package/dist/nodefony/src/token/AnonymousToken.js +1 -2
  96. package/dist/nodefony/src/token/JwtKeystore.js +89 -48
  97. package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
  98. package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
  99. package/dist/nodefony/src/token/UserToken.js +1 -2
  100. package/dist/nodefony/src/token/jwtRuntime.js +1 -2
  101. package/dist/nodefony/src/token/scopeGrants.js +109 -0
  102. package/dist/nodefony/src/token/secretFile.js +60 -4
  103. package/dist/nodefony/src/token/tokenCriteria.js +1 -2
  104. package/dist/nodefony/src/token/tokenFilters.js +1 -2
  105. package/dist/nodefony/src/token/tokenSort.js +1 -2
  106. package/dist/nodefony/src/token/tokenStatus.js +1 -2
  107. package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
  108. package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
  109. package/dist/nodefony/src/totp/totpCipher.js +1 -2
  110. package/dist/nodefony/src/totp/totpCrypto.js +1 -2
  111. package/dist/nodefony/src/totp/totpOperations.js +1 -2
  112. package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
  113. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
  114. package/dist/nodefony/src/voter/RoleVoter.js +1 -2
  115. package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
  116. package/dist/nodefony/src/voter/voterRegistry.js +1 -2
  117. package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
  118. package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
  119. package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
  120. package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
  121. package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
  122. package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
  123. package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
  124. package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
  125. package/dist/nodefony/src/webhook/webhookSort.js +1 -2
  126. package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
  127. package/dist/types/index.d.ts +26 -6
  128. package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
  129. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  130. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  131. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  132. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  133. package/dist/types/nodefony/config/config.d.ts +64 -1
  134. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  135. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  136. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  137. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  138. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  139. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  140. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  141. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  142. package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
  143. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  144. package/dist/types/nodefony/errors/index.d.ts +1 -0
  145. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  146. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  147. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  148. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  149. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  150. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  151. package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
  152. package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
  153. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  154. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  155. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  156. package/dist/types/nodefony/service/oauth2.d.ts +145 -9
  157. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  158. package/dist/types/nodefony/service/totp.d.ts +28 -4
  159. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  160. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  161. package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
  162. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
  163. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
  164. package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
  165. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
  166. package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
  167. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  168. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  169. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  170. package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
  171. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  172. package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
  173. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  174. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  175. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  176. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  177. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  178. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  179. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  180. package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
  181. package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
  182. package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
  183. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  184. package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
  185. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  186. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  187. package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
  188. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  189. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  190. package/docs/api-keys.md +182 -175
  191. package/docs/audit.md +141 -141
  192. package/docs/authenticators.md +129 -96
  193. package/docs/authorization.md +116 -69
  194. package/docs/cors.md +64 -64
  195. package/docs/csrf.md +77 -72
  196. package/docs/external-jwt.md +83 -11
  197. package/docs/firewall.md +119 -83
  198. package/docs/headers.md +82 -82
  199. package/docs/index.md +16 -8
  200. package/docs/keycloak.md +835 -0
  201. package/docs/login-page.md +361 -0
  202. package/docs/oauth2.md +291 -166
  203. package/docs/obtenir-un-jeton.md +131 -34
  204. package/docs/tokens.md +197 -140
  205. package/docs/totp.md +177 -136
  206. package/docs/webauthn.md +157 -154
  207. package/docs/webhooks.md +128 -125
  208. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  209. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  210. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  211. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  212. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  213. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  214. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  215. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  216. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  217. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  218. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  219. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  220. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  221. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  222. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  223. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  224. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  225. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  226. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  227. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  228. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  229. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  230. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  231. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  232. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  233. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  234. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  235. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  236. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  237. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  238. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  239. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  240. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  241. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  242. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  243. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  244. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  245. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  246. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  247. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  248. package/package.json +19 -17
  249. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/firewall.md CHANGED
@@ -52,10 +52,10 @@ flowchart TD
52
52
  AU -->|succès| OK["user + token dans l'ALS → contrôleur"]
53
53
  ```
54
54
 
55
- `Firewall.isSecure()` (`firewall.ts:728`) rattache la requête à une **zone** via
56
- `Firewall.matchPath()` (`firewall.ts:719`) ; `Firewall.handleSecurity()` (`firewall.ts:761`) décide.
55
+ `Firewall.isSecure()` (`firewall.ts#Firewall.isSecure`) rattache la requête à une **zone** via
56
+ `Firewall.matchPath()` (`firewall.ts#Firewall.matchPath`) ; `Firewall.handleSecurity()` (`firewall.ts#Firewall.handleSecurity`) décide.
57
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:257`).
58
+ le plus long gagne, pas le premier déclaré (`firewall.ts#Firewall.#build`).
59
59
 
60
60
  ## 📖 Lexique
61
61
 
@@ -64,7 +64,7 @@ le plus long gagne, pas le premier déclaré (`firewall.ts:257`).
64
64
  | Zone | Un motif d'URL (+ host) avec sa politique (`config.areas`, un objet par nom). |
65
65
  | Authenticator | Une stratégie d'identification (session, userpassword, jwt, apikey, anonymous). |
66
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. |
67
+ | Challenge | En-tête `WWW-Authenticate` (RFC 9110 §11) qui dit comment s'authentifier. |
68
68
  | BFF | Backend-For-Frontend : le serveur gère session/jetons pour le front web. |
69
69
  | PAT | Personal Access Token : une clé d'API opaque, révocable côté serveur. |
70
70
  | Bearer | Schéma `Authorization: Bearer <token>` (RFC 6750). |
@@ -74,31 +74,34 @@ le plus long gagne, pas le premier déclaré (`firewall.ts:257`).
74
74
  ### Dans une app `nodefony create app`, le firewall est DÉJÀ actif
75
75
 
76
76
  Le scaffold déclare trois zones (`main`, `secure`, `machine`) dans `nodefony/config/security.ts`, importé par `nodefony.config.ts` ; en voici deux — c'est la forme canonique (un **objet par
77
- nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts:1126-1127`) :
77
+ nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts#securityConfigSchema`) :
78
78
 
79
79
  ```typescript
80
- // nodefony/config/security.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"],
80
+ // nodefony/config/security.ts
81
+ import type { ISecurityConfigInput } from "@nodefony/security";
82
+
83
+ export const securityConfig = () =>
84
+ ({
85
+ areas: {
86
+ // Zone de TES routes : `session` PUIS `anonymous` → identifié si cookie,
87
+ // sinon visiteur accepté. Hors zone, l'identité n'est JAMAIS résolue.
88
+ main: {
89
+ pattern: "^/api",
90
+ authenticators: ["session", "anonymous"],
91
+ },
92
+ // Zone PROTÉGÉE — pattern PLUS SPÉCIFIQUE que ^/api : le firewall trie
93
+ // par longueur → /api/secure/* tombe ICI. Pas d'`anonymous` : sans
94
+ // session → 401 AVANT ton controller (Zero Trust).
95
+ secure: {
96
+ pattern: "^/api/secure",
97
+ authenticators: ["session"],
98
+ },
88
99
  },
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"],
100
+ roleHierarchy: {
101
+ ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
102
+ ROLE_ADMIN: ["ROLE_USER"],
95
103
  },
96
- },
97
- roleHierarchy: {
98
- ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
99
- ROLE_ADMIN: ["ROLE_USER"],
100
- },
101
- });
104
+ }) satisfies ISecurityConfigInput;
102
105
  ```
103
106
 
104
107
  > [!IMPORTANT]
@@ -182,6 +185,39 @@ Authorization: Bearer eyJhbGciOiJFZERTQS␣…␣.␣…␣.␣… # un J
182
185
  Authorization: Bearer nf_9a2c… # une clé API (préfixe nf_)
183
186
  ```
184
187
 
188
+ ### Protéger des pages : conduire le visiteur à la connexion (`entryPoint`)
189
+
190
+ Sans réglage, un visiteur anonyme qui ouvre une page protégée reçoit un `401` brut.
191
+ `entryPoint` lui donne la page de connexion à la place :
192
+
193
+ ```typescript
194
+ use("@nodefony/security", {
195
+ areas: {
196
+ pages: {
197
+ pattern: "^/compte(/|$)",
198
+ authenticators: ["session"],
199
+ entryPoint: "/login", // la page de connexion du framework (loginPage.path)
200
+ },
201
+ },
202
+ });
203
+ ```
204
+
205
+ | La requête refusée | Réponse |
206
+ | ---------------------------------------------------------------------------- | ------------------------------------ |
207
+ | `GET`/`HEAD`, `Accept: text/html`, `Sec-Fetch-Mode: navigate` ou absent | `302` → `/login?from=%2Fcompte` |
208
+ | appel d'API (`Accept: application/json`), `fetch()` (`Sec-Fetch-Mode: cors`) | `401` + `WWW-Authenticate`, inchangé |
209
+ | `POST`, `PUT`, `DELETE`…, WebSocket | refus inchangé |
210
+
211
+ - La page de connexion ramène au chemin demandé (`from`, requête comprise) une fois connecté.
212
+ `from` passe par `safeRedirectPath`, la garde anti-redirection ouverte du cœur.
213
+ - C'est toujours un **refus** : l'audit enregistre `auth.denied` / `no_credentials`, et le hook
214
+ `onAuthFailure` se déclenche, comme pour un `401`.
215
+ - `entryPoint` doit être un chemin de l'origine : une adresse externe (`https://…`, `//…`) est
216
+ refusée au démarrage.
217
+ - La page de connexion elle-même n'est jamais redirigée vers elle-même : si elle tombe dans la
218
+ zone, sa requête garde son `401`.
219
+ - Absent (le défaut), tous les clients reçoivent le `401`.
220
+
185
221
  ## 🔐 Les authenticators intégrés
186
222
 
187
223
  Tous respectent le **même contrat** (`IAuthenticator`) : `supports(context)` (test bon marché : la
@@ -203,9 +239,9 @@ Point commun de sécurité : **message d'échec uniforme** (`"Invalid token"` /
203
239
  Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
204
240
 
205
241
  - **N'ouvre jamais la session lui-même** : il exige une session reprise portant un user
206
- (`supports()`, `SessionAuthenticator.ts:75`). C'est `AuthFlow.login()` (BFF) qui ouvre et
242
+ (`supports()`, `SessionAuthenticator.ts#SessionAuthenticator.supports`). C'est `AuthFlow.login()` (BFF) qui ouvre et
207
243
  régénère l'ID (anti-fixation).
208
- - **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts:96`) → rôles frais,
244
+ - **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts#SessionAuthenticator.authenticate`) → rôles frais,
209
245
  révocation et verrouillage effectifs immédiatement.
210
246
  - **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login
211
247
  (pas de popup Basic).
@@ -213,11 +249,11 @@ Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
213
249
  ### `userpassword` — HTTP Basic, avec throttle NIST
214
250
 
215
251
  Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617, split au **premier**
216
- `:` (`UserPasswordAuthenticator.ts:74`).
252
+ `:` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`).
217
253
 
218
254
  - **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
219
255
  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`) :
256
+ - **Le throttle NIST SP 800-63B passe AVANT le verifier** (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) :
221
257
  un identifiant bloqué ne coûte **aucun hash argon2** — protège d'un DoS par hachage.
222
258
  Échec → backoff ; `ThrottledError` → **429 + `Retry-After`**.
223
259
  - Challenge : `Basic realm="nodefony"`.
@@ -226,23 +262,23 @@ Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617,
226
262
 
227
263
  ### `jwt` — Bearer JWT signé, durci RFC 8725
228
264
 
229
- Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts:21`).
265
+ Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts#COMPACT_JWS`).
230
266
  Réservé API service↔service / agents (le web reste sur la session). Access token **EdDSA** signé par
231
267
  le keystore du serveur. Défenses **dures**, prouvées en test (RFC 8725 JWT BCP), toutes dans
232
268
  `JwtAuthenticator.authenticate()` :
233
269
 
234
270
  - **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`).
271
+ token ; `alg=none` rejeté (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
236
272
  - **clé par `kid` depuis le JWKS LOCAL** (`createLocalJWKSet`) — jamais `jku`/`jwk` de l'en-tête
237
- (anti-injection de clé / SSRF, `JwtAuthenticator.ts:174`).
273
+ (anti-injection de clé / SSRF, `JwtAuthenticator.ts#JwtAuthenticator.#ensureGetKey`).
238
274
  - **`aud` + `iss` obligatoires** + `typ:"at+jwt"` (un refresh présenté comme access est rejeté) +
239
- exp/nbf (`JwtAuthenticator.ts:119-124`).
275
+ exp/nbf (`JwtAuthenticator.ts#JwtAuthenticator.verifyForResource`).
240
276
  - **révocation** malgré l'auto-portage : denylist `jti` + `invalidBefore` par sujet
241
- (`JwtAuthenticator.ts:138-148`).
277
+ (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
242
278
  - **sujet revérifié** à réception (`loadUserByIdentifier(sub)`) : compte disparu/inactif/verrouillé
243
- = rejet (`JwtAuthenticator.ts:190-201`).
279
+ = rejet (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
244
280
 
245
- Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts:178-188`).
281
+ Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
246
282
 
247
283
  > [!WARNING]
248
284
  > Un JWT est **auto-porté** : sans état serveur il n'est **pas** révocable. C'est la denylist
@@ -251,26 +287,26 @@ Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts:178-188`).
251
287
 
252
288
  ### `apikey` — clé d'API opaque (PAT), révocable
253
289
 
254
- Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts:75-79`). Contrairement au JWT,
290
+ Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.supports`). Contrairement au JWT,
255
291
  c'est un **bearer opaque** : sa vérité vit côté serveur (`ITokenStore`) → **révocable immédiatement**.
256
292
 
257
293
  Défenses de `ApiKeyAuthenticator.authenticate()` :
258
294
 
259
295
  - **forme + CRC validés AVANT tout accès au store** — anti-DoS (`parseApiKey()`,
260
- `ApiKeyAuthenticator.ts:103`) ;
261
- - lookup par **hash sha256** : le secret n'existe nulle part au repos (`ApiKeyAuthenticator.ts:109`) ;
296
+ `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
297
+ - lookup par **hash sha256** : le secret n'existe nulle part au repos (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
262
298
  - révocation (`revokedAt`), expiration (`expiresAt`), **ban en masse** du porteur
263
- (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts:111-124`) ;
264
- - **sujet revérifié** à chaque requête — rôles frais (`ApiKeyAuthenticator.ts:127`) ;
265
- - `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`ApiKeyAuthenticator.ts:136-143`).
299
+ (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
300
+ - **sujet revérifié** à chaque requête — rôles frais (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
301
+ - `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
266
302
 
267
- Le token porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:147-149`). `jwt` et `apikey` **cohabitent** dans
303
+ Le token porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`). `jwt` et `apikey` **cohabitent** dans
268
304
  une zone : ils se discriminent par la forme (JWT = `a.b.c`, PAT = `prefix_…`).
269
305
 
270
306
  ### `anonymous` — accepter l'anonymat, explicitement
271
307
 
272
308
  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
309
+ (`AnonymousAuthenticator.ts#AnonymousAuthenticator`). À lister **volontairement** : `["jwt", "anonymous"]` en mode
274
310
  `first` = « identifié si preuve présente, sinon **visiteur anonyme accepté** ». En mode `all`, utile
275
311
  en dernier : « le canal doit être prouvé (ex. mTLS), l'identité utilisateur est optionnelle ». Coût
276
312
  nul : `supports()` accepte tout, le token porte le singleton gelé `anonymousUser` (0 allocation).
@@ -280,14 +316,14 @@ Sans lui, zone protégée + aucune preuve = 401.
280
316
 
281
317
  Il promeut en jeton realtime **toute** identité que le firewall a résolue — session BFF comme jeton
282
318
  porteur (JWT, clé d'API). **Enregistré automatiquement** par `Firewall.#wireRealtime()` au
283
- handshake des zones protégées `realtime` (`firewall.ts:279`).
319
+ handshake des zones protégées `realtime` (`firewall.ts#Firewall.#wireRealtime`).
284
320
 
285
321
  - **Perf : il ne relit pas la base.** Handshake et frames tournent dans la même bulle ALS —
286
322
  l'identité déjà posée est réutilisée, 2 lectures base économisées par connexion
287
- (`FirewallRealtimeAuthenticator.ts:24-30`).
323
+ (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
288
324
  - **Asymétrie HTTP↔WS assumée** : le jeton est **figé au handshake** (les frames lisent un cache
289
325
  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
326
+ `api.request`), pas à la frame suivante (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`). C'est l'état
291
327
  de l'art (Socket.IO et Phoenix figent aussi).
292
328
  - **Filet** : un revalidator re-lit la session avant chaque action data plane ; fail-closed →
293
329
  fermeture 4001.
@@ -320,7 +356,7 @@ laisser chercher pourquoi `context.session` est nul.
320
356
  ## ⚙️ Ordre et modes (`mode: "first"` vs `"all"`)
321
357
 
322
358
  La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
323
- (`firewall.ts:1128`). Le `mode` dit comment la parcourir. Trois situations concrètes :
359
+ (`firewall.ts#Firewall.#authenticate`). Le `mode` dit comment la parcourir. Trois situations concrètes :
324
360
 
325
361
  ### Situation 1 — humains ET machines sur la même API (`first`, le mode courant)
326
362
 
@@ -342,7 +378,7 @@ Ce qui se passe, requête par requête :
342
378
  | --- | --- | --- |
343
379
  | le cookie de session | `session` | identifié, `apikey` jamais consulté |
344
380
  | `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:1128`) |
381
+ | une clé **révoquée** `nf_…` | `apikey` | **401 direct** — l'échec d'`authenticate()` remonte, pas de fallback (`firewall.ts#Firewall.#authenticate`) |
346
382
  | rien | aucun | **401** (Zero Trust) |
347
383
 
348
384
  ### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
@@ -373,11 +409,11 @@ danger: {
373
409
 
374
410
  Le client doit présenter **les deux preuves** dans la même requête (cookie + `Authorization:
375
411
  Basic …`). Une seule manque → 401. Le **dernier** token de la chaîne porte l'identité
376
- (`firewall.ts:1199`) — ici la preuve mot de passe, la plus fraîche.
412
+ (`firewall.ts#Firewall.#authenticate`) — ici la preuve mot de passe, la plus fraîche.
377
413
 
378
414
  > [!TIP]
379
415
  > Un nom d'authenticator inconnu en config **fait échouer le boot** —
380
- > `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts:435`) : jamais de zone
416
+ > `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts#Firewall.#instantiateAuthenticators`) : jamais de zone
381
417
  > « protégée » silencieusement ouverte à cause d'une faute de frappe.
382
418
 
383
419
  ## 🧑‍⚖️ Autorisation — rôles, scopes, voters (« as-tu le droit ? »)
@@ -399,30 +435,30 @@ du contrôleur** (seam Resolver) — une action protégée ne s'exécute jamais
399
435
 
400
436
  ### Le jury et sa stratégie
401
437
 
402
- `Authorization.decide(token, attribut, subject?)` (`service/authorization.ts:70`) applique une
438
+ `Authorization.decide(token, attribut, subject?)` (`service/authorization.ts#Authorization.decide`) applique une
403
439
  stratégie **affirmative + DENY veto**, fermée par défaut (**Zero Trust**) :
404
440
 
405
441
  - un seul **`DENY`** bloque (veto, court-circuit — inutile de finir le jury,
406
- `authorization.ts:94-97`) ;
442
+ `authorization.ts#Authorization.decide`) ;
407
443
  - sinon un **`GRANT`** suffit ;
408
444
  - **silence total** (tous `ABSTAIN`, ou aucun voter compétent) → **`DENY`**
409
- (`authorization.ts:100-108`) ;
445
+ (`authorization.ts#Authorization`) ;
410
446
  - un voter qui **throw** → **`DENY`** + log ERROR (fail-closed : jamais 500, jamais octroi,
411
- `authorization.ts:85-93`).
447
+ `authorization.ts#Authorization.decide`).
412
448
 
413
- Tout refus est audité (WARNING + `recordAudit`, `authorization.ts:113-142`) ; les octrois restent
449
+ Tout refus est audité (WARNING + `recordAudit`, `authorization.ts#Authorization`) ; les octrois restent
414
450
  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`).
451
+ (aucun nom en dur, `authorization.ts#Authorization`).
416
452
 
417
453
  ### Les voters intégrés — deux axes
418
454
 
419
455
  - **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`).
456
+ résolue** ; **`ABSTAIN` sinon**, jamais `DENY` (`RoleVoter.vote()`, `RoleVoter.ts#RoleVoter.vote`).
421
457
  Constat : l'absence d'un rôle ne doit pas opposer son veto aux autres axes — c'est le
422
458
  **default-DENY du jury** qui ferme la porte, pas ce voter. C'est ce qui rend une clause OR
423
459
  (`@IsGranted(["A","B"])`) possible.
424
460
  - **ScopeVoter** (`scope`, attributs `api:action`) — un scope **ne bride jamais un humain**
425
- (`ScopeVoter.ts:17-62`) :
461
+ (`ScopeVoter.ts`) :
426
462
  - jeton humain (`session`/`userpassword`/`anonymous`) → `GRANT` no-op : l'autorisation d'un
427
463
  humain passe par ses **rôles** ;
428
464
  - jeton **machine délégué** (`apikey`/`jwt`/`oauth2`) → `GRANT` si le scope exact est présent,
@@ -486,8 +522,8 @@ destinataire.
486
522
 
487
523
  `RoleHierarchyWalker` (`src/RoleHierarchyWalker.ts`) : `ROLE_ADMIN` hérite `ROLE_USER`, etc.
488
524
  **Aplatissement précalculé au boot** → `hasRole()` est O(1) sur le hot path
489
- (`RoleHierarchyWalker.ts:23-30`), et les **cycles sont détectés au boot** (throw avec le chemin
490
- complet, pas de fail-silent, `RoleHierarchyWalker.ts:69-95`). La hiérarchie est posée au container
525
+ (`RoleHierarchyWalker.ts#RoleHierarchyWalker.hasRole`), et les **cycles sont détectés au boot** (throw avec le chemin
526
+ complet, pas de fail-silent, `RoleHierarchyWalker.ts#RoleHierarchyWalker.#detectCycles`). La hiérarchie est posée au container
491
527
  par le firewall au boot ; le `RoleVoter` la lit en lazy.
492
528
 
493
529
  ### Voters métier (le vrai pouvoir applicatif)
@@ -506,41 +542,41 @@ registerVoterFactory(
506
542
  ```
507
543
 
508
544
  Le voter est **découvert automatiquement** par le service `authorization` — aucun changement dans le
509
- cœur (`registerVoterFactory()`, `voterRegistry.ts:40`). Pourquoi un registre et pas un scan DI des
545
+ cœur (`registerVoterFactory()`, `voterRegistry.ts#registerVoterFactory`). Pourquoi un registre et pas un scan DI des
510
546
  `@injectable` : les interfaces TS sont **effacées à la compilation** — rien à scanner ; le registre
511
- **est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts:6-16`). Trois axes (rôles,
547
+ **est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts#IVoterFactoryContext`). Trois axes (rôles,
512
548
  scopes, métier), un même jury, combinables.
513
549
 
514
550
  ## 🔌 HTTP et WebSocket — le même firewall
515
551
 
516
- `Firewall.#wireRealtime()` (`firewall.ts:279`) câble, pour toute zone protégée `realtime !== false`
517
- (opt-out, `firewall.ts:277`), le `FirewallRealtimeAuthenticator` au handshake (`firewall.ts:300`)
518
- **et** un `frameAuthorizer` (RBAC par canal, `firewall.ts:348`). Même résolution de zone que HTTP.
552
+ `Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) câble, pour toute zone protégée `realtime !== false`
553
+ (opt-out, `firewall.ts#Firewall.#build`), le `FirewallRealtimeAuthenticator` au handshake (`firewall.ts#Firewall.#wireRealtime`)
554
+ **et** un `frameAuthorizer` (RBAC par canal, `firewall.ts#Firewall.#wireRealtime`). Même résolution de zone que HTTP.
519
555
  Sur une socket, un refus n'a pas d'en-tête `WWW-Authenticate` (`Firewall.#setChallenge()`,
520
- `firewall.ts:1216`) : le **code de fermeture** suffit.
556
+ `firewall.ts#Firewall.#setChallenge`) : le **code de fermeture** suffit.
521
557
 
522
558
  ## 🛡️ En-têtes de sécurité, CSRF, CORS
523
559
 
524
- - **`Firewall.applySecurityHeaders()`** (`firewall.ts:1045`) : CSP, Referrer-Policy, COOP/COEP/CORP
525
- au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts:1045`) :
560
+ - **`Firewall.applySecurityHeaders()`** (`firewall.ts#Firewall.applySecurityHeaders`) : CSP, Referrer-Policy, COOP/COEP/CORP
561
+ au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts#Firewall.enforceCsrf`) :
526
562
  alloué seulement si une directive en a besoin.
527
- - **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts:948`) : Fetch Metadata
528
- (`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts:971`), puis double-submit `x-csrf-token` ≡
529
- cookie + HMAC (`firewall.ts:778`).
530
- - **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts:1014`).
563
+ - **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts#Firewall.enforceCsrf`) : Fetch Metadata
564
+ (`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts#Firewall.#startTrace`), puis double-submit `x-csrf-token` ≡
565
+ cookie + HMAC (`firewall.ts#Firewall.describe`).
566
+ - **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts#Firewall.handleCors`).
531
567
 
532
568
  ## 📜 Normes appliquées
533
569
 
534
- | Domaine | Norme | Ancrage |
535
- | ---------------------- | --------------- | ------------------------------------------------------ |
536
- | Challenge d'auth (401) | RFC 7235 | `Firewall.#setChallenge()` (`firewall.ts:1216`) |
537
- | Bearer | RFC 6750 | `JwtAuthenticator.ts:13` · `ApiKeyAuthenticator.ts:11` |
538
- | JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts:33-44,104-108` |
539
- | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts:10-28` |
540
- | Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts:777-780`) |
541
- | Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts:43-46,101-104` |
542
- | CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts:948`) |
543
- | Modèle | Zero Trust | `firewall.ts:818-833` (aucune preuve → 401) |
570
+ | Domaine | Norme | Ancrage |
571
+ | ---------------------- | --------------- | ----------------------------------------------------------------- |
572
+ | Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts#Firewall.#setChallenge`) |
573
+ | Bearer | RFC 6750 | `JwtAuthenticator.ts` · `ApiKeyAuthenticator.ts#INVALID_TOKEN` |
574
+ | JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts#JwtAuthenticator,104-108` |
575
+ | HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts` |
576
+ | Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts#Firewall.handleSecurity`) |
577
+ | Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts#UserPasswordAuthenticator,101-104` |
578
+ | CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts#Firewall.enforceCsrf`) |
579
+ | Modèle | Zero Trust | `firewall.ts#Firewall` (aucune preuve → 401) |
544
580
 
545
581
  ## ⚡ Performance & mémoire
546
582