@nodefony/security 10.0.0-beta.1 → 10.0.0-beta.3

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 (164) hide show
  1. package/README.md +7 -5
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +44 -10
  5. package/dist/nodefony/command/security-oauth-doctor.js +66 -0
  6. package/dist/nodefony/command/security-secrets.js +42 -25
  7. package/dist/nodefony/command/security-token.js +46 -15
  8. package/dist/nodefony/command/security-user-delete.js +5 -4
  9. package/dist/nodefony/command/security-user-password.js +4 -11
  10. package/dist/nodefony/config/config.js +80 -5
  11. package/dist/nodefony/contracts/index.js +1 -2
  12. package/dist/nodefony/errors/InsecureTransportError.js +24 -0
  13. package/dist/nodefony/errors/index.js +2 -1
  14. package/dist/nodefony/keycloak/keycloakAdmin.js +255 -0
  15. package/dist/nodefony/keycloak/keycloakRealm.js +260 -0
  16. package/dist/nodefony/keycloak/keycloakRealmInput.js +124 -0
  17. package/dist/nodefony/keycloak/scaffold.js +231 -0
  18. package/dist/nodefony/keycloak/security-keycloak-realm.js +128 -0
  19. package/dist/nodefony/scaffold/contribute.js +20 -0
  20. package/dist/nodefony/service/authFlow.js +111 -11
  21. package/dist/nodefony/service/firewall.js +111 -21
  22. package/dist/nodefony/service/keycloak.js +209 -0
  23. package/dist/nodefony/service/oauth2.js +379 -26
  24. package/dist/nodefony/service/securityHeaders.js +12 -1
  25. package/dist/nodefony/service/tokenService.js +57 -6
  26. package/dist/nodefony/service/totp.js +51 -6
  27. package/dist/nodefony/service/webAuthn.js +20 -2
  28. package/dist/nodefony/src/SecuredArea.js +14 -3
  29. package/dist/nodefony/src/admin/SecurityAdminApi.js +70 -40
  30. package/dist/nodefony/src/admin/adminAudit.js +0 -1
  31. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  32. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +66 -6
  33. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +38 -15
  34. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +1 -0
  35. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +13 -3
  36. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +9 -2
  37. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  38. package/dist/nodefony/src/boot/publicRoutesNotice.js +27 -0
  39. package/dist/nodefony/src/entryPoint.js +39 -0
  40. package/dist/nodefony/src/oauth/clientCredentials.js +40 -0
  41. package/dist/nodefony/src/oauth/metadata.js +36 -9
  42. package/dist/nodefony/src/oauth/oauth2Client.js +60 -33
  43. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +39 -6
  44. package/dist/nodefony/src/oauth/providerDiagnosis.js +283 -0
  45. package/dist/nodefony/src/oauth/providerRoles.js +79 -0
  46. package/dist/nodefony/src/oauth/providers/github.js +32 -6
  47. package/dist/nodefony/src/oauth/providers/oidc.js +82 -3
  48. package/dist/nodefony/src/openToSignup.js +47 -0
  49. package/dist/nodefony/src/sessionAuthentication.js +26 -0
  50. package/dist/nodefony/src/token/JwtKeystore.js +78 -46
  51. package/dist/nodefony/src/token/RemoteJwtVerifier.js +40 -13
  52. package/dist/nodefony/src/token/secretFile.js +59 -2
  53. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +51 -0
  54. package/dist/types/index.d.ts +26 -6
  55. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  56. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  57. package/dist/types/nodefony/command/security-token.d.ts +9 -0
  58. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  59. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  60. package/dist/types/nodefony/config/config.d.ts +63 -1
  61. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  62. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  63. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  64. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  65. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  66. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  67. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  68. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  69. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  70. package/dist/types/nodefony/errors/index.d.ts +1 -0
  71. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  72. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  73. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  74. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  75. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  76. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  77. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  78. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  79. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  80. package/dist/types/nodefony/service/oauth2.d.ts +125 -9
  81. package/dist/types/nodefony/service/securityHeaders.d.ts +2 -1
  82. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  83. package/dist/types/nodefony/service/totp.d.ts +28 -4
  84. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  85. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  86. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +54 -12
  87. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +18 -0
  88. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +13 -0
  89. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  90. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  91. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  92. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  93. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  94. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  95. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  96. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  97. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  98. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  99. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  100. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  101. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  102. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  103. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  104. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  105. package/docs/api-keys.md +18 -18
  106. package/docs/audit.md +24 -22
  107. package/docs/authenticators.md +62 -29
  108. package/docs/authorization.md +16 -16
  109. package/docs/cors.md +30 -30
  110. package/docs/csrf.md +35 -33
  111. package/docs/external-jwt.md +68 -5
  112. package/docs/firewall.md +78 -45
  113. package/docs/headers.md +35 -35
  114. package/docs/index.md +15 -7
  115. package/docs/keycloak.md +818 -0
  116. package/docs/lexique.md +15 -15
  117. package/docs/login-page.md +361 -0
  118. package/docs/oauth2.md +242 -125
  119. package/docs/obtenir-un-jeton.md +18 -11
  120. package/docs/tokens.md +138 -86
  121. package/docs/totp.md +113 -71
  122. package/docs/webauthn.md +115 -115
  123. package/docs/webhooks.md +21 -21
  124. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  125. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  126. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  127. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  128. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  129. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  130. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  131. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  132. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  133. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  134. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  135. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  136. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  137. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  138. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  139. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  140. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  141. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  142. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  143. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  144. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  145. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  146. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  147. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  148. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  149. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  150. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  151. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  152. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  153. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  154. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  155. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  156. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  157. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  158. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  159. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  160. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  161. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  162. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  163. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  164. package/package.json +17 -15
