@nodefony/security 10.0.0-beta.2 → 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 (160) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +44 -10
  4. package/dist/nodefony/command/security-oauth-doctor.js +66 -0
  5. package/dist/nodefony/command/security-secrets.js +42 -25
  6. package/dist/nodefony/command/security-token.js +46 -15
  7. package/dist/nodefony/command/security-user-delete.js +5 -4
  8. package/dist/nodefony/command/security-user-password.js +4 -11
  9. package/dist/nodefony/config/config.js +80 -5
  10. package/dist/nodefony/contracts/index.js +1 -2
  11. package/dist/nodefony/errors/InsecureTransportError.js +24 -0
  12. package/dist/nodefony/errors/index.js +2 -1
  13. package/dist/nodefony/keycloak/keycloakAdmin.js +255 -0
  14. package/dist/nodefony/keycloak/keycloakRealm.js +260 -0
  15. package/dist/nodefony/keycloak/keycloakRealmInput.js +124 -0
  16. package/dist/nodefony/keycloak/scaffold.js +231 -0
  17. package/dist/nodefony/keycloak/security-keycloak-realm.js +128 -0
  18. package/dist/nodefony/scaffold/contribute.js +20 -0
  19. package/dist/nodefony/service/authFlow.js +111 -11
  20. package/dist/nodefony/service/firewall.js +111 -21
  21. package/dist/nodefony/service/keycloak.js +209 -0
  22. package/dist/nodefony/service/oauth2.js +379 -26
  23. package/dist/nodefony/service/tokenService.js +57 -6
  24. package/dist/nodefony/service/totp.js +51 -6
  25. package/dist/nodefony/service/webAuthn.js +20 -2
  26. package/dist/nodefony/src/SecuredArea.js +14 -3
  27. package/dist/nodefony/src/admin/SecurityAdminApi.js +70 -40
  28. package/dist/nodefony/src/admin/adminAudit.js +0 -1
  29. package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
  30. package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +66 -6
  31. package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +38 -15
  32. package/dist/nodefony/src/authenticator/JwtAuthenticator.js +1 -0
  33. package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +13 -3
  34. package/dist/nodefony/src/authenticator/authenticatorRegistry.js +9 -2
  35. package/dist/nodefony/src/authenticator/bearer.js +1 -2
  36. package/dist/nodefony/src/boot/publicRoutesNotice.js +27 -0
  37. package/dist/nodefony/src/entryPoint.js +39 -0
  38. package/dist/nodefony/src/oauth/clientCredentials.js +40 -0
  39. package/dist/nodefony/src/oauth/metadata.js +36 -9
  40. package/dist/nodefony/src/oauth/oauth2Client.js +60 -33
  41. package/dist/nodefony/src/oauth/oauthProviderRegistry.js +39 -6
  42. package/dist/nodefony/src/oauth/providerDiagnosis.js +283 -0
  43. package/dist/nodefony/src/oauth/providerRoles.js +79 -0
  44. package/dist/nodefony/src/oauth/providers/github.js +32 -6
  45. package/dist/nodefony/src/oauth/providers/oidc.js +82 -3
  46. package/dist/nodefony/src/openToSignup.js +47 -0
  47. package/dist/nodefony/src/sessionAuthentication.js +26 -0
  48. package/dist/nodefony/src/token/JwtKeystore.js +78 -46
  49. package/dist/nodefony/src/token/RemoteJwtVerifier.js +40 -13
  50. package/dist/nodefony/src/token/secretFile.js +59 -2
  51. package/dist/nodefony/src/transport/CredentialTransportPolicy.js +51 -0
  52. package/dist/types/index.d.ts +26 -6
  53. package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
  54. package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
  55. package/dist/types/nodefony/command/security-token.d.ts +9 -0
  56. package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
  57. package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
  58. package/dist/types/nodefony/config/config.d.ts +63 -1
  59. package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
  60. package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
  61. package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
  62. package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
  63. package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
  64. package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
  65. package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
  66. package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
  67. package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
  68. package/dist/types/nodefony/errors/index.d.ts +1 -0
  69. package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
  70. package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
  71. package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
  72. package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
  73. package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
  74. package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
  75. package/dist/types/nodefony/service/authFlow.d.ts +35 -3
  76. package/dist/types/nodefony/service/firewall.d.ts +17 -3
  77. package/dist/types/nodefony/service/keycloak.d.ts +115 -0
  78. package/dist/types/nodefony/service/oauth2.d.ts +125 -9
  79. package/dist/types/nodefony/service/tokenService.d.ts +7 -1
  80. package/dist/types/nodefony/service/totp.d.ts +28 -4
  81. package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
  82. package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
  83. package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +54 -12
  84. package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +18 -0
  85. package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +13 -0
  86. package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
  87. package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
  88. package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
  89. package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
  90. package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
  91. package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
  92. package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
  93. package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
  94. package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
  95. package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
  96. package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
  97. package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
  98. package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
  99. package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
  100. package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
  101. package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
  102. package/docs/api-keys.md +17 -17
  103. package/docs/audit.md +16 -16
  104. package/docs/authenticators.md +60 -27
  105. package/docs/authorization.md +16 -16
  106. package/docs/cors.md +29 -29
  107. package/docs/csrf.md +35 -33
  108. package/docs/external-jwt.md +66 -4
  109. package/docs/firewall.md +57 -24
  110. package/docs/headers.md +30 -30
  111. package/docs/index.md +12 -4
  112. package/docs/keycloak.md +818 -0
  113. package/docs/login-page.md +361 -0
  114. package/docs/oauth2.md +236 -122
  115. package/docs/obtenir-un-jeton.md +18 -11
  116. package/docs/tokens.md +136 -85
  117. package/docs/totp.md +105 -64
  118. package/docs/webauthn.md +90 -87
  119. package/docs/webhooks.md +3 -3
  120. package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
  121. package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
  122. package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
  123. package/keycloak/themes/nodefony/account/theme.properties +14 -0
  124. package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
  125. package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
  126. package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
  127. package/keycloak/themes/nodefony/admin/theme.properties +12 -0
  128. package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
  129. package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
  130. package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
  131. package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
  132. package/keycloak/themes/nodefony/email/theme.properties +8 -0
  133. package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
  134. package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
  135. package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
  136. package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
  137. package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
  138. package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
  139. package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
  140. package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
  141. package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
  142. package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
  143. package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
  144. package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
  145. package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
  146. package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
  147. package/keycloak/themes/nodefony/login/template.ftl +423 -0
  148. package/keycloak/themes/nodefony/login/theme.properties +17 -0
  149. package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
  150. package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
  151. package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
  152. package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
  153. package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
  154. package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
  155. package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
  156. package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
  157. package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
  158. package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
  159. package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
  160. package/package.json +17 -15
