@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
@@ -3,10 +3,9 @@ import { UnverifiableTokenError } from "../../errors/UnverifiableTokenError.js";
3
3
  import { UserToken } from "../token/UserToken.js";
4
4
  import { bearerToken } from "./bearer.js";
5
5
  import peekIssuer from "./peekIssuer.js";
6
- import { localIdentifierFor } from "./externalSubject.js";
7
- import { ACCESS_TOKEN_VERIFIER, buildBearerChallenge, canonicalIssuer, protectedResourceMetadataUrl } from "nodefony";
8
- import { BaseUser } from "@nodefony/user";
9
- //#region nodefony/src/authenticator/ExternalJwtAuthenticator.ts
6
+ import { ExternalSubjectResolver } from "./ExternalSubjectResolver.js";
7
+ import { ACCESS_TOKEN_VERIFIER, buildBearerChallenge, protectedResourceMetadataUrl } from "nodefony";
8
+ // nodefony/src/authenticator/ExternalJwtAuthenticator.ts
10
9
  /**
11
10
  * Message UNIFORME de refus — la cause fine (expiré, audience, signature, sujet
12
11
  * inconnu) part au journal, jamais au client : un refus détaillé est un oracle
@@ -16,6 +15,11 @@ const INVALID_TOKEN = "Invalid token";
16
15
  /** Clé de transport de l'audience entre `createToken` et `authenticate`. */
17
16
  const AUDIENCE = "audience";
18
17
  /**
18
+ * Clé de transport de la zone qui SURCHARGE la politique de rattachement.
19
+ * Posée seulement par ces zones-là : les autres ne paient rien de plus.
20
+ */
21
+ const SUBJECT_AREA = "subjectArea";
22
+ /**
19
23
  * Authentification par **jeton d'accès émis par un serveur d'autorisation
20
24
  * TIERS** (Keycloak, Auth0, Entra, ou l'émetteur d'une flotte d'agents).
21
25
  *
@@ -55,25 +59,15 @@ const AUDIENCE = "audience";
55
59
  var ExternalJwtAuthenticator = class {
56
60
  name = "external-jwt";
57
61
  #container;
58
- #issuers;
59
- #policy;
60
- #ephemeralRoles;
61
- #userProvider = null;
62
+ /** Rattachement du sujet à un compte local — partagé, jamais recopié. */
63
+ #subjects;
62
64
  /**
63
65
  * @param container - container DI (résolution lazy du vérificateur et de `users`)
64
66
  * @param options - émetteurs reconnus et politique de rattachement
65
67
  */
66
68
  constructor(container, options) {
67
69
  this.#container = container;
68
- const issuers = /* @__PURE__ */ new Map();
69
- for (const binding of options.issuers) try {
70
- issuers.set(canonicalIssuer(binding.issuer), binding.subjectMapping);
71
- } catch {
72
- continue;
73
- }
74
- this.#issuers = issuers;
75
- this.#policy = options.subjectPolicy;
76
- this.#ephemeralRoles = options.ephemeralRoles;
70
+ this.#subjects = new ExternalSubjectResolver(container, options);
77
71
  }
