@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
package/docs/tokens.md ADDED
@@ -0,0 +1,520 @@
1
+ ---
2
+ title: "Tokens — émission, clés (keystore), rotation et révocation"
3
+ navTitle: Tokens
4
+ lang: fr
5
+ module: "@nodefony/security"
6
+ topic: tokens
7
+ coverageModule: security
8
+ coverageFiles: "tokenService,JwtKeystore,MemoryTokenStore,jwtRuntime,tokenStoreRegistry"
9
+ section: "Sécurité"
10
+ audience: [developer, devops]
11
+ tags:
12
+ [
13
+ security,
14
+ jwt,
15
+ tokens,
16
+ refresh,
17
+ keystore,
18
+ jwks,
19
+ rotation,
20
+ revocation,
21
+ pagination,
22
+ rfc9700,
23
+ rfc6749,
24
+ ed25519,
25
+ ]
26
+ version: "doc"
27
+ status: stable
28
+ updated: 2026-07-19
29
+ source: "src/packages/@nodefony/security/docs/tokens.md"
30
+ ---
31
+
32
+ # Tokens — émission, clés, rotation et révocation
33
+
34
+ > Les authenticators _vérifient_ des jetons ; cette page décrit leur **face émission** : comment
35
+ > Nodefony signe un access token JWT, gère la **clé** (keystore Ed25519 + JWKS), fait **tourner** les
36
+ > refresh tokens avec détection de rejeu (RFC 9700), et **révoque** — au-dessus d'un `ITokenStore`
37
+ > pluggable (memory/drizzle/mongoose/redis) désormais **paginé** pour l'admin. Ancré sur
38
+ > `src/packages/@nodefony/security/nodefony/service/tokenService.ts` et `nodefony/src/token/`.
39
+
40
+ 📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Jetons**
41
+
42
+ ## 🧠 Le modèle mental — émission, rotation, révocation
43
+
44
+ ```mermaid
45
+ flowchart TD
46
+ CLI["POST /nodefony/security/api/token<br/>{username, password, scope?}"] --> VER["users.authenticate<br/>(+ throttle NIST)"]
47
+ VER --> ISS["issueTokens"]
48
+ ISS --> AT["access token<br/>JWT EdDSA, typ at+jwt, 15 min"]
49
+ ISS --> RT["refresh token<br/>secret opaque nfr_…, stocké HACHÉ"]
50
+ RT --> ST[("ITokenStore<br/>memory · drizzle · mongoose · redis")]
51
+ AT -.->|kid| KS["JwtKeystore<br/>Ed25519 · JWKS public"]
52
+ REF["POST …/api/token/refresh<br/>{refresh_token}"] --> ROT{"déjà révoqué ?"}
53
+ ROT -->|"oui = rejeu"| FAM["revokeFamily<br/>toute la famille coupée"]
54
+ ROT -->|non| NEW["rotation : nouveau couple<br/>ancien chaîné + révoqué"]
55
+ ```
56
+
57
+ ## 📖 Lexique
58
+
59
+ | Terme | Sens |
60
+ | --------------- | --------------------------------------------------------------------------------------------------- |
61
+ | Access token | JWT court (15 min) signé EdDSA, porté en `Authorization: Bearer` — jamais en cookie/URL. |
62
+ | Refresh token | Secret opaque longue durée (`nfr_…`), stocké **haché**, échangé contre un nouvel access. |
63
+ | PAT | _Personal Access Token_ (clé API) — même store, autre page ([authenticators](./authenticators.md)). |
64
+ | Grant | Échange d'un credential (identifiant/mot de passe) contre un couple access+refresh. |
65
+ | Keystore | Gestionnaire des clés de signature Ed25519 + du JWKS public. |
66
+ | JWKS | _JSON Web Key Set_ : les clés **publiques** exposées pour vérifier les signatures. |
67
+ | `kid` | Identifiant de clé (empreinte) posé dans l'en-tête du JWT → sélection de la bonne clé. |
68
+ | `jti` | Identifiant unique d'un JWT — clé de la denylist de révocation ciblée. |
69
+ | Rotation | Émettre un nouveau refresh à chaque usage et révoquer l'ancien (RFC 9700). |
70
+ | Famille | Chaîne de refresh liés par rotation ; un rejeu coupe toute la famille. |
71
+ | Downscoping | Les scopes ne **montent** jamais le long d'une chaîne de refresh. |
72
+ | `invalidBefore` | Seuil par porteur : tout access émis avant cet instant est rejeté (révocation en masse). |
73
+
74
+ ## Qu'est-ce que ce système résout — la faille
75
+
76
+ Un JWT est **auto-porté** : le serveur peut le vérifier sans état. Génial pour la scalabilité,
77
+ dangereux pour la révocation — un jeton volé reste valide jusqu'à son expiration si rien ne le suit
78
+ côté serveur. Deux attaques concrètes :
79
+
80
+ - le **vol de refresh token** — l'attaquant le rejoue pour obtenir des access frais indéfiniment ;
81
+ - l'**absence de révocation** — bannir un compte ne coupe pas ses jetons déjà émis.
82
+
83
+ Nodefony répond par un **store de vérité côté serveur** (denylist `jti` + `invalidBefore` + rotation
84
+ avec détection de rejeu) et une **gestion de clé** qui ne génère jamais de secret en clair « par
85
+ défaut » en prod.
86
+
87
+ ## La vision Nodefony — un service propriétaire, des endpoints minces
88
+
89
+ `TokenService` est **propriétaire** du store et du keystore : à `TokenService.#build()`
90
+ (`tokenService.ts:93`), si `jwt.enabled` ou `apiKeys.enabled`, il résout le store pluggable, pose
91
+ `tokenStore` au container (`tokenService.ts:163`) puis crée le keystore et pose `jwtKeystore`
92
+ (`tokenService.ts:167-173`) — consommés par le `JwtAuthenticator` et les endpoints. Il arme un
93
+ **gc** via `GcScheduler` (timer `unref` + **jitter** de phase pour étaler les balayages entre pods,
94
+ `tokenService.ts:175-181`).
95
+
96
+ Les endpoints HTTP sont des **adaptateurs minces** portés par `@nodefony/framework`, couplés **par
97
+ nom de service** via le contrat structurel `ITokenIssuer` — framework n'importe jamais security
98
+ (`TokenAuthController.ts:11-22`).
99
+
100
+ Constat clé de cohérence : `iss`/`aud`/`ttl` sont dérivés **une seule fois** par
101
+ `resolveJwtRuntime()` (`jwtRuntime.ts:30-41`) et **partagés** entre l'émetteur et le vérificateur —
102
+ une divergence ferait tout rejeter. Fonction pure : les deux côtés obtiennent la même valeur sans la
103
+ partager par référence. `issuer` omis → `"nodefony"` (`jwtRuntime.ts:31`), à surcharger en prod.
104
+
105
+ ### Être DÉCOUVRABLE — publier ses clés (RFC 8414)
106
+
107
+ Tant que Nodefony émet **et** vérifie ses propres jetons, `iss` n'est qu'une chaîne comparée à
108
+ elle-même. Dès qu'un **tiers** doit valider une signature émise ici (une autre application
109
+ Nodefony, un agent, un service), il lui faut deux documents publics :
110
+
111
+ | Route | Contenu |
112
+ | --------------------------------------------- | -------------------------------------------------------------- |
113
+ | `GET /.well-known/oauth-authorization-server` | `issuer`, `jwks_uri` — RFC 8414 §2 (chemin **non négociable**) |
114
+ | `GET /.well-known/jwks.json` | clés publiques de signature (jamais `d`) |
115
+
116
+ Les deux sont montées par `@nodefony/framework` (`IssuerMetadataController.ts`) **uniquement si**
117
+ `TokenService.publishedIssuer()` répond — soit `jwt.enabled`, `jwt.jwks`, **et** `jwt.issuer` écrit
118
+ sous forme d'URL https. Sinon : aucune route (`404`) et un avertissement au boot.
119
+
120
+ 🔴 **L'URL ne se devine pas.** Derrière un relais (HAProxy, ingress, CDN), `Host` et
121
+ `X-Forwarded-*` viennent de la requête, donc du client : un document dérivé de l'en-tête ferait
122
+ servir, par le vrai serveur, l'identité d'un attaquant — et empoisonnerait tout cache mutualisé.
123
+ L'exploitant l'écrit, comme il écrit son domaine (`NF_JWT_ISSUER` dans `env.ts`).
124
+
125
+ ```ts
126
+ use("@nodefony/security", { jwt: { issuer: ctx.env.NF_JWT_ISSUER } });
127
+ ```
128
+
129
+ Le document publié n'annonce **aucun** flux d'autorisation (`response_types_supported: []`,
130
+ `grant_types_supported: []`) : Nodefony n'est pas un serveur d'autorisation OAuth 2.1, elle rend
131
+ seulement ses signatures vérifiables. Omettre `grant_types_supported` annoncerait
132
+ `["authorization_code", "implicit"]` par défaut (RFC 8414 §2) — deux flux inexistants.
133
+
134
+ ## 🚀 Démarrage rapide
135
+
136
+ ### Les endpoints d'émission sont FOURNIS
137
+
138
+ Dans une app `nodefony create app`, dès que le module security est chargé avec `jwt.enabled` (défaut),
139
+ le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.ts:132-147`) :
140
+
141
+ - `POST /nodefony/security/api/token` — body `{username, password, scope?}` → couple access/refresh ;
142
+ - `POST /nodefony/security/api/token/refresh` — body `{refresh_token}` → rotation.
143
+
144
+ > [!IMPORTANT]
145
+ > Ces routes n'existent **que si** le service `tokenService` est présent — sinon 404, zéro surface
146
+ > (`TokenAuthController.ts:46-48`). Elles sont `bypassFirewall: true` (`TokenAuthController.ts:145`) :
147
+ > elles SONT le mécanisme d'émission — protégées, obtenir un token exigerait d'être déjà
148
+ > authentifié (deadlock). Le JWT part en **réponse JSON** (Bearer), jamais en cookie ni en URL.
149
+
150
+ ### La config : une zone protégée par `jwt` + une clé qui survit au redémarrage
151
+
152
+ ```typescript
153
+ // nodefony.config.ts (extrait) — la zone API machine + la source de clé
154
+ use("@nodefony/security", {
155
+ jwt: {
156
+ // dev/VPS : persiste la clé Ed25519 (sinon clé ÉPHÉMÈRE + warning au boot).
157
+ // prod cloud : préférer keystore.keySetJson injecté depuis l'env (même clé
158
+ // sur tous les pods) — voir la section keystore.
159
+ keystore: { dir: "./config/jwt" },
160
+ },
161
+ areas: {
162
+ // Le firewall vérifie le Bearer JWT sur CHAQUE requête de la zone.
163
+ api: { pattern: "^/api/v1", authenticators: ["jwt"] },
164
+ },
165
+ });
166
+ ```
167
+
168
+ ### Ce que TU écris : le controller scopé
169
+
170
+ ```typescript
171
+ // nodefony/controllers/OrdersController.ts — complet, compile tel quel
172
+ import {
173
+ controller,
174
+ Controller,
175
+ Get,
176
+ RequireScope,
177
+ CurrentUser,
178
+ } from "@nodefony/framework";
179
+ import type { IUser } from "@nodefony/user";
180
+
181
+ @controller("/api/v1/orders")
182
+ class OrdersController extends Controller {
183
+ // Zone `api` : le firewall a déjà validé le JWT (signature, exp, aud/iss,
184
+ // denylist, sujet actif). @RequireScope borne ce que la CLÉ a le droit de
185
+ // faire — un token émis sans `orders:read` reçoit 403, même sujet valide.
186
+ @RequireScope("orders:read")
187
+ @Get("/list")
188
+ async list(@CurrentUser() user: IUser) {
189
+ return this.renderJson({ subject: user.identifier, orders: [] });
190
+ }
191
+ }
192
+
193
+ export default OrdersController;
194
+ ```
195
+
196
+ ### Ce qu'on observe
197
+
198
+ ```bash
199
+ # 0) Un compte (mot de passe demandé MASQUÉ — jamais en dur dans un script)
200
+ npx nodefony security:user:add ci-bot
201
+
202
+ # 1) Grant : credential → couple access/refresh (réponse RFC 6749 §5.1)
203
+ curl -s -H 'Content-Type: application/json' \
204
+ -d "{\"username\":\"ci-bot\",\"password\":\"$NF_PASS\",\"scope\":\"orders:read\"}" \
205
+ http://localhost:5151/nodefony/security/api/token
206
+ # {"access_token":"eyJ…","refresh_token":"nfr_…","token_type":"Bearer",
207
+ # "expires_in":900,"scope":"orders:read"}
208
+
209
+ # 2) L'access token en Bearer → 200 (zone api, scope vérifié)
210
+ curl -s -H "Authorization: Bearer $ACCESS" \
211
+ http://localhost:5151/api/v1/orders/list
212
+ # {"subject":"ci-bot","orders":[]}
213
+
214
+ # 3) Rotation : le refresh → NOUVEAU couple (l'ancien refresh est révoqué)
215
+ curl -s -H 'Content-Type: application/json' \
216
+ -d "{\"refresh_token\":\"$REFRESH\"}" \
217
+ http://localhost:5151/nodefony/security/api/token/refresh
218
+
219
+ # 4) Rejouer l'ANCIEN refresh → 401 {"error":"invalid_grant"} + famille coupée
220
+ ```
221
+
222
+ Erreurs mappées par duck-typing dans `#renderAuthError()` (`TokenAuthController.ts:108-120`) :
223
+ 401 message uniforme `invalid_grant` (anti-énumération), 429 avec `Retry-After` du throttler NIST.
224
+ Émission indisponible (JWT désactivé, store absent) → 503 `isEnabled()`
225
+ (`TokenAuthController.ts:67-69`).
226
+
227
+ ## 🏗️ Architecture interne — la vie d'un couple access/refresh
228
+
229
+ ### Émission (grant M2M/CLI)
230
+
231
+ `issueForCredentials()` (`tokenService.ts:317`) vérifie l'identifiant/mot de passe via le
232
+ service `users`, avec le **throttling NIST partagé** — `ThrottledError` avant tout hachage
233
+ (`tokenService.ts:733`). Chaque tentative échouée est auditée `login.failure`/`login.throttled`
234
+ par `#auditGrant()` (`tokenService.ts:362-374`). Puis `issueTokens()` (`tokenService.ts:490`)
235
+ produit :
236
+
237
+ - un **access token** : JWT signé EdDSA, en-tête `typ:"at+jwt"` + `kid`, claims
238
+ `iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts:402-415`) ;
239
+ - un **refresh token** : secret opaque haute entropie `nfr_<32 octets base64url>`, **stocké haché**
240
+ `sha256` (le clair n'existe qu'en réponse, jamais au repos) — `#buildRefresh()`
241
+ (`tokenService.ts:674-709`).
242
+
243
+ La réponse suit RFC 6749 §5.1 — `ITokenResponse` (`tokenService.ts:43-51`). Tout succès est audité
244
+ `token.issued` via `recordAudit` avec le `tokenId` corrélable (`tokenService.ts:312-318`).
245
+
246
+ ### Rotation & détection de rejeu (RFC 9700 §4.14)
247
+
248
+ `refresh()` (`tokenService.ts:555`) est le cœur défensif, dans l'ordre :
249
+
250
+ 1. Lookup par hash — `findByHash`, refus uniforme si inconnu/mauvais type (`tokenService.ts:564`).
251
+ 2. **Détection de rejeu** : refresh **déjà révoqué** re-présenté → `revokeFamily` coupe toute la
252
+ famille + audit `token.reuse_detected`, signal d'attaque fort (`tokenService.ts:345-361`).
253
+ 3. Expiration `expiresAt` vérifiée (`tokenService.ts:363-368`).
254
+ 4. **Sujet revérifié** — compte disparu/inactif/verrouillé rejeté sans attendre l'exp,
255
+ `#resolveUserForRefresh()` (`tokenService.ts:745`).
256
+ 5. **Downscoping** : les `scopes` du nouveau couple sont ceux de l'ancien, jamais plus
257
+ (`tokenService.ts:592`).
258
+ 6. **Rotation** : nouveau refresh (même famille), l'ancien chaîné `replacedBy` + révoqué
259
+ `"rotated"` (`tokenService.ts:698`). Si `rotateRefresh` est désactivé, l'access est réémis
260
+ et le refresh courant reste valide (`tokenService.ts:627`).
261
+
262
+ ### Mise en situation — ton refresh token a été volé
263
+
264
+ Besoin vécu : le refresh d'un poste compromis est exfiltré. Rotation active (défaut) — voici ce que
265
+ chacun vit, requête par requête :
266
+
267
+ | # | Qui présente quoi | Ce que fait `refresh()` | Résultat client |
268
+ | --- | ------------------------------- | ----------------------------------------------------------- | ----------------------------------- |
269
+ | 1 | Client légitime → refresh R1 | rotation : R2 émis, R1 révoqué `rotated` | 200, nouveau couple |
270
+ | 2 | Voleur → R1 (volé, déjà tourné) | R1 révoqué re-présenté = **rejeu** → famille entière coupée | 401 `invalid_grant` |
271
+ | 3 | Client légitime → R2 | famille coupée : R2 est révoqué aussi | 401 → se reconnecte (nouveau grant) |
272
+
273
+ Si le **voleur joue en premier**, la rotation lui répond normalement (le serveur ne peut pas encore
274
+ le distinguer) — mais dès que le légitime rejoue son vieux refresh, le rejeu est détecté et le
275
+ voleur perd aussi son couple. Dans les deux ordres, **l'attaque est bornée à une fenêtre courte** et
276
+ la victime est déconnectée (signal visible) au lieu d'un vol silencieux indéfini.
277
+
278
+ ## 🔐 Le keystore Ed25519 — la clé ne fuit pas, pas de secret « par défaut » en prod
279
+
280
+ `JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts:97-128`), pensée
281
+ pour ne jamais auto-générer une clé en clair silencieusement en prod :
282
+
283
+ 1. **env** — `keySetJson` (JWK Set injecté depuis le catalogue d'env) : prod cloud, secret géré
284
+ hors-app, même clé sur tous les pods (`JwtKeystore.ts:100-106`).
285
+ 2. **fichier** — `dir/keyset.json`, généré si absent, écriture atomique tmp+rename en mode 600 —
286
+ `#writeAtomic()` (`JwtKeystore.ts:208-217`) : opt-in dev/VPS mono-machine.
287
+ 3. **mémoire** — aucune source → clé **éphémère + WARNING** explicite : perdue au redémarrage =
288
+ refresh invalidés, incohérente en cluster (`JwtKeystore.ts:121-127`).
289
+
290
+ Le JWKS servi par `getPublicJWKS()` (`JwtKeystore.ts:87-90`) est **public** : la composante privée
291
+ `d` est retirée à l'import par `#importKeyset()` (`JwtKeystore.ts:156-158`, RFC 8037/7517) — c'est
292
+ lui qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts:174`), jamais
293
+ une clé venue du token. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts:93-95`).
294
+
295
+ > [!WARNING]
296
+ > **Race au 1ᵉʳ boot d'un cluster sans clé pré-provisionnée** : deux workers peuvent générer des
297
+ > clés différentes — le dernier `rename` gagne (`JwtKeystore.ts:61-64`). En prod, provisionner
298
+ > `keySetJson` hors-bande élimine ce cas : c'est la source recommandée.
299
+
300
+ ## 🧩 Le store pluggable — durable par défaut, jamais de faux durable silencieux
301
+
302
+ ### Le contrat et l'enregistrement
303
+
304
+ Le `tokenStore` héberge **trois structures** : les records (refresh + PAT), la denylist `jti` et le
305
+ seuil `invalidBefore` par porteur (`ITokenStore.ts:11-15`). Les backends s'enregistrent par
306
+ fabrique — `registerTokenStore()` (`tokenStoreRegistry.ts:41-46`) : les adapters lourds importent
307
+ `import type { ITokenStore }` (effacé à la compilation), zéro couplage runtime.
308
+
309
+ Sa résolution au boot (`tokenService.ts:112-163`) suit la doctrine `store:"auto"` du framework :
310
+
311
+ - `auto` (défaut) → suit l'infra database déclarée via `resolveAutoStore` — **borné aux backends
312
+ réellement enregistrés**, repli memory **annoncé** (`tokenService.ts:116-124`) ;
313
+ - store explicite **inconnu** → en prod, **boot avorté** (fail-loud) ; en dev, brique désactivée et
314
+ annoncée avec la liste `listTokenStores()` (`tokenService.ts:127-138`) — jamais de fallback
315
+ memory silencieux pour du durable ;
316
+ - store `memory` **en prod** → `WARNING` nommant l'impact : denylist/refresh/clés API per-pod et
317
+ volatils, révocation non partagée (`tokenService.ts:142-149`).
318
+
319
+ La décision (configuré → résolu, raison) est publiée au kernel par `registerStoreResolution()`
320
+ (`tokenService.ts:151-160`) — visible dans Studio.
321
+
322
+ ### Mise en situation — quel store pour quelle app ?
323
+
324
+ | Ta situation | Store | Pourquoi |
325
+ | --------------------------------------------------- | ---------------- | --------------------------------------------------------------------- |
326
+ | Dev / tests mono-process | `memory` (auto) | 0 dépendance ; volatil — un redémarrage déconnecte tout le monde |
327
+ | Prod, base SQL déclarée (`NF_DATABASE_URL`) | `auto` → drizzle | durable + partagé entre pods : révocation et rejeu vus PARTOUT |
328
+ | Prod, MongoDB | `mongoose` | même contrat, même banc, sur Mongo |
329
+ | Flotte de pods, Redis déjà présent, denylist chaude | `redis` | TTL natif, lecture O(1) ; listing par curseur SCAN (capacité réduite) |
330
+ | Prod **sans** infra durable | ❌ `memory` | ça boote, mais WARNING mérité : la révocation ne traverse pas un pod |
331
+
332
+ ### Les backends (catalogue)
333
+
334
+ ### `memory` — la référence 0 dépendance
335
+
336
+ - Builtin, enregistré à l'import du module via `registerTokenStore` (`tokenStoreRegistry.ts:63-68`).
337
+ - `listPage` : tri `createdAt` DESC + tiebreaker `id`, déterministe pour l'offset — parité SQL
338
+ (`MemoryTokenStore.ts:122-142`).
339
+ - Denylist bornée : purge **amortie** tous les 256 ajouts — `#maybeSweep()`
340
+ (`MemoryTokenStore.ts:360`) + expiration paresseuse à la lecture. Pas de minuterie, pas de fuite.
341
+ - `snapshot()`/`restore()` sérialisables — base d'une persistance fichier, index reconstruits
342
+ (`MemoryTokenStore.ts:253-283`).
343
+ - Volatil, par-process : dev/tests. Pilote le banc de contrat commun.
344
+
345
+ ### `drizzle` — SQL, le défaut durable
346
+
347
+ - Enregistré par le module drizzle (`drizzle/nodefony/registerStores.ts:220`).
348
+ - Élu par `store:"auto"` dès qu'une infra database SQL est déclarée ; sinon sqlite local si drizzle
349
+ est chargé (`config.ts:394-399`).
350
+ - Pagination **offset + total** (helper `paginate()` d'orm-core) ; e2e sur PostgreSQL et MySQL réels.
351
+
352
+ ### `mongoose` — MongoDB
353
+
354
+ - Enregistré par le module mongoose (`mongoose/nodefony/registerStores.ts:119`).
355
+ - Pagination **offset + total** via `listPage` (`MongooseTokenStore.ts:163-173`).
356
+ - Purge par `gc()` explicite sur `expiresAt` (`MongooseTokenStore.ts:196`).
357
+
358
+ ### `redis` — cluster, TTL natif
359
+
360
+ - Enregistré par le module redis (`redis/nodefony/registerStores.ts:49`).
361
+ - TTL natif : `expire()` posé à l'écriture du record (`RedisTokenStore.ts:254`) — l'expiration ne
362
+ dépend pas du gc.
363
+ - Listing par `SCAN` : curseur opaque `skip:scanCursor`, `decodeCursor()`
364
+ (`RedisTokenStore.ts:35-44`) — sans ordre global ni total, capacité réduite **assumée**.
365
+ - `countTokens()` renvoie `-1` : un comptage exact exigerait un SCAN complet O(N), refusé
366
+ (`RedisTokenStore.ts:432`).
367
+
368
+ ### Le record — une seule table pour PAT et refresh
369
+
370
+ `IAccessTokenRecord` (`ITokenStore.ts:69`) est **single-table** : un même schéma porte PAT et
371
+ refresh, champs non pertinents à `null`, horodatages epoch ms (`ITokenStore.ts:60-64`). Deux
372
+ constats de design :
373
+
374
+ - `subjectId` est une **référence logique souple, PAS une FK SQL** : porteur polymorphe
375
+ (user/service), store multi-backend, découplage des modules — intégrité assurée à l'usage
376
+ (`ITokenStore.ts:80-95`) ;
377
+ - slots prêts sans migration : `resources` (permissions fine-grained façon GitHub) et `cnf`
378
+ (sender-constrained DPoP/mTLS) (`ITokenStore.ts:101-119`).
379
+
380
+ Les colonnes par dialecte vivent dans la doc de chaque adapter (règle anti-triple-vérité).
381
+
382
+ ### Lister pour l'admin — pagination native
383
+
384
+ - `listPage()` ne matérialise **jamais** plus d'une page (`ITokenStore.ts:233`) — capacité par
385
+ backend : **offset + total** (SQL/Mongo/mémoire) ou **curseur** (`nextCursor`, Redis).
386
+ - `countTokens()` donne le `total` ; Redis répond `-1` (`ITokenStore.ts:238`).
387
+ - Filtres portables `ITokenListQuery` : `subjectId`, `kind`, `revoked` (`ITokenStore.ts:154-161`) —
388
+ prédicat partagé `matchesTokenQuery()` (`MemoryTokenStore.ts:11-26`).
389
+ - `listAll()` reste réservé au **dump d'incident** cross-porteur, cold-path admin
390
+ (`ITokenStore.ts:222`).
391
+ - Consommateur type : le data plane des clés API — `ApiKeyService.listPagePat()`
392
+ (`apiKeys.ts:216`), jamais un listAll matérialisé.
393
+
394
+ ### Révoquer — trois portées
395
+
396
+ - **Un access** avant son `exp` : denylist `denyJti()`/`isJtiDenied()` (`ITokenStore.ts:251`).
397
+ - **Un refresh/PAT** : `revoke()` idempotent, `revokeFamily()` pour la chaîne de rotation
398
+ (`ITokenStore.ts:242`).
399
+ - **Tout un porteur** (logout global, ban) : seuil `revokeAllForSubject()` — tout access dont
400
+ `iat < invalidBefore` est rejeté (`ITokenStore.ts:226-234`) ; le seuil est **monotone**, deux
401
+ logouts successifs ne le reculent pas (`MemoryTokenStore.ts:229`).
402
+
403
+ ### La maintenance (gc)
404
+
405
+ Le `gc()` du store purge la `denylist` expirée, les records à terme, les PAT révoqués au-delà de
406
+ la rétention (`ITokenStore.ts:237-251`). Orchestré par le `GcScheduler` du service ; `runGc()` reste public pour
407
+ un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:293`).
408
+
409
+ > [!TIP]
410
+ > Un refresh révoqué **par rotation** n'est PAS purgé tout de suite : il est conservé jusqu'à son
411
+ > `expiresAt` — c'est la **fenêtre de détection de rejeu** — puis tombe au gc, `#isPurgeable()`
412
+ > (`MemoryTokenStore.ts:239-248`). Un store **local** (memory) est par-process : seul SON process
413
+ > peut le purger → ne déléguez le gc au cron QUE pour un store partagé.
414
+
415
+ ## ⚙️ Configuration
416
+
417
+ Tables dérivées du schéma Zod — `jwtSchema` (`config.ts:334-390`) et `tokenStoreSchema`
418
+ (`config.ts:392-425`), défauts inclus.
419
+
420
+ ### `jwt.*`
421
+
422
+ <!-- prettier-ignore -->
423
+ | Option | Type | Défaut | Effet |
424
+ | --- | --- | --- | --- |
425
+ | `enabled` | boolean | `true` | Active signature + refresh (`config.ts:317`) |
426
+ | `alg` | `EdDSA` \| `RS256` | `EdDSA` | `RS256` = slot non câblé (`jwtRuntime.ts:21`) |
427
+ | `accessTtlS` | number (s) | `900` | TTL de l'access token — 15 min (`config.ts:364`) |
428
+ | `refreshTtlS` | number (s) | `604800` | TTL du refresh — 7 jours (`config.ts:369`) |
429
+ | `rotateRefresh` | boolean | `true` | Rotation du refresh à chaque usage, OWASP (`config.ts:374`) |
430
+ | `jwks` | boolean | `true` | Publie `/.well-known/jwks.json` + les métadonnées RFC 8414 — sans `issuer` en URL https, rien n'est publié |
431
+ | `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts:384`) |
432
+ | `issuer` | string? | — | Claim `iss`, **STABLE** après émission ; omis → repli `"nodefony"`, qui n'est PAS publiable (RFC 8414 §2 exige une URL https) |
433
+ | `keystore.keySetJson` | string? | — | JWK Set privé injecté depuis l'env — source prod, SECRET (`security/nodefony/config/config.ts:398`) |
434
+ | `keystore.dir` | string? | — | Dossier `keyset.json` chmod 600 — source dev/VPS (`config.ts:376-381`) |
435
+
436
+ ### `tokenStore.*`
437
+
438
+ | Option | Type | Défaut | Effet |
439
+ | ---------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- |
440
+ | `store` | string | `"auto"` | `auto`\|`memory`\|`drizzle`\|`mongoose`\|`redis` — pluggable (`config.ts:394-399`) |
441
+ | `gcIntervalS` | number (s) | `600` | Purge périodique ; `0` = désactivé — chaque process purge SON store (`config.ts:428`) |
442
+ | `gcJitter` | boolean | `true` | Étale le gc d'un délai aléatoire par process — cluster (`config.ts:436`) |
443
+ | `retentionRevokedDays` | number (j) | `30` | Rétention d'un PAT révoqué SANS expiration avant purge (`config.ts:442`) |
444
+
445
+ ## 📜 Normes appliquées
446
+
447
+ | Domaine | Norme | Ancrage |
448
+ | -------------------------------- | --------------- | ------------------------------------------------------- |
449
+ | Réponse d'émission | RFC 6749 §5.1 | `ITokenResponse` (`tokenService.ts:43-51`) |
450
+ | Rotation + détection de rejeu | RFC 9700 §4.14 | `refresh()` (`tokenService.ts:555`) |
451
+ | Profil access token `typ:at+jwt` | RFC 9068 | `#signAccess()` (`tokenService.ts:402-415`) |
452
+ | Claims JWT (`iss/sub/aud/exp`) | RFC 7519 | `#signAccess()` (`tokenService.ts:406-414`) |
453
+ | Ed25519 / JWK / JWKS public | RFC 8037 · 7517 | `#importKeyset()` (`JwtKeystore.ts:156-158`) |
454
+ | Audiences liées à la ressource | RFC 8707 | `audience` du record (`ITokenStore.ts:104-105`) |
455
+ | 429 + `Retry-After` | RFC 6585 | `#renderAuthError()` (`TokenAuthController.ts:108-115`) |
456
+ | Backoff de login | NIST SP 800-63B | `ThrottledError` avant hachage (`tokenService.ts:346`) |
457
+
458
+ ## ⚡ Performance & mémoire
459
+
460
+ - **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts:648`)
461
+ — le boot ne paie rien si le JWT n'est jamais sollicité ; keystore mémoïsé pareil.
462
+ - **Rien sur le hot path requête** : émission et rotation sont des endpoints cold-path ; la
463
+ vérification (hot path) vit chez le `JwtAuthenticator`.
464
+ - **Timers civilisés** : `GcScheduler` `unref` (n'empêche pas l'arrêt) + jitter anti-balayages
465
+ simultanés (`tokenService.ts:175-181`) ; denylist mémoire bornée par purge amortie
466
+ (`MemoryTokenStore.ts:331-341`).
467
+ - **Jamais N en RAM** : `listPage()` borne toute lecture admin à une page (`ITokenStore.ts:233`).
468
+
469
+ ## 📡 Observabilité — Studio
470
+
471
+ - **Écran Stores** (`/nodefony/stores`) : la résolution du store de jetons (configuré → résolu +
472
+ raison) publiée par `registerStoreResolution()` (`tokenService.ts:151-160`).
473
+ - **Écran Audit** (`/nodefony/audit`) : événements `token.issued`, `token.reuse_detected` (signal d'attaque),
474
+ `login.failure`/`login.throttled` du grant — corrélables par `tokenId`.
475
+ - **Écran ApiKeys** : le même store côté PAT, listing paginé serveur.
476
+
477
+ ## ⚠️ Pièges (symptôme → cause → correction)
478
+
479
+ | Symptôme | Cause (dans le code) | Correction |
480
+ | --------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------- |
481
+ | 404 sur `/nodefony/security/api/token` | Routes montées seulement si `tokenService` existe | Charger `@nodefony/security` + `jwt.enabled: true` |
482
+ | 503 « Token issuance unavailable » | Service non initialisé (JWT désactivé, store indisponible) | Vérifier config `jwt`/`tokenStore` + logs de boot |
483
+ | Refresh tokens invalidés à chaque redémarrage | Keystore en mémoire (aucune source configurée) | `jwt.keystore.keySetJson` (prod) ou `dir` (dev) |
484
+ | JWT rejeté après un déploiement multi-pod | Clés différentes par pod (pas de clé partagée) | Provisionner `keySetJson` hors-bande (même clé partout) |
485
+ | Tout rejeté après changement de config | `issuer`/`audiences` divergents entre émission et vérification | `issuer` STABLE — ne pas le changer après émission |
486
+ | Révocation sans effet entre pods | `tokenStore:"memory"` en prod (per-pod) | Store durable (`NF_DATABASE_URL` → drizzle, ou redis) |
487
+ | Reconnexion forcée inattendue | Détection de rejeu : un vieux refresh révoqué a été rejoué | Attendu (anti-vol) — la famille est coupée |
488
+ | Boot avorté « token store inconnu » | `tokenStore.store` explicite introuvable (fail-loud prod) | Corriger le nom / enregistrer le store |
489
+ | Scopes qui n'augmentent pas au refresh | Downscoping volontaire | Réémettre via un nouveau grant pour élargir |
490
+ | Listing admin sans `total` sur Redis | `countTokens()` = `-1` (comptage O(N) refusé), curseur SCAN | Attendu — capacité réduite annoncée, paginer par curseur |
491
+
492
+ ## 🧪 Tests & couverture
493
+
494
+ Cinq familles couvrent la brique — les **chiffres exacts vivent dans la carte de l'aperçu**
495
+ (régénérée par `gen-counters.mjs` depuis vitest, jamais figée ici) :
496
+
497
+ - **unit** : `tokenService` (émission, rotation, rejeu, downscoping), `tokenStore` (révocation,
498
+ denylist, gc, rétention), `jwtKeystore` (les 3 priorités de source), `jwtPipeline` (bout en bout
499
+ signature → vérification), `tokenPagination` (le banc de contrat piloté par le store mémoire) ;
500
+ - **banc de contrat** : `tokenPaginationContract` — invariants `listPage`/`countTokens` tenus par
501
+ **tous** les backends (tri, offset, filtres, curseur) ;
502
+ - **intégration adapters** : token-store + token-pagination chez drizzle (sqlite), mongoose et
503
+ redis — le MÊME banc rejoué sur chaque backend ;
504
+ - **e2e base réelle** : drizzle sur PostgreSQL et MySQL ;
505
+ - **manque assumé** : pas de banc d'attaque dédié à l'émission (le rejeu est couvert en unit) ni de
506
+ test de charge sur le grant.
507
+
508
+ Les bancs sur serveur réel se **skippent sans leurs variables d'infra** — un skip compte comme
509
+ vert : lire le bloc gates (`vitest.gates.ts`, affiché en fin de run) avant de conclure.
510
+ Couverture : `npm run coverage` dans `@nodefony/security`.
511
+
512
+ ## 🔗 Pour aller plus loin
513
+
514
+ - ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
515
+ - 🧭 **Pages sœurs** : [api-keys](api-keys.md) · [Authenticators](authenticators.md)
516
+
517
+ - La vérification de ces jetons (JWT/PAT) → [authenticators](./authenticators.md)
518
+ - Le firewall qui applique la zone → [firewall](./firewall.md) · L'autorisation par scopes → [authorization](./authorization.md)
519
+ - La doctrine `store:"auto"` → [configuration](../../../../../docs/architecture/configuration.md)
520
+ - Vue d'ensemble sécurité → [index](./index.md)