@@ -0,0 +1,92 @@
1
+ import type { IKeycloakMachineInput, IKeycloakRealmInput } from "./keycloakRealm.js";
2
+ import { type LoginPageSkin } from "nodefony";
3
+ import type { ISecurityConfig } from "../config/config.js";
4
+ /**
5
+ * Les ports de l'application, lus dans `servers` de sa configuration.
6
+ *
7
+ * L'HÔTE n'en fait pas partie : le domaine de la config est une adresse
8
+ * d'ÉCOUTE (`127.0.0.1`, `0.0.0.0`), pas celle que tape le navigateur. Celle-ci
9
+ * est l'hôte de `redirectUri`, que Keycloak compare au caractère près.
10
+ */
11
+ export interface IAppServers {
12
+ /** Port HTTP, `null` si le serveur en clair est coupé. */
13
+ readonly httpPort: number | null;
14
+ /** Port HTTPS, `null` si le serveur TLS est coupé. */
15
+ readonly httpsPort: number | null;
16
+ }
17
+ /** Ce qu'il faut savoir de l'application pour dériver son realm. */
18
+ export interface IKeycloakRealmContext {
19
+ /** Nom du fournisseur dans `oauth2.providers` (défaut `keycloak`). */
20
+ readonly providerName: string;
21
+ /** La configuration de sécurité VALIDÉE. */
22
+ readonly config: ISecurityConfig;
23
+ readonly servers: IAppServers;
24
+ /**
25
+ * `true` en production : aucun secret écrit (Keycloak le génère, l'exploitant
26
+ * le copie dans son gestionnaire), aucune adresse locale de développement.
27
+ */
28
+ readonly production: boolean;
29
+ /** Client machine à tenir à jour ; `null` = aucun. */
30
+ readonly machine: IKeycloakMachineInput | null;
31
+ /**
32
+ * Origine que Keycloak utilise pour JOINDRE l'application (canal arrière).
33
+ * Défaut développement : `http://host.docker.internal:<port http>` — le
34
+ * Keycloak du compose vit dans un conteneur, `localhost` y désigne le sien.
35
+ */
36
+ readonly backchannelOrigin?: string;
37
+ /**
38
+ * `loginTheme` du realm déjà écrit, `null` s'il n'en déclare pas (ou s'il
39
+ * n'existe pas encore). Seul un realm déjà habillé par Nodefony change
40
+ * d'habillage : un thème propre à l'application n'est jamais remplacé.
41
+ */
42
+ readonly currentLoginTheme?: string | null;
43
+ }
44
+ /** Une dérivation, et ce qu'elle a remarqué en chemin. */
45
+ export interface IKeycloakRealmDerivation {
46
+ readonly input: IKeycloakRealmInput;
47
+ /** Constats à DIRE à l'utilisateur (le realm reste écrit). */
48
+ readonly warnings: string[];
49
+ }
50
+ /** Erreur de dérivation : la config ne permet pas d'écrire un realm. */
51
+ export declare class KeycloakRealmError extends Error {
52
+ readonly name = "KeycloakRealmError";
53
+ }
54
+ /**
55
+ * Le nom du realm, lu dans l'émetteur (`https://kc.example/realms/<nom>`).
56
+ *
57
+ * @throws {KeycloakRealmError} Si l'émetteur n'a pas la forme Keycloak
58
+ */
59
+ export declare function realmNameFromIssuer(issuer: string | undefined): string;
60
+ /**
61
+ * Dérive, depuis la configuration EFFECTIVE de l'application, ce que son realm
62
+ * Keycloak doit dire.
63
+ *
64
+ * - **Client** : `clientId`, et son secret hors production.
65
+ * - **URL de retour** : `redirectUri` du fournisseur, plus — hors production —
66
+ * le même chemin sur chaque serveur local (http ET https), parce que
67
+ * l'application répond sur les deux et que Keycloak compare au caractère près.
68
+ * - **Audience** : `audiences` du fournisseur, sinon la `resource` de chaque
69
+ * zone `external-jwt`. Une audience qu'aucune zone ne porte est DITE : le
70
+ * jeton serait refusé partout.
71
+ * - **Rôles** : les clés de `roleMapping`, rangées selon `rolesSource`.
72
+ *
73
+ * @param ctx - la configuration et le décor de l'application
74
+ * @returns l'entrée du constructeur de realm et les constats
75
+ * @throws {KeycloakRealmError} Si le fournisseur est absent ou sans émetteur
76
+ */
77
+ /**
78
+ * Le thème Keycloak de connexion qui porte un habillage de la page `/login`.
79
+ *
80
+ * @param skin - habillage (`loginPage.skin`)
81
+ * @returns `nodefony` pour l'habillage par défaut, `nodefony-<habillage>` sinon
82
+ * (thème enfant livré sous `keycloak/themes/`)
83
+ */
84
+ export declare function keycloakLoginTheme(skin: LoginPageSkin): string;
85
+ /**
86
+ * Dit si un thème Keycloak est l'un de ceux que livre Nodefony.
87
+ *
88
+ * @param theme - nom du thème
89
+ * @returns `true` pour `nodefony` et `nodefony-<habillage>`
90
+ */
91
+ export declare function isNodefonyKeycloakTheme(theme: string): boolean;
92
+ export declare function deriveKeycloakRealmInput(ctx: IKeycloakRealmContext): IKeycloakRealmDerivation;
@@ -0,0 +1,98 @@
1
+ import type { IAppContributionContext } from "nodefony";
2
+ import { type IKeycloakRealmInput } from "./keycloakRealm.js";
3
+ /** Le nom du thème, tel que le realm le déclare et que Keycloak le monte. */
4
+ export declare const KEYCLOAK_THEME_NAME = "nodefony";
5
+ /** Où l'application reçoit le décor Keycloak (chemins qui voyagent : `/`). */
6
+ export declare const APP_KEYCLOAK_REALM_FILE = "docker/keycloak/import/realm.json";
7
+ export declare const APP_KEYCLOAK_THEMES_DIR = "docker/keycloak/themes";
8
+ export declare const APP_KEYCLOAK_THEME_DIR = "docker/keycloak/themes/nodefony";
9
+ /**
10
+ * Ressource de l'API partenaire d'une application neuve en développement :
11
+ * ce que rend `partnerResource()` du gabarit `complete` sans `NF_JWT_ISSUER`
12
+ * ni `NF_PORT_HTTPS` — le port HTTPS par défaut, comme les URL de retour du
13
+ * realm. Deux paquets, une valeur : le test de contribution compare le
14
+ * gabarit rendu à cette constante.
15
+ */
16
+ export declare const APP_PARTNER_DEV_RESOURCE = "https://localhost:5152/api/partner";
17
+ /** Les types de thème que le realm sait déclarer, et la clé qui les porte. */
18
+ declare const THEME_TYPES: readonly ["login", "account", "email", "admin"];
19
+ type TThemeType = (typeof THEME_TYPES)[number];
20
+ /**
21
+ * Le dossier du thème Keycloak livré par le paquet.
22
+ *
23
+ * @returns chemin natif absolu (`<paquet>/keycloak/themes/nodefony`)
24
+ */
25
+ export declare function keycloakThemeSource(): string;
26
+ /**
27
+ * Le dossier de TOUS les thèmes Keycloak livrés : `nodefony` et ses thèmes
28
+ * enfants `nodefony-<habillage>`, un par habillage de la page `/login`.
29
+ * Keycloak les monte côte à côte (`/opt/keycloak/themes/`).
30
+ *
31
+ * @returns chemin natif absolu (`<paquet>/keycloak/themes`)
32
+ */
33
+ export declare function keycloakThemesSource(): string;
34
+ /**
35
+ * Les types que le thème livré couvre — un dossier par type. Le realm ne
36
+ * déclare QUE ceux-là : déclarer un type absent ferait retomber Keycloak sur
37
+ * son thème par défaut, avec un avertissement à chaque page.
38
+ *
39
+ * @param themeDir - dossier du thème
40
+ * @returns les types présents, dans l'ordre de {@link THEME_TYPES}
41
+ */
42
+ export declare function deliveredThemeTypes(themeDir: string): TThemeType[];
43
+ /**
44
+ * Le secret du client navigateur dans le décor de développement d'une app.
45
+ *
46
+ * ⚠️ Même règle que le gabarit `.env` du cœur (`keycloak.clientSecret` dans
47
+ * `engine.ts`) : la frontière de paquets empêche de l'appeler, un test de ce
48
+ * paquet compare donc les deux sorties.
49
+ *
50
+ * @param appName - nom de l'application
51
+ * @returns le secret, PUBLIC par nature (écrit dans le realm versionné)
52
+ */
53
+ export declare function devClientSecret(appName: string): string;
54
+ /**
55
+ * Le secret de l'appelant machine (`<app>-machine`) dans le décor de
56
+ * développement d'une app.
57
+ *
58
+ * ⚠️ Même règle que le gabarit `.env` du cœur (`keycloak.machineSecret` dans
59
+ * `engine.ts`), comparée par le même test que {@link devClientSecret}.
60
+ *
61
+ * @param appName - nom de l'application
62
+ * @returns le secret, PUBLIC par nature (écrit dans le realm versionné)
63
+ */
64
+ export declare function devMachineSecret(appName: string): string;
65
+ /**
66
+ * Ce que dit la configuration rendue par le gabarit `security.ts` d'une app
67
+ * neuve : client `<app>`, URL de retour sur les deux ports de développement,
68
+ * rôle client `admin` (`roleMapping: { admin: "ROLE_ADMIN" }`), et les
69
+ * audiences des zones `external-jwt` que la configuration rendue déclare.
70
+ *
71
+ * @param appName - nom de l'application (= realm = identifiant de client)
72
+ * @param themes - types de thème à déclarer
73
+ * @param ids - `sub` des comptes de démonstration, FIXÉS dans le fichier :
74
+ * laissés à Keycloak, ils changent à chaque réimport et la connexion
75
+ * suivante est refusée (identifiant déjà lié à un autre `sub`)
76
+ * @param audiences - ressources que les jetons du realm doivent porter
77
+ * (mapper sur les deux clients) ; vide = aucune zone ne les accepte
78
+ * @returns l'entrée du constructeur de realm
79
+ */
80
+ export declare function appDevRealmInput(appName: string, themes: readonly TThemeType[], ids: {
81
+ readonly user: string;
82
+ readonly admin: string;
83
+ }, audiences?: readonly string[]): IKeycloakRealmInput;
84
+ /**
85
+ * Recopie les thèmes livrés dans l'application — `nodefony` et ses thèmes
86
+ * d'habillage —, fichier par fichier, sans jamais remplacer un fichier présent.
87
+ *
88
+ * @param context - contexte de contribution
89
+ */
90
+ export declare function copyKeycloakTheme(context: IAppContributionContext): void;
91
+ /**
92
+ * Pose le realm d'import et le thème Keycloak, si l'application monte un
93
+ * décor Keycloak.
94
+ *
95
+ * @param context - contexte fourni par le cœur (`runAppContributions`)
96
+ */
97
+ export declare function contributeKeycloak(context: IAppContributionContext): void;
98
+ export {};
@@ -0,0 +1,43 @@
1
+ import { CliKernel, Command } from "nodefony";
2
+ /**
3
+ * `nodefony security:keycloak:realm` — le realm Keycloak que la configuration
4
+ * de l'application implique.
5
+ *
6
+ * Le realm d'import répète ce que la config dit déjà : identifiant du client,
7
+ * URL de retour, audience de chaque zone, rôles de `roleMapping`. Écrit à la
8
+ * main, il diverge — et le symptôme est un 401 ou une URL de retour refusée,
9
+ * jamais un message qui nomme la cause. Cette commande le DÉRIVE, par le
10
+ * service `keycloak` — qui porte toutes les actions Keycloak ; elle n'en
11
+ * garde que l'affichage et les codes de sortie.
12
+ *
13
+ * Elle FUSIONNE dans le fichier existant au lieu de l'écraser : comptes de
14
+ * démonstration, titres, secret machine, mappers posés dans la console
15
+ * survivent (`mergeKeycloakRealm`, cœur — le même constructeur que
16
+ * `create app`).
17
+ *
18
+ * - sans option : affiche le realm fusionné ;
19
+ * - `--write` : l'écrit (le fichier par défaut est l'unique `.json` de
20
+ * `docker/keycloak/import/`) ;
21
+ * - `--check` : sort en 1 si le fichier n'est plus à jour — la garde de CI ;
22
+ * - `--apply` : pose sur le realm EXISTANT le thème de connexion qui suit
23
+ * `loginPage.skin`, par l'API d'administration (service `keycloak`) —
24
+ * développement seulement, compte `oauth2.providers.<nom>.admin` requis.
25
+ *
26
+ * ⚠️ Keycloak n'importe un realm qu'à sa CRÉATION : un realm déjà présent
27
+ * n'est pas relu. Après `--write`, le porter par un import PARTIEL (clients en
28
+ * « Overwrite » : comptes et seconds facteurs survivent) ; retirer le volume
29
+ * ne se fait que sur un realm qui ne porte rien à garder.
30
+ */
31
+ declare class SecurityKeycloakRealm extends Command {
32
+ #private;
33
+ constructor(cli: CliKernel);
34
+ generate(opts: {
35
+ write?: string | boolean;
36
+ check?: string | boolean;
37
+ apply?: boolean;
38
+ provider?: string;
39
+ machine?: string;
40
+ backchannelOrigin?: string;
41
+ }): Promise<this>;
42
+ }
43
+ export default SecurityKeycloakRealm;
@@ -0,0 +1,15 @@
1
+ import type { IAppContributionContext } from "nodefony";
2
+ /**
3
+ * Le point de contribution de `@nodefony/security` : ce que le paquet livre à
4
+ * une application, déclaré dans son `package.json` (`nodefony.contribute`) et
5
+ * joué par le cœur, qui n'en connaît rien.
6
+ *
7
+ * @module
8
+ */
9
+ /**
10
+ * La contribution du paquet — aujourd'hui le seul décor Keycloak de
11
+ * développement (`nodefony/keycloak/`).
12
+ *
13
+ * @param context - contexte fourni par le cœur (`runAppContributions`)
14
+ */
15
+ export declare function contribute(context: IAppContributionContext): void;
@@ -1,4 +1,5 @@
1
1
  import { Service, Module } from "nodefony";