78
72
  /**
79
73
  * La requête porte-t-elle un jeton se réclamant d'un émetteur de confiance ?
@@ -89,12 +83,7 @@ var ExternalJwtAuthenticator = class {
89
83
  if (raw === null) return false;
90
84
  const issuer = peekIssuer(raw);
91
85
  if (issuer === null) return false;
92
- if (this.#issuers.has(issuer)) return true;
93
- try {
94
- return this.#issuers.has(canonicalIssuer(issuer));
95
- } catch {
96
- return false;
97
- }
86
+ return this.#subjects.recognizes(issuer);
98
87
  }
99
88
  /**
100
89
  * Extrait le jeton brut ET l'audience de la zone.
@@ -108,6 +97,7 @@ var ExternalJwtAuthenticator = class {
108
97
  const token = new UserToken("external-jwt", bearerToken(auth) ?? "");
109
98
  const area = context.security;
110
99
  if (area?.resource) token.setAttribute(AUDIENCE, area.resource);
100
+ if (area?.subjectPolicy !== void 0) token.setAttribute(SUBJECT_AREA, area);
111
101
  return Promise.resolve(token);
112
102
  }
113
103
  /**
@@ -134,9 +124,7 @@ var ExternalJwtAuthenticator = class {
134
124
  if (principal === null) throw new AuthenticationError(INVALID_TOKEN);
135
125
  const subject = principal.subject;
136
126
  if (!subject) throw new AuthenticationError(INVALID_TOKEN);
137
- const mapping = this.#issuers.get(principal.issuer);
138
- if (mapping === void 0) throw new UnverifiableTokenError(`émetteur « ${principal.issuer} » vérifié mais absent de la table de rattachement des sujets — configuration incohérente.`);
139
- const user = await this.#resolveUser(principal.issuer, subject, mapping);
127
+ const user = await this.#subjects.resolve(principal.issuer, subject, principal.claims, token.getAttribute(SUBJECT_AREA));
140
128
  const ut = token;
141
129
  ut.promote(user);
142
130
  ut.setAttribute("scopes", [...principal.scopes]);
@@ -187,38 +175,5 @@ var ExternalJwtAuthenticator = class {
187
175
  validateArea(area) {
188
176
  if (!area.resource) throw new Error(`area "${area.name}": l'authenticator "${this.name}" exige que la zone déclare sa ressource (\`resource\`) — c'est l'audience que le jeton doit porter (RFC 8707 §2). Sans elle, un jeton valide délivré au même porteur pour un AUTRE service serait accepté ici.`);
189
177
  }
190
- /**
191
- * Établit l'utilisateur applicatif à partir du sujet du jeton.
192
- *
193
- * Les deux politiques répondent à deux questions différentes : « qui, chez
194
- * nous, est cette personne ? » et « quel pouvoir accorde-t-on à une machine
195
- * que l'annuaire a authentifiée ? ».
196
- */
197
- async #resolveUser(issuer, subject, mapping) {
198
- const identifier = localIdentifierFor(issuer, subject, mapping);
199
- if (this.#policy === "ephemeral") return new BaseUser({
200
- id: identifier,
201
- identifier,
202
- roles: [...this.#ephemeralRoles]
203
- });
204
- const provider = this.#resolveUserProvider();
205
- let user;
206
- try {
207
- user = await provider.loadUserByIdentifier(identifier);
208
- } catch {
209
- throw new AuthenticationError(INVALID_TOKEN);
210
- }
211
- if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
212
- return user;
213
- }
214
- #resolveUserProvider() {
215
- if (this.#userProvider === null) {
216
- const provider = this.#container.get("users");
217
- if (!provider) throw new Error("ExternalJwtAuthenticator: aucun service \"users\" (IUserProvider) dans le container — enregistrer un UserService au boot, ou passer la politique de sujet à \"ephemeral\" si l'appelant est une machine sans compte local.");
218
- this.#userProvider = provider;
219
- }
220
- return this.#userProvider;
221
- }
222
178
  };
223
- //#endregion
224
179
  export { ExternalJwtAuthenticator, ExternalJwtAuthenticator as default };
