@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +44 -10
- package/dist/nodefony/command/security-oauth-doctor.js +66 -0
- package/dist/nodefony/command/security-secrets.js +42 -25
- package/dist/nodefony/command/security-token.js +46 -15
- package/dist/nodefony/command/security-user-delete.js +5 -4
- package/dist/nodefony/command/security-user-password.js +4 -11
- package/dist/nodefony/config/config.js +80 -5
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +24 -0
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +255 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +260 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +124 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +128 -0
- package/dist/nodefony/scaffold/contribute.js +20 -0
- package/dist/nodefony/service/authFlow.js +111 -11
- package/dist/nodefony/service/firewall.js +111 -21
- package/dist/nodefony/service/keycloak.js +209 -0
- package/dist/nodefony/service/oauth2.js +379 -26
- package/dist/nodefony/service/tokenService.js +57 -6
- package/dist/nodefony/service/totp.js +51 -6
- package/dist/nodefony/service/webAuthn.js +20 -2
- package/dist/nodefony/src/SecuredArea.js +14 -3
- package/dist/nodefony/src/admin/SecurityAdminApi.js +70 -40
- package/dist/nodefony/src/admin/adminAudit.js +0 -1
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +66 -6
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +38 -15
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +1 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +13 -3
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +9 -2
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +27 -0
- package/dist/nodefony/src/entryPoint.js +39 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +40 -0
- package/dist/nodefony/src/oauth/metadata.js +36 -9
- package/dist/nodefony/src/oauth/oauth2Client.js +60 -33
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +39 -6
- package/dist/nodefony/src/oauth/providerDiagnosis.js +283 -0
- package/dist/nodefony/src/oauth/providerRoles.js +79 -0
- package/dist/nodefony/src/oauth/providers/github.js +32 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +82 -3
- package/dist/nodefony/src/openToSignup.js +47 -0
- package/dist/nodefony/src/sessionAuthentication.js +26 -0
- package/dist/nodefony/src/token/JwtKeystore.js +78 -46
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +40 -13
- package/dist/nodefony/src/token/secretFile.js +59 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +51 -0
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-token.d.ts +9 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +63 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +125 -9
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +54 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +18 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +17 -17
- package/docs/audit.md +16 -16
- package/docs/authenticators.md +60 -27
- package/docs/authorization.md +16 -16
- package/docs/cors.md +29 -29
- package/docs/csrf.md +35 -33
- package/docs/external-jwt.md +66 -4
- package/docs/firewall.md +57 -24
- package/docs/headers.md +30 -30
- package/docs/index.md +12 -4
- package/docs/keycloak.md +818 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +236 -122
- package/docs/obtenir-un-jeton.md +18 -11
- package/docs/tokens.md +136 -85
- package/docs/totp.md +105 -64
- package/docs/webauthn.md +90 -87
- package/docs/webhooks.md +3 -3
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- 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
|
-
|
|
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: [
|
|
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
|
|
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
|
|
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
|
|
106
|
-
* auth par signature/clé)
|
|
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;
|