2
+ import type { ILoginPageDescription } from "nodefony";
2
3
  import type { ContextType, ISession } from "@nodefony/http";
3
4
  /**
4
5
  * Projection PUBLIQUE de l'utilisateur — ce qui sort en JSON vers le client.
@@ -7,7 +8,7 @@ import type { ContextType, ISession } from "@nodefony/http";
7
8
  */
8
9
  export interface ISafeUser {
9
10
  id: string;
10
- username: string;
11
+ identifier: string;
11
12
  roles: string[];
12
13
  }
13
14
  /**
@@ -19,7 +20,7 @@ export type ILoginOutcome = {
19
20
  user: ISafeUser;
20
21
  } | {
21
22
  status: "mfa_required";
22
- methods: ["totp"];
23
+ methods: readonly string[];
23
24
  };
24
25
  /**
25
26
  * Flux de session BFF — login/logout/me côté serveur (P6 J3).
@@ -56,6 +57,8 @@ declare class AuthFlow extends Service {
56
57
  * @param identifier - identifiant saisi (body JSON, non typé à la frontière).
57
58
  * @param password - mot de passe saisi.
58
59
  * @returns `authenticated` (identité établie) ou `mfa_required` (2ᵉ facteur requis).
60
+ * @throws InsecureTransportError (403) — mot de passe reçu en clair en
61
+ * production (refusé AVANT le throttle et le verifier).
59
62
  * @throws ThrottledError (429 + `Retry-After`) — backoff actif.
60
63
  * @throws AuthenticationError (401, message uniforme) — credential absent ou
61
64
  * invalide.
@@ -86,6 +89,7 @@ declare class AuthFlow extends Service {
86
89
  * @param context - contexte HTTP (porte la session PENDING).
87
90
  * @param code - code présenté (TOTP ou code de récupération).
88
91
  * @returns la projection publique de l'utilisateur authentifié.
92
+ * @throws InsecureTransportError (403) — code reçu en clair en production.
89
93
  * @throws ThrottledError (429) — trop de tentatives.
90
94
  * @throws AuthenticationError (401, uniforme) — aucun défi en cours, code absent
91
95
  * ou invalide (la session N'est PAS ouverte ; le défi reste pour un retry).
@@ -110,14 +114,42 @@ declare class AuthFlow extends Service {
110
114
  * @returns `true` si une session a réellement été détruite.
111
115
  */