@@ -1,4 +1,31 @@
1
1
  import { Service, Module } from "nodefony";
2
+ import { type IOAuthDiagnosis } from "../src/oauth/providerDiagnosis.js";
3
+ /**
4
+ * Ce que la session retient d'un login fédéré pour pouvoir fermer AUSSI la
5
+ * session du fournisseur (RP-Initiated Logout). Stocké côté serveur ; l'ID
6
+ * token ne traverse le navigateur qu'une fois, dans l'adresse de déconnexion
7
+ * (`id_token_hint`), la session locale déjà détruite.
8
+ *
9
+ * @remarks L'ID token est une exception assumée à « les jetons du fournisseur
10
+ * ne sont pas conservés » : il n'ouvre aucun accès (ni API, ni rafraîchissement),
11
+ * il désigne la session à fermer. Jetons d'accès et de rafraîchissement restent
12
+ * jetés.
13
+ */
14
+ export interface IFederatedLogoutHint {
15
+ /** Fournisseur qui a ouvert la session. */
16
+ readonly provider: string;
17
+ /** ID token reçu au login, rejoué en `id_token_hint`. */
18
+ readonly idToken: string;
19
+ /** Identifiant de session du fournisseur (claim `sid`), ou `null`. */
20
+ readonly sid: string | null;
21
+ }
22
+ /** Vue minimale d'une session — celle que porte le contexte. */
23
+ interface IHintSession {
24
+ get(key: string): unknown;
25
+ set(key: string, value: unknown): unknown;
26
+ /** Métadonnée énumérable — absente d'une session réduite (tests). */
27
+ setMetaBag?(key: string, value: unknown): unknown;
28
+ }
2
29
  /**
3
30
  * Libellé affichable d'un fournisseur, quand sa configuration n'en donne pas.
4
31
  *
@@ -20,6 +47,8 @@ export interface IOAuthDisplayProvider {
20
47
  readonly name: string;
21
48
  /** Libellé du bouton : celui de la config, sinon dérivé du nom. */
22
49
  readonly label: string;
50
+ /** Image du bouton (`icon` de la config) — absente = icône du framework. */
51
+ readonly icon?: string;
23
52
  }
24
53
  /** Données à porter en session entre `authorize` et `callback` (anti-replay). */