@@ -0,0 +1,180 @@
1
+ import { compileProviderRoleMapping, mapProviderRoles } from "../oauth/providerRoles.js";
2
+ import { AuthenticationError } from "../../errors/AuthenticationError.js";
3
+ import { UnverifiableTokenError } from "../../errors/UnverifiableTokenError.js";
4
+ import { localIdentifierFor } from "./externalSubject.js";
5
+ import { canonicalIssuer } from "nodefony";
6
+ import { BaseUser } from "@nodefony/user";
7
+ // nodefony/src/authenticator/ExternalSubjectResolver.ts
8
+ /**
9
+ * Message UNIFORME de refus — la cause fine (sujet inconnu, compte verrouillé)
10
+ * part au journal, jamais au client : un refus détaillé est un oracle.
11
+ */
12
+ const INVALID_TOKEN = "Invalid token";
13
+ /**
14
+ * Les options de rattachement des sujets externes, lues dans la configuration
15
+ * de sécurité — UNE lecture, pour la zone `external-jwt` comme pour le
16
+ * résolveur de compte d'une ressource protégée.
17
+ *
18
+ * @param config - configuration de sécurité validée
19
+ * @returns émetteurs, politique de sujet, fournisseurs de connexion liables
20
+ */
21
+ function externalSubjectOptions(config) {
22
+ const rs = config.resourceServer;
23
+ return {
24
+ issuers: rs.issuers.map((i) => ({
25
+ issuer: i.issuer,
26
+ subjectMapping: i.subjectMapping
27
+ })),
28
+ subjectPolicy: rs.subjectPolicy,
29
+ ephemeralRoles: rs.ephemeralRoles,
30
+ oauthProviders: Object.entries(config.oauth2.providers).flatMap(([name, p]) => p.issuer ? [{
31
+ name,
32
+ issuer: p.issuer,
33
+ roleMapping: compileProviderRoleMapping(p)
34
+ }] : [])
35
+ };
36
+ }
37
+ /**
38
+ * Établit l'utilisateur applicatif que désigne le sujet d'un jeton TIERS —
39
+ * la décision qui transforme « un annuaire a authentifié ce sujet » en « ce
40
+ * compte agit chez nous ».
41
+ *
42
+ * UNE implémentation, deux consommateurs : la zone `external-jwt` du pare-feu
43
+ * (`ExternalJwtAuthenticator`) et le résolveur de compte d'une ressource
44
+ * protégée (porte MCP). Recopiée, la règle d'espace de noms aurait divergé dès
45
+ * le premier émetteur ajouté — et c'est elle qui empêche un annuaire tiers de
46
+ * réclamer le compte local `admin`.
47
+ */
48
+ var ExternalSubjectResolver = class {
49
+ #container;
50
+ #issuers;
51
+ #policy;
52
+ #ephemeralRoles;
53
+ /** Émetteur canonique → fournisseur de connexion ; `null` = aucun lien. */
54
+ #linkedProviders;
55
+ /** Fournisseur de connexion → table des rôles ; `null` = aucune table. */
56
+ #roleMappings;
57
+ #userProvider = null;
58
+ /**
59
+ * @param container - container DI (résolution lazy de `users`)
60
+ * @param options - émetteurs reconnus et politique de rattachement
61
+ */
62
+ constructor(container, options) {
63
+ this.#container = container;
64
+ const issuers = /* @__PURE__ */ new Map();
65
+ for (const binding of options.issuers) try {
66
+ issuers.set(canonicalIssuer(binding.issuer), binding.subjectMapping);
67
+ } catch {
68
+ continue;
69
+ }
70
+ this.#issuers = issuers;
71
+ this.#policy = options.subjectPolicy;
72
+ this.#ephemeralRoles = options.ephemeralRoles;
73
+ this.#linkedProviders = linkProviders(issuers, options.oauthProviders);
74
+ this.#roleMappings = collectRoleMappings(options.oauthProviders);
75
+ }
76
+ /**
77
+ * L'émetteur revendiqué est-il l'un des nôtres ? Ne lève JAMAIS : la valeur
78
+ * vient d'un jeton que n'importe qui peut forger.
79
+ *
80
+ * @param issuer - émetteur lu dans le jeton, non vérifié
81
+ */
82
+ recognizes(issuer) {
83
+ if (this.#issuers.has(issuer)) return true;
84
+ try {
85
+ return this.#issuers.has(canonicalIssuer(issuer));
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ /**
91
+ * Établit l'utilisateur applicatif à partir du sujet d'un jeton VÉRIFIÉ.
92
+ *
93
+ * Les deux politiques répondent à deux questions différentes : « qui, chez
94
+ * nous, est cette personne ? » et « quel pouvoir accorde-t-on à une machine
95
+ * que l'annuaire a authentifiée ? ».
96
+ *
97
+ * @param issuer - émetteur canonique, tel que le vérificateur l'a établi
98
+ * @param subject - sujet brut (`sub`) chez cet émetteur
99
+ * @param claims - revendications du jeton (rôles d'annuaire)
100
+ * @param area - zone qui SURCHARGE la politique, s'il y en a une
101
+ * @throws AuthenticationError (401) — sujet sans compte local utilisable
102
+ * @throws UnverifiableTokenError (503) — émetteur vérifié mais absent de la
103
+ * table de rattachement : configuration incohérente
104
+ */
105
+ async resolve(issuer, subject, claims, area) {
106
+ const mapping = this.#issuers.get(issuer);
107
+ if (mapping === void 0) throw new UnverifiableTokenError(`émetteur « ${issuer} » vérifié mais absent de la table de rattachement des sujets — configuration incohérente.`);
108
+ const identifier = localIdentifierFor(issuer, subject, mapping);
109
+ if ((area?.subjectPolicy ?? this.#policy) === "ephemeral") return new BaseUser({
110
+ id: identifier,
111
+ identifier,
112
+ roles: [...area?.ephemeralRoles ?? this.#ephemeralRoles]
113
+ });
114
+ const provider = this.#resolveUserProvider();
115
+ let user = null;
116
+ const linked = this.#linkedProviders?.get(issuer);
117
+ if (linked !== void 0) try {
118
+ const roleMapping = this.#roleMappings?.get(linked);
119
+ user = roleMapping !== void 0 && canSyncRoles(provider) ? await provider.syncOAuthRoles(linked, subject, mapProviderRoles(roleMapping, claims)) : await provider.loadUserByOAuth(linked, subject);
120
+ } catch {}
121
+ if (user === null) try {
122
+ user = await provider.loadUserByIdentifier(identifier);
123
+ } catch {
124
+ throw new AuthenticationError(INVALID_TOKEN);
125
+ }
126
+ if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
127
+ return user;
128
+ }
129
+ #resolveUserProvider() {
130
+ if (this.#userProvider === null) {
131
+ const provider = this.#container.get("users");
132
+ if (!provider) throw new Error("ExternalJwtAuthenticator: aucun service \"users\" (IUserProvider) dans le container — enregistrer un UserService au boot, ou passer la politique de sujet à \"ephemeral\" si l'appelant est une machine sans compte local.");
133
+ this.#userProvider = provider;
134
+ }
135
+ return this.#userProvider;
136
+ }
137
+ };
138
+ /** Le fournisseur d'utilisateurs sait-il recalculer les rôles gérés par un annuaire ? */
139
+ function canSyncRoles(provider) {
140
+ return typeof provider.syncOAuthRoles === "function";
141
+ }
142
+ /** Tables de rôles par fournisseur de connexion — `null` quand aucun n'en déclare. */
143
+ function collectRoleMappings(providers) {
144
+ if (!providers) return null;
145
+ let mappings = null;
146
+ for (const { name, roleMapping } of providers) {
147
+ if (roleMapping == null) continue;
148
+ mappings ??= /* @__PURE__ */ new Map();
149
+ mappings.set(name, roleMapping);
150
+ }
151
+ return mappings;
152
+ }
153
+ /**
154
+ * Associe chaque émetteur de confiance au fournisseur de connexion qui partage
155
+ * son émetteur canonique.
156
+ *
157
+ * Un émetteur revendiqué par DEUX fournisseurs n'est lié à aucun : choisir l'un
158
+ * des deux ferait dépendre l'identité de l'ordre de la configuration.
159
+ *
160
+ * @returns la table, ou `null` quand aucun émetteur n'est lié (rien à chercher).
161
+ */
162
+ function linkProviders(trusted, providers) {
163
+ if (!providers || providers.length === 0) return null;
164
+ const links = /* @__PURE__ */ new Map();
165
+ const ambiguous = /* @__PURE__ */ new Set();
166
+ for (const { name, issuer } of providers) {
167
+ let canonical;
168
+ try {
169
+ canonical = canonicalIssuer(issuer);
170
+ } catch {
171
+ continue;
172
+ }
173
+ if (!trusted.has(canonical)) continue;
174
+ if (links.has(canonical)) ambiguous.add(canonical);
175
+ links.set(canonical, name);
176
+ }
177
+ for (const issuer of ambiguous) links.delete(issuer);
178
+ return links.size > 0 ? links : null;
179
+ }
180
+ export { ExternalSubjectResolver, externalSubjectOptions };
@@ -1,8 +1,9 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { isBoundToAccount } from "../accountBinding.js";
2
3
  import { UserRealtimeToken } from "../realtime/UserRealtimeToken.js";
3
4
  import { Nodefony, RequestContext } from "nodefony";
4
- import { anonymousUser } from "@nodefony/user";
5
- //#region nodefony/src/authenticator/FirewallRealtimeAuthenticator.ts
5
+ import { authenticatedUser } from "@nodefony/user";
6
+ // nodefony/src/authenticator/FirewallRealtimeAuthenticator.ts
6
7
  /**
7
8
  * Authenticator realtime des identités résolues par le **firewall** — équivalent
8
9
  * WS de tout ce que le pipeline HTTP sait authentifier.
@@ -57,16 +58,25 @@ var FirewallRealtimeAuthenticator = class {
57
58
  */
58
59
  #resolveStore;
59
60
  /**
61
+ * Accès paresseux à l'annuaire des comptes, pour confirmer avant chaque
62
+ * `api.request` que le compte de la socket existe toujours sous le même `id`.
63
+ * Absent (`null`) → aucune confirmation (seul `isValid` fait foi).
64
+ */
65
+ #resolveUsers;
66
+ /**
60
67
  * @param resolveStore - fournit le store de révocation des jetons (le firewall
61
68
  * passe une closure sur son container). Omis → mode dégradé documenté :
62
69
  * seules les bornes portées par le jeton lui-même sont vérifiables.
70
+ * @param resolveUsers - fournit l'annuaire des comptes (`users`). Omis → pas
71
+ * de confirmation du compte avant `api.request`.
63
72
  */
64
- constructor(resolveStore = null) {
73
+ constructor(resolveStore = null, resolveUsers = null) {
65
74
  this.#resolveStore = resolveStore;
75
+ this.#resolveUsers = resolveUsers;
66
76
  }
67
77
  /** Une identité authentifiée a-t-elle été résolue (par le firewall) au handshake ? */
68
78
  supports(_handshake) {
69
- return isAuthenticatedUser(RequestContext.getUser());
79
+ return authenticatedUser() !== null;
70
80
  }
71
81
  /**
72
82
  * Promeut l'identité déjà résolue (ALS) en jeton realtime — 0 lecture base.
@@ -76,13 +86,61 @@ var FirewallRealtimeAuthenticator = class {
76
86
  * filet défensif fail-closed → le hub ferme la socket en 4001).
77
87
  */
78
88
  async authenticate(_handshake) {
79
- const user = RequestContext.getUser();
80
- if (!isAuthenticatedUser(user)) throw new AuthenticationError("Invalid realtime session");
89
+ const user = authenticatedUser();
90
+ if (user === null) throw new AuthenticationError("Invalid realtime session");
81
91
  const issued = readFirewallToken();
82
- const type = typeof issued?.type === "string" && issued.type ? issued.type : "session";
83
- const revalidate = type === "session" ? buildSessionRevalidator(user.identifier) : this.#buildBearerRevalidator(issued, user.identifier, type);
92
+ const type = issuedType(issued);
93
+ const revalidate = this.#revalidatorFor(user, issued, type);
84
94
  const scopes = issued ? issued.getScopes() : void 0;
85
- return new UserRealtimeToken(user, revalidate, type, scopes);
95
+ return new UserRealtimeToken(user, revalidate, type, scopes, this.#accountConfirmationFor(user, type));
96
+ }
97
+ /**
98
+ * Le revalidateur de l'identité que le firewall a posée sur la requête
99
+ * COURANTE (ALS), pour toute connexion longue — socket WebSocket ou flux SSE.
100
+ *
101
+ * Une seule règle décide qu'une identité est morte, quel que soit le
102
+ * transport qui la porte : c'est celle-ci, et le hub comme le noyau HTTP
103
+ * l'appellent au lieu de la recopier.
104
+ *
105
+ * @returns `null` pour un anonyme (rien à révoquer), sinon la fonction qui
106
+ * dit si l'identité est TOUJOURS valable — fail-closed : `false` quand elle
107
+ * ne peut pas le prouver.
108
+ */
109
+ currentRevalidator() {
110
+ const user = authenticatedUser();
111
+ if (user === null) return null;
112
+ const issued = readFirewallToken();
113
+ return this.#revalidatorFor(user, issued, issuedType(issued));
114
+ }
115
+ /**
116
+ * Choisit la preuve de révocation selon le mode d'authentification : la
117
+ * session se relit, un jeton porteur se borne par `exp` et la denylist.
118
+ */
119
+ /**
120
+ * Confirmation du compte, pour les modes dont l'APPLICATION émet l'artefact
121
+ * (session, JWT maison, clé d'API) : le compte relu doit exister, être actif
122
+ * et porter le même `id` immuable qu'au handshake — sinon il a disparu ou un
123
+ * autre compte a repris son identifiant hors du framework (#139). Un sujet
124
+ * `external-jwt` peut être éphémère, sans compte local : il n'est pas
125
+ * confirmé. `null` = rien à confirmer.
126
+ */
127
+ #accountConfirmationFor(user, type) {
128
+ const resolveUsers = this.#resolveUsers;
129
+ if (resolveUsers === null || !ACCOUNT_CONFIRMED_TYPES.has(type)) return null;
130
+ return async () => {
131
+ const users = resolveUsers();
132
+ if (users === null) return true;
133
+ let current;
134
+ try {
135
+ current = await users.loadUserByIdentifier(user.identifier);
136
+ } catch {
137
+ return false;
138
+ }
139
+ return current.isActive() && !current.isLocked() && isBoundToAccount(current, user.id, user.credentialsVersion);
140
+ };
141
+ }
142
+ #revalidatorFor(user, issued, type) {
143
+ return type === "session" ? buildSessionRevalidator(user.identifier) : this.#buildBearerRevalidator(issued, user.identifier, type);
86
144
  }
87
145
  /**
88
146
  * Re-validation d'une identité portée par un **jeton** (JWT, clé API, OAuth) :
@@ -125,10 +183,23 @@ var FirewallRealtimeAuthenticator = class {
125
183
  }
126
184
  };
127
185
  const EMPTY_CLAIMS = Object.freeze({});
186
+ /** Modes dont l'application émet l'artefact : leur compte est local, donc relisible. */
187
+ const ACCOUNT_CONFIRMED_TYPES = /* @__PURE__ */ new Set([
188
+ "session",
189
+ "jwt",
190
+ "apikey"
191
+ ]);
128
192
  /** Claim temporel JWT (secondes, RFC 7519) → epoch ms, ou `null` si absent/invalide. */
129
193
  function secondsToMs(value) {
130
194
  return typeof value === "number" && Number.isFinite(value) ? value * 1e3 : null;
131
195
  }
196
+ /**
197
+ * Le mode d'authentification du jeton du firewall. Absent (zone historique) →
198
+ * le mode le plus strict, la session : un repli sûr, jamais permissif.
199
+ */
200
+ function issuedType(issued) {
201
+ return typeof issued?.type === "string" && issued.type ? issued.type : "session";
202
+ }
132
203
  /** Le jeton posé dans l'ALS par `firewall.handleSecurity`, s'il y en a un. */
133
204
  function readFirewallToken() {
134
205
  return RequestContext.get()?.token;
@@ -161,14 +232,4 @@ function buildSessionRevalidator(identifier) {
161
232
  }
162
233
  };
163
234
  }
164
- /**
165
- * Vrai si `user` est un utilisateur AUTHENTIFIÉ (≠ anonyme). Le firewall pose
166
- * `token.getUser()` dans l'ALS : soit l'`IUser` réel, soit `anonymousUser`
167
- * (singleton). Tout ce qui n'est pas l'anonyme et porte un `identifier` est
168
- * une identité valide.
169
- */
170
- function isAuthenticatedUser(user) {
171
- return !!user && user !== anonymousUser && typeof user.identifier === "string";
172
- }
173
- //#endregion
174
235
  export { FirewallRealtimeAuthenticator, FirewallRealtimeAuthenticator as default };
@@ -1,8 +1,10 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
+ import { loadBoundAccount } from "../accountBinding.js";
2
3
  import { UserToken } from "../token/UserToken.js";
3
4
  import { bearerToken } from "./bearer.js";
4
5
  import peekIssuer from "./peekIssuer.js";
5
- //#region nodefony/src/authenticator/JwtAuthenticator.ts
6
+ import { canonicalIssuer } from "nodefony";
7
+ // nodefony/src/authenticator/JwtAuthenticator.ts
6
8
  const COMPACT_JWS = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
7
9
  const INVALID_TOKEN = "Invalid token";
8
10
  /**
@@ -82,6 +84,57 @@ var JwtAuthenticator = class {
82
84
  async authenticate(token) {
83
85
  const raw = token.getCredentials();
84
86
  if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
87
+ const { user, payload } = await this.#verify(raw, this.#runtime.audiences);
88
+ return this.#promote(token, user, payload);
89
+ }
90
+ /**
91
+ * Vérifie un jeton émis par CETTE application pour une ressource protégée
92
+ * nommée par l'appelant — la porte d'une ressource (RFC 8707), pas une zone.
93
+ *
94
+ * Les MÊMES règles que {@link authenticate}, appliquées par la même fonction :
95
+ * signature par le keyset local, `iss`, `typ: at+jwt`, révocation ciblée
96
+ * (`jti`) et en masse (`invalidBefore`), compte encore actif. Seule
97
+ * l'audience diffère : c'est celle de la ressource qui appelle, et elle est
98
+ * exigée. Un jeton révoqué, ou celui d'un compte désactivé, n'ouvre donc pas
99
+ * plus une porte MCP qu'une route — ce que la vérification par le seul jeu de
100
+ * clés publiques laissait passer jusqu'à l'expiration.
101
+ *
102
+ * @param raw - le jeton présenté, brut
103
+ * @param audience - l'URI canonique de la ressource qui le reçoit
104
+ * @returns le principal établi, ou `null` pour tout refus (contrat
105
+ * `IAccessTokenVerifier` : un refus n'est jamais une exception)
106
+ * @throws Error si le câblage manque (keystore, store, annuaire) — la porte
107
+ * le traduit en `503`, jamais en jeton accepté
108
+ */
109
+ async verifyForResource(raw, audience) {
110
+ let verified;
111
+ try {
112
+ verified = await this.#verify(raw, audience);
113
+ } catch (error) {
114
+ if (error instanceof AuthenticationError) return null;
115
+ throw error;
116
+ }
117
+ const { payload } = verified;
118
+ const scope = payload.scope;
119
+ return {
120
+ issuer: this.#resourceIssuer(),
121
+ subject: payload.sub,
122
+ scopes: typeof scope === "string" && scope.length > 0 ? scope.split(" ") : [],
123
+ expiresAt: payload.exp,
124
+ issuedAt: payload.iat,
125
+ tokenId: payload.jti,
126
+ claims: payload
127
+ };
128
+ }
129
+ /**
130
+ * La vérification entière d'un jeton maison — UNE implémentation, pour la
131
+ * zone ({@link authenticate}) comme pour la ressource
132
+ * ({@link verifyForResource}).
133
+ *
134
+ * @throws AuthenticationError pour tout refus (message uniforme)
135
+ * @throws Error si le câblage manque
136
+ */
137
+ async #verify(raw, audience) {
85
138
  const jose = this.#jose ??= await import("jose");
86
139
  const getKey = await this.#ensureGetKey(jose);
87
140
  let payload;
@@ -89,7 +142,7 @@ var JwtAuthenticator = class {
89
142
  payload = (await jose.jwtVerify(raw, getKey, {
90
143
  algorithms: ["EdDSA"],
91
144
  issuer: this.#runtime.issuer,
92
- audience: this.#runtime.audiences,
145
+ audience,
93
146
  typ: "at+jwt"
94
147
  })).payload;
95
148
  } catch {
@@ -104,8 +157,31 @@ var JwtAuthenticator = class {
104
157
  if (invalidBefore !== null) {
105
158
  if ((typeof payload.iat === "number" ? payload.iat * 1e3 : 0) < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
106
159
  }
107
- const user = await this.#resolveUserOrReject(sub);
108
- return this.#promote(token, user, payload);
160
+ const user = await loadBoundAccount(this.#resolveUserProvider(), sub, payload["uid"], payload["cv"]);
161
+ if (user === null) throw new AuthenticationError(INVALID_TOKEN);
162
+ return {
163
+ user,
164
+ payload
165
+ };
166
+ }
167
+ /**
168
+ * Recharge le compte que désigne un principal établi par
169
+ * {@link verifyForResource} — pour une ressource protégée qui fait agir un
170
+ * outil au nom du propriétaire du jeton.
171
+ *
172
+ * La MÊME règle que la zone ({@link loadBoundAccount}), rejouée sur les
173
+ * revendications du principal : le vérificateur ne garde pas le compte qu'il
174
+ * a lu, et une seconde lecture de l'annuaire est le coût accepté d'un outil
175
+ * qui la demande — les autres ne la paient pas.
176
+ *
177
+ * @param principal - principal d'un jeton émis par CETTE application
178
+ * @returns le compte, ou `null` s'il n'est pas (ou plus) celui du jeton
179
+ * @throws Error si l'annuaire des comptes n'est pas câblé
180
+ */
181
+ resolveAccount(principal) {
182
+ const subject = principal.subject;
183
+ if (principal.issuer !== this.#resourceIssuer() || typeof subject !== "string" || subject.length === 0) return Promise.resolve(null);
184
+ return loadBoundAccount(this.#resolveUserProvider(), subject, principal.claims?.["uid"], principal.claims?.["cv"]);
109
185
  }
110
186
  /** Slot audit (J4b). */
111
187
  onSuccess(_context, _token) {
@@ -119,6 +195,17 @@ var JwtAuthenticator = class {
119
195
  challenge() {
120
196
  return "Bearer";
121
197
  }
198
+ /**
199
+ * L'émetteur que portent les principaux de {@link verifyForResource} — et
200
+ * donc celui qu'un principal doit porter pour être un jeton maison.
201
+ */
202
+ #resourceIssuer() {
203
+ try {
204
+ return canonicalIssuer(this.#runtime.issuer);
205
+ } catch {
206
+ return this.#runtime.issuer;
207
+ }
208
+ }
122
209
  /** Construit (une fois) le résolveur de clé par `kid` depuis le JWKS public. */
123
210
  async #ensureGetKey(jose) {
124
211
  if (this.#getKey !== null) return this.#getKey;
@@ -134,19 +221,9 @@ var JwtAuthenticator = class {
134
221
  ut.setAttribute("scopes", scopes);
135
222
  ut.setAttribute("jti", payload.jti);
136
223
  ut.setAttribute("claims", payload);
224
+ ut.setAttribute("issuer", payload.iss);
137
225
  return ut;
138
226
  }
139
- async #resolveUserOrReject(sub) {
140
- const provider = this.#resolveUserProvider();
141
- let user;
142
- try {
143
- user = await provider.loadUserByIdentifier(sub);
144
- } catch {
145
- throw new AuthenticationError(INVALID_TOKEN);
146
- }
147
- if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
148
- return user;
149
- }
150
227
  #resolveKeystore() {
151
228
  if (this.#keystore === null) {
152
229
  const ks = this.#container.get("jwtKeystore");
@@ -172,5 +249,4 @@ var JwtAuthenticator = class {
172
249
  return this.#userProvider;
173
250
  }
174
251
  };
175
- //#endregion
176
252
  export { JwtAuthenticator, JwtAuthenticator as default };
@@ -1,7 +1,8 @@
1
1
  import { AuthenticationError } from "../../errors/AuthenticationError.js";
2
- import { resolveSessionIdentity } from "../sessionIdentity.js";
2
+ import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY } from "../accountBinding.js";
3
+ import { resolveBoundSessionIdentity } from "../sessionIdentity.js";
3
4
  import { UserToken } from "../token/UserToken.js";
4
- //#region nodefony/src/authenticator/SessionAuthenticator.ts
5
+ // nodefony/src/authenticator/SessionAuthenticator.ts
5
6
  /**
6
7
  * Authentification par **session serveur** (cookie opaque, modèle BFF) — la
7
8
  * preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
@@ -59,7 +60,11 @@ var SessionAuthenticator = class {
59
60
  }
60
61
  /** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
61
62
  createToken(context) {
62
- const credentials = { identifier: context.session?.user ?? "" };
63
+ const credentials = {
64
+ identifier: context.session?.user ?? "",
65
+ accountId: context.session?.getMetaBag(ACCOUNT_ID_KEY),
66
+ credentialsVersion: context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)
67
+ };
63
68
  return Promise.resolve(new UserToken(this.name, credentials));
64
69
  }
65
70
  /**
@@ -74,7 +79,7 @@ var SessionAuthenticator = class {
74
79
  const credentials = token.getCredentials();
75
80
  if (!credentials?.identifier) throw new AuthenticationError("Invalid session");
76
81
  const provider = this.#provider ??= this.#resolveProvider();
77
- const user = await resolveSessionIdentity(provider, credentials.identifier);
82
+ const user = await resolveBoundSessionIdentity(provider, credentials.identifier, credentials.accountId, credentials.credentialsVersion);
78
83
  return token.promote(user);
79
84
  }
80
85
  /**
@@ -90,5 +95,4 @@ var SessionAuthenticator = class {
90
95
  return Promise.resolve();
91
96
  }
92
97
  };
93
- //#endregion
94
98
  export { SessionAuthenticator, SessionAuthenticator as default };