112
116
  logout(context: ContextType): Promise<boolean>;
117
+ /**
118
+ * Déconnexion COMPLÈTE : détruit la session locale et rend, si la session
119
+ * vient d'un fournisseur qui publie un point de déconnexion (Keycloak…),
120
+ * l'adresse où envoyer le navigateur pour fermer AUSSI sa session à lui —
121
+ * sans quoi le clic suivant sur « Continuer avec … » reconnecte sans mot de
122
+ * passe (OpenID Connect RP-Initiated Logout 1.0).
123
+ *
124
+ * @returns `destroyed` comme {@link logout} ; `logoutUrl` à suivre, ou `null`.
125
+ */
126
+ logoutWithProvider(context: ContextType): Promise<{
127
+ destroyed: boolean;
128
+ logoutUrl: string | null;
129
+ }>;
113
130
  /**
114
131
  * Identité portée par la session courante, re-résolue auprès du provider
115
132
  * (mêmes contrôles que le `SessionAuthenticator` — source unique).
116
133
  *
117
134
  * @returns la projection publique, ou `null` (pas de session, session
118
- * orpheline, compte verrouillé/désactivé) — le handler répond 401.
135
+ * orpheline, compte verrouillé/désactivé) — le handler répond `200`
136
+ * `{ user: null }`.
119
137
  */