25
54
  export interface IOAuthAuthorization {
@@ -30,20 +59,40 @@ export interface IOAuthAuthorization {
30
59
  /** `code_verifier` PKCE à stocker en session, ou `null` (fournisseur sans PKCE). */
31
60
  readonly codeVerifier: string | null;
32
61
  }
62
+ /**
63
+ * Confronte l'émetteur configuré d'un fournisseur à ce que sa fabrique exige.
64
+ *
65
+ * Une URL qui ne peut pas être un émetteur est refusée QUEL QUE SOIT le
66
+ * fournisseur : aucune valeur mal formée n'est une configuration voulue. Une
67
+ * absence ne l'est que si la fabrique a déclaré l'émetteur requis.
68
+ *
69
+ * @param name - nom du fournisseur (`oauth2.providers.<name>`).
70
+ * @param issuer - émetteur lu dans sa configuration.
71
+ * @throws BootConfigurationError - émetteur requis absent, ou mal formé.
72
+ */
73
+ export declare function checkProviderIssuer(name: string, issuer: string | undefined): void;
33
74
  /**
34
75
  * **Social login OAuth 2.0** (P6 J9) — orchestrateur du flux *Authorization Code*.
35
76
  *
36
77
  * Posture OAuth 2.1 (RFC 9700) : Authorization Code uniquement (jamais implicit /
37
78
  * ROPC), **PKCE S256** quand le fournisseur le supporte (RFC 7636), **state**
38
- * anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton n'atteint le navigateur
39
- * (le login produit une **session BFF**, gérée hors de ce service par le
40
- * controller + `AuthFlow`).
79
+ * anti-CSRF, **iss** anti-mix-up (RFC 9207) ; aucun jeton d'accès n'atteint le
80
+ * navigateur (le login produit une **session BFF**, gérée hors de ce service par
81
+ * le controller + `AuthFlow`) — seul l'ID token y passe, une fois, à la
82
+ * déconnexion ({@link IFederatedLogoutHint}).
41
83
  *
42
- * Les fournisseurs sont construits **au premier login** (cold path — jamais au boot
43
- * ni par requête) puis mémoïsés : c'est là que les points d'entrée d'un émetteur
44
- * OIDC sont découverts, une seule fois par processus. Au boot (si `oauth2.enabled`)
45
- * : seule la config est validée et les fournisseurs configurés sont confrontés au
46
- * registre (un nom inconnu = WARNING, pas fatal).
84
+ * Au boot (si `oauth2.enabled`), la configuration de chaque fournisseur est
85
+ * confrontée à ce que sa fabrique exige : un émetteur requis absent, ou qui n'est
86
+ * pas une URL d'émetteur valide, INTERROMPT le démarrage en nommant la clé — une
87
+ * faute d'écriture ne doit pas attendre le premier clic pour se montrer. Un nom
88
+ * inconnu du registre reste un WARNING.
89
+ *
90
+ * Les fournisseurs sont construits une fois puis mémoïsés : c'est là que les
91
+ * points d'entrée d'un émetteur OIDC sont découverts. Un run qui sert (ports
92
+ * ouverts) les construit DÈS le boot, sans l'attendre : un émetteur injoignable
93
+ * n'est pas une faute de configuration, il se journalise et son bouton quitte
94
+ * l'écran de connexion jusqu'à ce qu'une nouvelle tentative réussisse. Un run
95
+ * console ne touche jamais le réseau.
47
96
  *
48
97
  * Le service ne touche **ni HTTP ni session** : il rend à l'appelant les éléments
49
98
  * (URL, state, verifier) que le controller persiste en session — testable sans
@@ -78,6 +127,9 @@ declare class OAuth2Service extends Service {
78
127
  /**
79
128
  * Redirections post-login (succès / échec) — lues par le controller.
80
129
  * Surcharge PAR FOURNISSEUR si fournie, sinon valeur globale, sinon défaut.
130
+ *
131
+ * @remarks L'échec mène par défaut à la page de connexion (`loginPage.path`) :
132
+ * déplacer la page déplace la redirection, les deux ne peuvent pas diverger.
81
133
  */
82
134
  getRedirects(provider?: string): {
83
135
  success: string;
@@ -87,9 +139,13 @@ declare class OAuth2Service extends Service {
87
139
  * Étape 1 — prépare l'URL d'autorisation + les éléments anti-replay à stocker
88
140
  * en session (`state`, et `code_verifier` si PKCE).
89
141
  *
142
+ * @param options.theme - thème rendu par l'application, transmis au seul
143
+ * fournisseur qui sait l'afficher (Keycloak) ; les autres l'ignorent
90
144
  * @throws AuthenticationError — fournisseur non configuré / inconnu du registre.
91
145
  */
92
- createAuthorization(provider: string): Promise<IOAuthAuthorization>;
146
+ createAuthorization(provider: string, options?: {
147
+ theme?: "light" | "dark";
148
+ }): Promise<IOAuthAuthorization>;
93
149
  /**
94
150
  * Étape 2 — valide la réponse, échange le `code`, lit le profil et provisionne
95
151
  * l'utilisateur local (Shadow User). Retourne l'identifiant à ouvrir en session.
@@ -100,7 +156,67 @@ declare class OAuth2Service extends Service {
100
156
  */
101
157
  exchangeAndProvision(provider: string, code: string, codeVerifier: string | null, returnedIss: string | null): Promise<{
102
158
  identifier: string;
159
+ logoutHint: IFederatedLogoutHint | null;
103
160
  }>;
161
+ /**
162
+ * Retient en session l'indice de déconnexion d'un login fédéré — à appeler
163
+ * APRÈS l'ouverture de la session authentifiée (l'ID de session est
164
+ * régénéré à l'ouverture).
165
+ *
166
+ * @param session - session authentifiée de la requête.
167
+ * @param hint - indice rendu par {@link exchangeAndProvision}, ou `null`.
168
+ */
169
+ rememberLogout(session: IHintSession, hint: IFederatedLogoutHint | null): void;
170
+ /**
171
+ * Adresse de déconnexion chez le fournisseur qui a ouvert la session, ou
172
+ * `null` : session locale, fournisseur sans point de déconnexion, ou
173
+ * fournisseur devenu indisponible. À lire AVANT de détruire la session.
174
+ *
175
+ * @param session - session courante, ou `null`.
176
+ * @returns l'URL vers laquelle envoyer le navigateur, ou `null`.
177
+ */
178
+ logoutUrlFor(session: IHintSession | null): Promise<string | null>;
179
+ /**
180
+ * Déconnexion demandée PAR le fournisseur sur le canal arrière (OpenID
181
+ * Connect Back-Channel Logout 1.0) : vérifie le jeton, refuse un rejeu, puis
182
+ * détruit les sessions locales qu'il désigne.
183
+ *
184
+ * Une session est désignée si elle a été ouverte par CE fournisseur (index
185
+ * fédéré posé au login par {@link rememberLogout}) et, selon le jeton : porte son `sid` ; ou,
186
+ * sans `sid`, appartient à l'utilisateur lié à son `sub`. Une session ouverte
187
+ * par mot de passe ou par un autre fournisseur n'est jamais touchée — le
188
+ * fournisseur ne ferme que ce qu'il a ouvert.
189
+ *
190
+ * @remarks Les connexions WebSocket portées par une session détruite se
191
+ * ferment au tick de revalidation du hub temps réel (code `4001`).
192
+ *
193
+ * @param provider - fournisseur désigné par l'adresse appelée.
194
+ * @param logoutToken - paramètre `logout_token` reçu, brut.
195
+ * @returns `unsupported` si le fournisseur ne sait pas émettre de jeton de
196
+ * déconnexion ; `refused` si le jeton est invalide ou rejoué ; sinon le
197
+ * nombre de sessions détruites (0 : déjà déconnecté, c'est un succès §2.7).
198
+ * @throws Error - vérification impossible (jeu de clés injoignable) ou
199
+ * sessions impossibles à parcourir : la déconnexion a ÉCHOUÉ.
200
+ */
201
+ backchannelLogout(provider: string, logoutToken: string): Promise<{
202
+ readonly outcome: "unsupported" | "refused";
203
+ } | {
204
+ readonly outcome: "done";
205
+ readonly destroyed: number;
206
+ }>;
207
+ /**
208
+ * Diagnostique les fournisseurs CONFIGURÉS sans connexion humaine : découverte
209
+ * et émetteur, URL de retour acceptée, secret accepté (cf
210
+ * `diagnoseOAuthProvider`).
211
+ *
212
+ * Le fournisseur est construit à NEUF, hors de la mémoïsation du login : un
213
+ * diagnostic doit voir le serveur tel qu'il est maintenant, et son échec ne
214
+ * doit pas retirer un bouton de l'écran de connexion.
215
+ *
216
+ * @param name - un seul fournisseur ; à défaut, tous ceux de la configuration.
217
+ * @returns un diagnostic par fournisseur ; vide si le social login est éteint.
218
+ */
219
+ diagnose(name?: string): Promise<IOAuthDiagnosis[]>;
104
220
  }
105
221
  export default OAuth2Service;
106
222
  export { OAuth2Service };
@@ -43,7 +43,8 @@ export declare class SecurityHeaders {
43
43
  get hasNonce(): boolean;
44
44
  /**
45
45
  * Recompose le CSP en injectant le `nonce` de la requête aux emplacements
46
- * `{{nonce}}`. 1 `join` (segments pré-split au boot). Le nonce est base64 (jamais
46
+ * `{{nonce}}` : une concaténation s'il n'y en a qu'un, un `join` sinon (segments
47
+ * pré-split au boot). Le nonce est base64 (jamais
47
48
  * `'`/`;`/espace) → aucune évasion possible du token CSP.
48
49
  *
49
50
  * @param nonce - nonce base64 de la requête (`Context.cspNonce`).
@@ -1,6 +1,7 @@
1
1
  import { Service, Module } from "nodefony";
2
2
  import type * as Jose from "jose";
3
3
  import type { IUser } from "@nodefony/user";
4
+ import type { ICredentialTransportContext } from "../src/transport/CredentialTransportPolicy.js";
4
5
  /**
5
6
  * Réponse d'émission de jetons — nommage RFC 6749 §5.1 (snake_case, JSON).
6
7
  * Le JWT part en `Authorization: Bearer`, JAMAIS en cookie ni en URL.
@@ -76,10 +77,15 @@ declare class TokenService extends Service {
76
77
  * Émet un couple access/refresh après vérification d'un credential
77
78
  * identifiant/mot de passe (grant M2M/CLI). Throttling NIST partagé si activé.
78
79
  *
80
+ * @param context - requête HTTP qui porte le credential. Présente, elle
81
+ * soumet l'appel au refus du clair en production ; absente (CLI, appel
82
+ * programmatique), il n'y a pas de transport à juger. Un endpoint HTTP DOIT
83
+ * la passer.
84
+ * @throws InsecureTransportError (403) — credential reçu en clair en production.
79
85
  * @throws ThrottledError (429) — backoff actif.
80
86
  * @throws AuthenticationError (401, message uniforme) — credential invalide.
81
87
  */
82
- issueForCredentials(identifier: unknown, password: unknown, requestedScopes?: string[], resource?: unknown): Promise<ITokenResponse>;
88
+ issueForCredentials(identifier: unknown, password: unknown, requestedScopes?: string[], resource?: unknown, context?: ICredentialTransportContext): Promise<ITokenResponse>;
83
89
  issueTokens(user: IUser, requestedScopes?: string[], resource?: unknown, accessTtlS?: number): Promise<ITokenResponse>;
84
90
  /**
85
91
  * Rotation d'un refresh token (RFC 9700 §4.14) : valide le refresh présenté,
@@ -27,12 +27,36 @@ declare class TotpService extends Service {
27
27
  isEnabled(): boolean;
28
28
  /** Démarre l'enrôlement (secret + QR affichés 1×). */
29
29
  beginEnrollment(userId: string, account: string): Promise<ITotpEnrollment>;
30
- /** Confirme l'enrôlement par un 1ᵉʳ code → active + codes de récupération clairs. */
31
- confirmEnrollment(userId: string, code: string): Promise<ITotpActivation>;
30
+ /**
31
+ * Confirme l'enrôlement par un 1ᵉʳ code → active + codes de récupération clairs.
32
+ *
33
+ * Activer un second facteur est un geste de SÉCURITÉ : il est audité ici
34
+ * (`user.totp_enabled`), avec la requête qui l'a porté — sans trace, un facteur
35
+ * posé à l'insu du titulaire ne se retrouve pas.
36
+ *
37
+ * @param userId - compte dont on active le 2FA (le porteur de la session).
38
+ * @param code - premier code de l'application d'authentification.
39
+ * @param context - contexte de la requête (IP, navigateur, identifiant de
40
+ * requête) recopié dans l'événement d'audit ; absent, l'événement part sans.
41
+ * @returns les codes de récupération, affichés une seule fois.
42
+ * @throws quand le code est faux, l'enrôlement absent ou déjà confirmé.
43
+ */
44
+ confirmEnrollment(userId: string, code: string, context?: unknown): Promise<ITotpActivation>;
32
45
  /** Vérifie un second facteur au login (code TOTP ou code de récupération). */
33
46
  verifyLogin(userId: string, code: string): Promise<ITotpLoginResult>;
34
- /** Désactive le 2FA d'un utilisateur. */
35
- disable(userId: string): Promise<void>;
47
+ /**
48
+ * Désactive le 2FA d'un utilisateur.
49
+ *
50
+ * Avec un `context`, c'est le TITULAIRE qui retire son facteur (porte
51
+ * libre-service) : le geste est audité ici (`user.totp_disabled`). Sans
52
+ * contexte, l'appelant porte lui-même l'audit — le plan d'administration le
53
+ * fait avec l'identité de l'administrateur (`viaAdmin`), qu'ici on ignore.
54
+ * Un seul événement par geste, jamais deux.
55
+ *
56
+ * @param userId - compte dont on retire le 2FA.
57
+ * @param context - contexte de la requête du titulaire (audit libre-service).
58
+ */
59
+ disable(userId: string, context?: unknown): Promise<void>;
36
60
  /** État 2FA d'un utilisateur (absent / pending / activé + codes restants). */
37
61
  status(userId: string): Promise<ITotpStatus>;
38
62
  /**
@@ -5,13 +5,35 @@ import type { IWebAuthnCredential } from "../contracts/IWebAuthnCredential.js";
5
5
  import type { IWebAuthnCredentialSummary, IWebAuthnListQuery } from "../contracts/IWebAuthnCredentialStore.js";
6
6
  /** Sujet d'une cérémonie d'enregistrement — l'utilisateur qui crée un passkey. */
7
7
  export interface IWebAuthnUser {
8
- /** Identifiant applicatif stable (sub / username) — devient le `userHandle`. */
8
+ /**
9
+ * Clé de propriétaire = `IUser.identifier` — range les credentials
10
+ * (`findByUser`), cf `IWebAuthnCredentialStore`. N'est PAS le `userHandle`.
11
+ */
9
12
  readonly id: string;
13
+ /**
14
+ * Source du `userHandle` WebAuthn : l'`IUser.id` (UUID) du compte. WebAuthn
15
+ * L3 (§4, « User Handle ») l'exige opaque, d'au plus 64 octets, et « MUST NOT
16
+ * contain personally identifying information » — l'identifiant de connexion,
17
+ * souvent un email, est donc exclu. Omis (ou trop long), le handle est le
18
+ * condensat SHA-256 de {@link IWebAuthnUser.id} : 32 octets, jamais l'identifiant
19
+ * en clair. Le handle n'est jamais relu par le serveur — l'authentification
20
+ * retrouve le compte par l'id du credential stocké.
21
+ */
22
+ readonly handle?: string;
10
23
  /** Nom de compte affiché par l'OS (email, identifiant…). */
11
24
  readonly name: string;
12
25
  /** Nom complet optionnel (UX de l'invite système). */
13
26
  readonly displayName?: string;
14
27
  }
28
+ /**
29
+ * Octets du `userHandle` d'une cérémonie d'enregistrement — opaques, bornés, sans
30
+ * donnée personnelle (cf {@link IWebAuthnUser.handle}).
31
+ *
32
+ * @param user - sujet de la cérémonie.
33
+ * @returns `handle` encodé s'il est fourni et tient en 64 octets, sinon le
34
+ * condensat SHA-256 de l'`id`.
35
+ */
36
+ export declare function userHandleBytes(user: IWebAuthnUser): Uint8Array<ArrayBuffer>;
15
37
  /** Résultat d'une authentification WebAuthn vérifiée. */
16
38
  export interface IWebAuthnAssertionResult {
17
39
  /** Le credential résolu (état déjà mis à jour : compteur, sauvegarde, usage). */
@@ -9,6 +9,7 @@ import type { ISecurityAreaConfig } from "../config/defineModuleConfig.js";
9
9
  * spécificité au boot.
10
10
  */
11
11
  export declare class SecuredArea implements ISecuredArea {
12
+ #private;
12
13
  readonly name: string;
13
14
  readonly pattern: RegExp;
14
15
  readonly security: boolean;
@@ -18,6 +19,9 @@ export declare class SecuredArea implements ISecuredArea {
18
19
  readonly host?: string | undefined;
19
20
  readonly realtime: boolean;
20
21
  readonly resource?: string | undefined;
22
+ readonly entryPoint?: string | undefined;
23
+ readonly subjectPolicy?: "require" | "ephemeral" | undefined;
24
+ readonly ephemeralRoles?: readonly string[] | undefined;
21
25
  /**
22
26
  * Rôles exigés par défaut dans la zone, ou `null` quand elle n'en exige
23
27
  * aucun. `null` plutôt qu'un tableau vide : le hot path teste une référence,
@@ -1,31 +1,73 @@
1
- import type { Container } from "nodefony";
1
+ import type { Container, IBootNotice } from "nodefony";
2
2
  import { type IUserRevokedEvent } from "@nodefony/user";
3
+ /** Résolution d'un service par nom — tout ce que la purge demande au container. */
4
+ interface ServiceLookupLike {
5
+ get(name: string): unknown;
6
+ }
3
7
  /** Bus d'événements minimal (kernel) capable d'abonner un handler. */
4
8
  interface KernelListenerLike {
5
- on(event: string, handler: (...args: unknown[]) => void): unknown;
9
+ on(event: string, handler: (...args: unknown[]) => unknown): unknown;
6
10
  }
7
11
  /**
8
12
  * Cascade de révocation déclenchée par {@link USER_REVOKED_EVENT} : éjecte
9
13
  * **immédiatement** les artefacts d'accès du porteur — ses **sessions** (http,
10
14
  * `destroyByUser`) et ses **jetons/PAT** (`tokenStore.revokeAllForSubject`,
11
- * seuil `invalidBefore`). Best-effort par brique (une indispo n'empêche pas
12
- * l'autre) — l'accès était DÉJÀ neutralisé par le re-fetch des authenticators,
13
- * cette cascade est de la **propreté + défense en profondeur**, jamais l'unique
14
- * rempart. `tenantId` du payload est réservé (scoping non câblé en mono-tenant).
15
+ * seuil `invalidBefore`, qui couvre aussi les clés d'API). Best-effort par
16
+ * brique (une indispo n'empêche pas l'autre) — l'accès était DÉJÀ neutralisé par
17
+ * le re-fetch des authenticators. `tenantId` du payload est réservé (scoping non
18
+ * câblé en mono-tenant).
19
+ *
20
+ * **Suppression du compte** (`reason: "deleted"`) : retire AUSSI ses facteurs —
21
+ * secret TOTP (codes de récupération compris) et passkeys. Ils sont rangés sous
22
+ * l'`identifier`, qu'un compte recréé peut reprendre : laissés en place, le
23
+ * nouveau titulaire hériterait du second facteur de l'ancien. Un compte
24
+ * seulement désactivé ou verrouillé les GARDE — il les retrouve à sa
25
+ * réactivation.
15
26
  *
16
- * @param container - container du kernel (résolution lazy de `sessions`/`tokenStore`).
27
+ * @param container - container du kernel (résolution lazy de `sessions`, `tokenStore`, `totp`, `webauthn`).
17
28
  * @param event - charge utile de l'événement (porteur + raison).
18
29
  * @param now - horloge (epoch ms) injectable pour les tests.
19
30
  */
20
31
  export declare function cascadeUserRevocation(container: Container, event: IUserRevokedEvent, now?: number): Promise<void>;
21
32
  /**
22
- * Abonne la cascade au bus kernel. À appeler au `onKernelBoot` d'un module
23
- * bootable (ici `@nodefony/security`). **Extensible** : tout autre module
24
- * (webhooks…) peut s'abonner au MÊME `USER_REVOKED_EVENT` pour ses propres
25
- * artefacts, sans toucher à ce fichier.
33
+ * Retire les facteurs rangés sous un identifiant — secret TOTP (codes de
34
+ * récupération compris) et passkeys. SEULE implémentation du geste, appelée à
35
+ * la suppression d'un compte ET à la création d'un compte sous cet identifiant.
36
+ * Best-effort par brique : un TOTP indisponible n'empêche pas le retrait des
37
+ * passkeys.
38
+ *
39
+ * @param container - container du kernel (services `totp`, `webauthn`).
40
+ * @param identifier - `IUser.identifier` du compte.
41
+ */
42
+ export declare function removeAccountFactors(container: ServiceLookupLike, identifier: string): Promise<void>;
43
+ /**
44
+ * Abonne la cascade au bus kernel, UNE fois, au `onKernelBoot` de
45
+ * `@nodefony/security` — avant qu'aucun compte ne puisse être créé ou révoqué
46
+ * (les comptes semés au démarrage compris). Deux abonnements :
47
+ * - {@link USER_REVOKED_EVENT} → {@link cascadeUserRevocation} ;
48
+ * - {@link USER_CREATED_EVENT} → purge des facteurs HÉRITÉS : un compte qui
49
+ * vient de naître n'a encore rien enrôlé, donc tout facteur déjà rangé sous
50
+ * son identifiant appartient à un ancien titulaire. Règle exacte, aucune
51
+ * date à comparer. ⚠️ Un import qui chargerait les facteurs AVANT les
52
+ * comptes les verrait retirés : importer les comptes d'abord.
53
+ *
54
+ * Les deux sont émis par `UserService` seul, et attendus : chaque handler rend
55
+ * sa promesse. **Extensible** : tout autre module (webhooks…) s'abonne aux
56
+ * MÊMES événements pour ses propres artefacts, sans toucher à ce fichier.
26
57
  *
27
58
  * @param kernel - bus d'événements (kernel) exposant `on`.
28
- * @param container - container capturé par le handler.
59
+ * @param container - container capturé par les handlers.
29
60
  */
30
61
  export declare function registerUserRevocationCascade(kernel: KernelListenerLike, container: Container): void;
62
+ /**
63
+ * Avis de démarrage quand le service `users` n'émet pas sur le bus du kernel —
64
+ * construit sans container (`new UserService(repo, encoder)`). Ses événements
65
+ * partent alors sur son propre bus, où personne n'écoute : supprimer un compte
66
+ * laisserait ses sessions, ses jetons et ses facteurs en place, SANS UN MOT.
67
+ *
68
+ * @param container - container du kernel.
69
+ * @param kernel - le kernel attendu comme bus des événements.
70
+ * @returns l'avis à déclarer, ou `null` si `users` est absent ou bien câblé.
71
+ */
72
+ export declare function usersEventBusNotice(container: ServiceLookupLike, kernel: object): IBootNotice | null;
31
73
  export {};
@@ -4,6 +4,7 @@ import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
4
  import type { ISecuredArea } from "../../contracts/ISecuredArea.js";
5
5
  import type { IToken } from "../../contracts/IToken.js";
6
6
  import { type ExternalSubjectMapping } from "./externalSubject.js";
7
+ import { type IProviderRoleMapping } from "../oauth/providerRoles.js";
7
8
  /** Comment le sujet d'un jeton devient un utilisateur de cette application. */
8
9
  export type ExternalSubjectPolicy = "require" | "ephemeral";
9
10
  /** Un émetteur reconnu, et la façon dont ses sujets entrent chez nous. */
@@ -28,6 +29,23 @@ export interface IExternalJwtAuthenticatorOptions {
28
29
  subjectPolicy: ExternalSubjectPolicy;
29
30
  /** Rôles accordés en mode `ephemeral`. */
30
31
  ephemeralRoles: readonly string[];
32
+ /**
33
+ * Fournisseurs de CONNEXION (`oauth2.providers`) qui déclarent leur émetteur.
34
+ *
35
+ * Quand l'émetteur d'un jeton est celui d'un fournisseur de connexion, la
36
+ * même personne a pu se connecter par le navigateur : son compte local porte
37
+ * alors le lien `(fournisseur, sub)`. Le chercher d'abord évite qu'un jeton
38
+ * d'API désigne un AUTRE compte que la session de son porteur.
39
+ */
40
+ oauthProviders?: readonly {
41
+ name: string;
42
+ issuer: string;
43
+ /**
44
+ * Table de correspondance des rôles du fournisseur, compilée — absente ou
45
+ * `null` : les rôles du compte lié restent locaux.
46
+ */
47
+ roleMapping?: IProviderRoleMapping | null;
48
+ }[];
31
49
  }
32
50
  /**
33
51
  * Authentification par **jeton d'accès émis par un serveur d'autorisation
@@ -62,6 +62,19 @@ export declare class FirewallRealtimeAuthenticator implements IRealtimeAuthentic
62
62
  * filet défensif fail-closed → le hub ferme la socket en 4001).
63
63
  */
64
64
  authenticate(_handshake: IRealtimeHandshake): Promise<IRealtimeToken>;
65
+ /**
66
+ * Le revalidateur de l'identité que le firewall a posée sur la requête
67
+ * COURANTE (ALS), pour toute connexion longue — socket WebSocket ou flux SSE.
68
+ *
69
+ * Une seule règle décide qu'une identité est morte, quel que soit le
70
+ * transport qui la porte : c'est celle-ci, et le hub comme le noyau HTTP
71
+ * l'appellent au lieu de la recopier.
72
+ *
73
+ * @returns `null` pour un anonyme (rien à révoquer), sinon la fonction qui
74
+ * dit si l'identité est TOUJOURS valable — fail-closed : `false` quand elle
75
+ * ne peut pas le prouver.
76
+ */
77
+ currentRevalidator(): ((nowMs?: number) => Promise<boolean>) | null;
65
78
  }
66
79
  /**
67
80
  * Surface MINIMALE du store de jetons consommée ici : les deux lectures qui
@@ -3,6 +3,7 @@ import type { IPasswordVerifier } from "@nodefony/user";
3
3
  import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
4
4
  import type { IToken } from "../../contracts/IToken.js";
5
5
  import type { LoginThrottler } from "../throttle/LoginThrottler.js";
6
+ import type { CredentialTransportPolicy } from "../transport/CredentialTransportPolicy.js";
6
7
  /**
7
8
  * Authentification par identifiant + mot de passe — schéma **HTTP Basic**
8
9
  * (RFC 7617) : `Authorization: Basic base64(identifiant:motdepasse)`, charset
@@ -25,11 +26,18 @@ export declare class UserPasswordAuthenticator implements IAuthenticator {
25
26
  * @param resolveVerifier - résolution lazy de la source de vérification
26
27
  * (typiquement `container.get("users")`) — appelée au premier login.
27
28
  * @param throttler - limiteur de tentatives (backoff NIST), `null` = désactivé.
29
+ * @param transport - politique du transport des secrets, `null` = aucune.
28
30
  */
29
- constructor(resolveVerifier: () => IPasswordVerifier, throttler?: LoginThrottler | null);
31
+ constructor(resolveVerifier: () => IPasswordVerifier, throttler?: LoginThrottler | null, transport?: CredentialTransportPolicy | null);
30
32
  /** La requête porte-t-elle un en-tête `Authorization: Basic ...` ? */
31
33
  supports(context: ContextType): boolean;
32
- /** Décode l'enveloppe Basic — un contenu malformé donne un credential vide (échec uniforme). */
34
+ /**
35
+ * Décode l'enveloppe Basic — un contenu malformé donne un credential vide
36
+ * (échec uniforme).
37
+ *
38
+ * @throws InsecureTransportError (403) — en production, en-tête reçu hors
39
+ * TLS : refusé avant même d'être décodé.
40
+ */
33
41
  createToken(context: ContextType): Promise<IToken>;
34
42
  /**
35
43
  * Vérifie le credential via le verifier ou lève un 401 au message uniforme.
@@ -47,7 +55,7 @@ export declare class UserPasswordAuthenticator implements IAuthenticator {
47
55
  onSuccess(_context: ContextType, _token: IToken): Promise<void>;
48
56
  /** Slot J3+ (audit events). Le throttling vit dans `authenticate` (clé = identifiant) ; le 401 + challenge sont posés par le firewall. */
49
57
  onFailure(_context: ContextType, _error: Error): Promise<void>;
50
- /** Challenge RFC 7235 posé par le firewall sur les 401 de la zone. */
58
+ /** Challenge RFC 9110 §11 posé par le firewall sur les 401 de la zone. */
51
59
  challenge(): string;
52
60
  }
53
61
  export default UserPasswordAuthenticator;
@@ -0,0 +1,16 @@
1
+ import type { IBootNotice, IFirewallZoneView } from "nodefony";
2
+ /**
3
+ * Le point d'attention « routes métier publiques » du bilan de démarrage.
4
+ *
5
+ * Hors d'une zone du pare-feu, une requête n'est jamais authentifiée : son
6
+ * identité n'est pas résolue, et une route métier qu'on croyait protégée
7
+ * répond à n'importe qui. Les seules zones présentes sont alors les aires du
8
+ * framework (`/nodefony/*`) ; on le DIT au démarrage, avec la recette.
9
+ *
10
+ * La classification « applicative ou framework » n'est pas recopiée : elle
11
+ * vient du cœur (`isFrameworkZone`), qui l'applique aussi au tableau des zones.
12
+ *
13
+ * @param zones - zones montées (`firewall.describe().zones`).
14
+ * @returns le point à déclarer, ou `null` si une zone applicative existe.
15
+ */
16
+ export declare function publicRoutesNotice(zones: readonly IFirewallZoneView[]): IBootNotice | null;
@@ -0,0 +1,37 @@
1
+ /** Ce que la décision lit d'une requête refusée — rien de plus. */
2
+ export interface IEntryPointRequest {
3
+ /** Méthode HTTP (`GET`, `POST`…). */
4
+ readonly method: string | undefined;
5
+ /** En-tête `Accept`. */
6
+ readonly accept: string | undefined;
7
+ /** En-tête `Sec-Fetch-Mode` (Fetch Metadata), absent d'un client ancien. */
8
+ readonly fetchMode: string | undefined;
9
+ /** Chemin et requête demandés (`/compte?onglet=2`), jamais l'hôte. */
10
+ readonly path: string;
11
+ }
12
+ /**
13
+ * Dit si une requête refusée est la NAVIGATION d'un navigateur — le seul cas
14
+ * où conduire à la page de connexion aide, au lieu de casser un client.
15
+ *
16
+ * `GET` ou `HEAD`, `Accept` qui demande du HTML, et `Sec-Fetch-Mode: navigate`
17
+ * quand l'en-tête est présent : un `fetch()` d'une page porte `cors` ou
18
+ * `same-origin` même s'il accepte du HTML, et recevrait une page de connexion
19
+ * à la place de ses données.
20
+ *
21
+ * @param request - la requête refusée
22
+ * @returns `true` pour une navigation
23
+ */
24
+ export declare function isBrowserNavigation(request: IEntryPointRequest): boolean;
25
+ /**
26
+ * L'adresse de la page de connexion pour un visiteur refusé :
27
+ * `<entryPoint>?from=<chemin demandé>`, composée par `loginPagePath` (cœur).
28
+ * Le chemin demandé repasse par `safeRedirectPath` — la seule garde
29
+ * anti-redirection ouverte du framework, que la page de connexion appliquera
30
+ * de nouveau au retour.
31
+ *
32
+ * @param entryPoint - chemin de la page de connexion (vérifié au boot)
33
+ * @param request - la requête refusée
34
+ * @returns l'adresse du `Location`, ou `null` quand ce n'est pas une
35
+ * navigation, ou quand la page demandée EST la page de connexion
36
+ */
37
+ export declare function entryPointLocation(entryPoint: string, request: IEntryPointRequest): string | null;
@@ -0,0 +1,54 @@
1
+ import type { OAuth2ClientAuthMethod, OAuth2Tokens } from "./oauth2Client.js";
2
+ /**
3
+ * Ce qu'un programme présente pour obtenir un jeton EN SON PROPRE NOM
4
+ * (RFC 6749 §4.4) — un script, un service partenaire, un agent : aucun
5
+ * utilisateur, aucun navigateur.
6
+ */
7
+ export interface IClientCredentialsRequest {
8
+ /**
9
+ * Identifiant de l'émetteur (claim `iss`), en https. Le point de jeton en est
10
+ * DÉCOUVERT (RFC 8414), jamais écrit à côté : un point de jeton recopié
11
+ * survit au déménagement de l'émetteur et envoie le secret ailleurs.
12
+ */
13
+ readonly issuer: string;
14
+ /** Identifiant du client confidentiel (ex. `<app>-machine` chez Keycloak). */
15
+ readonly clientId: string;
16
+ /** Secret du client — obligatoire : ce flux n'existe que pour un client confidentiel. */
17
+ readonly clientSecret: string;
18
+ /**
19
+ * Comment le client s'authentifie. Défaut `client_secret_basic`, la méthode
20
+ * que la RFC 6749 §2.3.1 rend obligatoire côté serveur. `none` n'existe pas
21
+ * ici : un client public ne peut pas obtenir de jeton en son nom.
22
+ */
23
+ readonly clientAuthMethod?: Exclude<OAuth2ClientAuthMethod, "none">;
24
+ /** Portées demandées ; aucune par défaut (le serveur applique les siennes). */
25
+ readonly scopes?: readonly string[];
26
+ /**
27
+ * Ressource visée (RFC 8707 §2) — l'audience que le jeton doit porter. Un
28
+ * serveur qui ne la connaît pas l'ignore ; Keycloak, lui, inscrit l'audience
29
+ * par un mapper déclaré sur le client.
30
+ */
31
+ readonly resource?: string;
32
+ /** Adresse où lire les métadonnées, si elle diffère de l'émetteur (cf `IDiscoveryOptions`). */
33
+ readonly discoveryUrl?: string | undefined;
34
+ /** Implémentation de `fetch` (banc sans réseau). */
35
+ readonly fetch?: typeof globalThis.fetch;
36
+ /** Délai d'attente de CHAQUE requête (découverte, puis jeton), en millisecondes. */
37
+ readonly timeoutMs?: number;
38
+ }
39
+ /**
40
+ * Obtient un jeton d'accès pour un programme, par le flux `client_credentials`
41
+ * (RFC 6749 §4.4), auprès d'un émetteur découvert (RFC 8414).
42
+ *
43
+ * C'est la voie de l'appelant MACHINE : il s'authentifie avec son propre
44
+ * secret, et le jeton rendu le désigne lui, pas un utilisateur. La même
45
+ * authentification de client que l'échange de code ({@link postTokenRequest}) —
46
+ * une seule implémentation, pour qu'aucune règle ne diverge d'un flux à l'autre.
47
+ *
48
+ * @param request - émetteur, identité du client, portées et ressource.
49
+ * @returns Les jetons rendus ; `accessToken()` est celui à présenter en `Bearer`.
50
+ * @throws Error - secret vide, émetteur invalide ou injoignable, métadonnées discordantes.
51
+ * @throws OAuth2RequestError - le serveur a refusé (`invalid_client` : secret
52
+ * faux ; `unauthorized_client` : le client n'a pas droit à ce flux).
53
+ */
54
+ export declare function requestClientCredentialsToken(request: IClientCredentialsRequest): Promise<OAuth2Tokens>;