120
138
  me(context: ContextType): Promise<ISafeUser | null>;
139
+ /**
140
+ * Décrit la page de connexion par défaut : ce qu'elle propose et comment
141
+ * elle s'habille, depuis `security.loginPage` et les fournisseurs
142
+ * opérationnels.
143
+ *
144
+ * Lue par le contrôleur de `@nodefony/framework`, qui n'importe pas ce
145
+ * paquet. Les fournisseurs sont relus à chaque appel : l'un d'eux peut
146
+ * tomber ou revenir pendant que le serveur tourne.
147
+ *
148
+ * @returns la description, ou `null` quand la page par défaut est
149
+ * désactivée (`loginPage.enabled: false`) ou la configuration invalide —
150
+ * le contrôleur répond alors 404.
151
+ */
152
+ describeLoginPage(): ILoginPageDescription | null;
121
153
  }
122
154
  export default AuthFlow;
123
155
  export { AuthFlow };
@@ -90,7 +90,7 @@ declare class Firewall extends Service implements IFirewall {
90
90
  isSecure(context: ContextType): boolean;
91
91
  /**
92
92
  * Pipeline complet de la zone : chaîne d'authenticators (selon `mode`) → ALS
93
- * → Zero Trust. Rejette (401, challenge RFC 7235 posé) ou résout.
93
+ * → Zero Trust. Rejette (401, challenge RFC 9110 §11 posé) ou résout.
94
94
  */
95
95
  handleSecurity(context: ContextType): Promise<ContextType>;
96
96
  /**
@@ -102,8 +102,13 @@ declare class Firewall extends Service implements IFirewall {
102
102
  * synchronizer token (`context.csrfToken`) — HttpContext pose ensuite le cookie
103
103
  * lisible `csrf-token`. Sinon, hot-path GET = retour immédiat (aucun en-tête lu).
104
104
  * - **Étape 1 (globale)** : sur une mutation, défense Fetch Metadata / Origin
105
- * (rejet cross-site même sur route publique). Skippée si `@CsrfExempt` (webhook,
106
- * auth par signature/clé) ou `bypassFirewall` (callbacks OAuth).
105
+ * (rejet cross-site même sur route publique). Skippée SEULEMENT par `@CsrfExempt`
106
+ * (webhook, auth par signature/clé). `bypassFirewall` ne l'en dispense PAS : il
107
+ * retire l'authentification, pas la provenance — les routes de connexion sont
108
+ * en bypass, et c'est précisément là qu'un formulaire tiers ouvrirait chez la
109
+ * victime une session sur le compte de l'attaquant (login CSRF, CWE-352). Un
110
+ * appel de serveur à serveur (canal arrière OIDC) ne porte ni `Sec-Fetch-Site`
111
+ * ni `Origin` : il passe toujours.
107
112
  * - **Étape 2 (opt-in)** : sur une mutation `@CsrfProtect`, exige EN PLUS le
108
113
  * synchronizer token (en-tête `x-csrf-token` ≡ cookie + HMAC valide).
109
114
  *
@@ -124,6 +129,15 @@ declare class Firewall extends Service implements IFirewall {
124
129
  * réponse), sinon `undefined` (la requête réelle suit le pipeline normal).
125
130
  */
126
131
  handleCors(context: ContextType): number | undefined;
132
+ /**
133
+ * Le revalidateur de l'identité de la requête courante, pour une connexion
134
+ * longue qui la gardera (flux SSE). Même règle, même store de jetons que les
135
+ * sockets WebSocket — un transport qui en aurait une autre serait celui
136
+ * qu'un attaquant prendrait pour survivre à sa révocation.
137
+ *
138
+ * @returns `null` pour un anonyme ; sinon la fonction qui re-valide.
139
+ */
140
+ currentRevalidator(): ((nowMs?: number) => Promise<boolean>) | null;
127
141
  /**
128
142
  * En-têtes de sécurité APPLICATIFS (P6 J5) — CSP, Referrer-Policy, isolation
129
143
  * cross-origin (COOP/COEP/CORP), Origin-Agent-Cluster, Permissions-Policy.
@@ -0,0 +1,115 @@
1
+ import { Service, Module } from "nodefony";
2
+ import type { LoginPageSkin } from "nodefony";
3
+ import type { ISecurityConfig } from "../config/config.js";
4
+ import { KeycloakAdminClient, type IKeycloakLoginThemeResult } from "../keycloak/keycloakAdmin.js";
5
+ /** Ce qu'on demande au realm dérivé. */
6
+ export interface IKeycloakRealmRequest {
7
+ /** Nom dans `oauth2.providers` (défaut `keycloak`). */
8
+ readonly provider?: string | undefined;
9
+ /** Fichier d'import, relatif à la racine de l'application ou absolu. */
10
+ readonly file?: string | undefined;
11
+ /** Client machine dont l'audience suit la config. */
12
+ readonly machine?: string | undefined;
13
+ /** Origine par laquelle Keycloak joint l'application (canal arrière). */
14
+ readonly backchannelOrigin?: string | undefined;
15
+ }
16
+ /** Le realm que la configuration implique, confronté au fichier d'import. */
17
+ export interface IKeycloakRealmPlan {
18
+ /** Fichier d'import visé (chemin natif absolu). */
19
+ readonly file: string;
20
+ /** Contenu actuel du fichier, `null` s'il n'existe pas. */
21
+ readonly current: string | null;
22
+ /** Le realm fusionné, tel qu'il serait écrit. */
23
+ readonly text: string;
24
+ /** Constats à DIRE (le realm reste écrivable). */
25
+ readonly warnings: readonly string[];
26
+ /** `true` si le fichier porte déjà exactement {@link text}. */
27
+ readonly upToDate: boolean;
28
+ }
29
+ /**
30
+ * Service `keycloak` — toutes les actions de Nodefony sur un serveur Keycloak,
31
+ * en un point du conteneur.
32
+ *
33
+ * - **le realm** que la configuration implique : dérivé, fusionné avec le
34
+ * fichier d'import existant, comparé, écrit ({@link realmPlan},
35
+ * {@link writeRealm}) ;
36
+ * - **l'API d'administration** d'un realm vivant ({@link admin}), et ce qui s'y
37
+ * fait : le thème de connexion qui suit `loginPage.skin`
38
+ * ({@link applyLoginTheme}).
39
+ *
40
+ * Les commandes `security:keycloak:*` et les routes de développement
41
+ * l'appellent ; elles ne gardent que l'affichage. Seule la contribution de
42
+ * scaffold (`create app`, `scaffold:sync`) appelle les fonctions pures en
43
+ * direct : elle tourne SANS démarrer l'application, donc sans conteneur.
44
+ *
45
+ * Rien n'est construit au démarrage : la configuration est lue à l'appel, et
46
+ * les clients d'administration (un par fournisseur, jeton gardé) au premier
47
+ * usage.
48
+ */
49
+ declare class KeycloakService extends Service {
50
+ #private;
51
+ module: Module;
52
+ constructor(module: Module);
53
+ /**
54
+ * La configuration de sécurité VALIDÉE, relue à chaque appel (chemin froid).
55
+ *
56
+ * @returns la configuration
57
+ */
58
+ config(): ISecurityConfig;
59
+ /**
60
+ * Le realm que la configuration implique, fusionné avec le fichier d'import.
61
+ *
62
+ * Le realm d'import répète ce que la config dit déjà (client, URL de retour,
63
+ * audiences, rôles de `roleMapping`) : écrit à la main, il diverge. Ce plan
64
+ * le DÉRIVE, puis le fusionne pour que ce qu'un humain a ajouté survive
65
+ * (comptes, titres, secret machine, mappers posés dans la console).
66
+ *
67
+ * @param request - fournisseur, fichier, client machine, canal arrière
68
+ * @returns le fichier, son contenu, le realm à écrire et les constats
69
+ * @throws {KeycloakRealmError} Fournisseur absent ou mal décrit, fichier qui
70
+ * n'est pas un realm
71
+ */
72
+ realmPlan(request?: IKeycloakRealmRequest): IKeycloakRealmPlan;
73
+ /**
74
+ * Écrit le realm d'un plan — rien si le fichier est déjà à jour.
75
+ *
76
+ * ⚠️ Keycloak n'importe un realm qu'à sa CRÉATION : un realm déjà présent ne
77
+ * relit pas ce fichier ({@link applyLoginTheme} pour le thème, import
78
+ * partiel dans la console pour le reste).
79
+ *
80
+ * @param plan - le rendu de {@link realmPlan}
81
+ * @returns `true` si le fichier a été écrit
82
+ */
83
+ writeRealm(plan: IKeycloakRealmPlan): boolean;
84
+ /**
85
+ * Le client de l'API d'administration d'un fournisseur Keycloak.
86
+ *
87
+ * @param providerName - nom dans `oauth2.providers` (défaut `keycloak`)
88
+ * @returns le client, construit au premier appel puis gardé (jeton réutilisé)
89
+ * @throws {KeycloakAdminError} Hors développement, fournisseur absent, sans
90
+ * émetteur ou sans compte d'administration
91
+ */
92
+ admin(providerName?: string): KeycloakAdminClient;
93
+ /**
94
+ * Le thème de connexion qu'un habillage demande à Keycloak.
95
+ *
96
+ * @param skin - habillage ; défaut : `loginPage.skin` de la configuration
97
+ * @returns `nodefony` ou `nodefony-<habillage>`
98
+ */
99
+ expectedLoginTheme(skin?: LoginPageSkin): string;
100
+ /**
101
+ * Pose sur le realm VIVANT le thème de connexion d'un habillage.
102
+ *
103
+ * @param options - fournisseur (défaut `keycloak`) et habillage (défaut :
104
+ * `loginPage.skin` ; un aperçu de `/login` passe le sien)
105
+ * @returns le thème d'avant, celui demandé, et ce qui a été fait
106
+ * @throws {KeycloakAdminError} Hors développement, compte absent ou refusé,
107
+ * Keycloak injoignable
108
+ */
109
+ applyLoginTheme(options?: {
110
+ provider?: string;
111
+ skin?: LoginPageSkin;
112
+ }): Promise<IKeycloakLoginThemeResult>;
113
+ }
114
+ export { KeycloakService };
115
+ export default KeycloakService;