@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.5
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.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +6 -8
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
- package/dist/nodefony/command/security-oauth-doctor.js +5 -3
- package/dist/nodefony/command/security-secrets.js +10 -4
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +5 -3
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +5 -3
- package/dist/nodefony/config/config.js +20 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +1 -2
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
- package/dist/nodefony/keycloak/scaffold.js +3 -3
- package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
- package/dist/nodefony/scaffold/contribute.js +1 -2
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +10 -8
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +5 -4
- package/dist/nodefony/service/keycloak.js +1 -2
- package/dist/nodefony/service/oauth2.js +29 -7
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +38 -22
- package/dist/nodefony/service/totp.js +4 -5
- package/dist/nodefony/service/webAuthn.js +4 -5
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +1 -2
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -2
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +1 -2
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +5 -16
- package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
- package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
- package/dist/nodefony/src/oauth/providerRoles.js +1 -2
- package/dist/nodefony/src/oauth/providers/github.js +16 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
- package/dist/nodefony/src/openToSignup.js +1 -2
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +2 -3
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +1 -2
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +14 -5
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +1 -2
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/nodefony/testing/index.js +108 -0
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/oauth2.d.ts +20 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/testing/index.d.ts +51 -0
- package/docs/api-keys.md +181 -174
- package/docs/audit.md +140 -140
- package/docs/authenticators.md +101 -101
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +74 -71
- package/docs/external-jwt.md +22 -12
- package/docs/firewall.md +85 -82
- package/docs/headers.md +81 -81
- package/docs/index.md +4 -4
- package/docs/keycloak.md +95 -78
- package/docs/login-page.md +23 -23
- package/docs/oauth2.md +157 -146
- package/docs/obtenir-un-jeton.md +116 -26
- package/docs/tokens.md +128 -122
- package/docs/totp.md +136 -136
- package/docs/webauthn.md +153 -153
- package/docs/webhooks.md +136 -124
- package/package.json +19 -13
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -2,7 +2,7 @@ import { KeycloakRealmError } from "./keycloakRealmInput.js";
|
|
|
2
2
|
import { KeycloakAdminError } from "./keycloakAdmin.js";
|
|
3
3
|
import { Command } from "nodefony";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
|
|
5
|
+
// nodefony/keycloak/security-keycloak-realm.ts
|
|
6
6
|
const options = {
|
|
7
7
|
helpGroup: "COMPTES ET SECRETS",
|
|
8
8
|
showBanner: false,
|
|
@@ -47,7 +47,10 @@ const EXIT_CONFIG = 78;
|
|
|
47
47
|
*/
|
|
48
48
|
var SecurityKeycloakRealm = class extends Command {
|
|
49
49
|
constructor(cli) {
|
|
50
|
-
super("security:keycloak:realm", "écrit le realm Keycloak déduit de la configuration", cli,
|
|
50
|
+
super("security:keycloak:realm", "écrit le realm Keycloak déduit de la configuration", cli, {
|
|
51
|
+
...options,
|
|
52
|
+
examples: ["nodefony security:keycloak:realm --check", "nodefony security:keycloak:realm --write"]
|
|
53
|
+
});
|
|
51
54
|
this.addOption("-w, --write [fichier]", "écrit le realm fusionné dans le fichier d'import");
|
|
52
55
|
this.addOption("-c, --check [fichier]", "sort en 1 si le fichier d'import n'est plus à jour");
|
|
53
56
|
this.addOption("-a, --apply", "pose le thème de connexion sur le realm existant (développement)");
|
|
@@ -124,5 +127,4 @@ var SecurityKeycloakRealm = class extends Command {
|
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
};
|
|
127
|
-
//#endregion
|
|
128
130
|
export { SecurityKeycloakRealm as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { contributeKeycloak } from "../keycloak/scaffold.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/scaffold/contribute.ts
|
|
3
3
|
/**
|
|
4
4
|
* Le point de contribution de `@nodefony/security` : ce que le paquet livre à
|
|
5
5
|
* une application, déclaré dans son `package.json` (`nodefony.contribute`) et
|
|
@@ -16,5 +16,4 @@ import { contributeKeycloak } from "../keycloak/scaffold.js";
|
|
|
16
16
|
function contribute(context) {
|
|
17
17
|
contributeKeycloak(context);
|
|
18
18
|
}
|
|
19
|
-
//#endregion
|
|
20
19
|
export { contribute };
|
|
@@ -1,25 +1,52 @@
|
|
|
1
|
+
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
1
2
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
3
|
+
import { UserToken } from "../src/token/UserToken.js";
|
|
4
|
+
import peekIssuer from "../src/authenticator/peekIssuer.js";
|
|
5
|
+
import { JwtAuthenticator } from "../src/authenticator/JwtAuthenticator.js";
|
|
6
|
+
import { ExternalSubjectResolver, externalSubjectOptions } from "../src/authenticator/ExternalSubjectResolver.js";
|
|
7
|
+
import { resolveJwtRuntime } from "../src/token/jwtRuntime.js";
|
|
2
8
|
import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
|
|
3
|
-
import { ACCESS_TOKEN_VERIFIER, Service
|
|
4
|
-
|
|
9
|
+
import { ACCESS_ACCOUNT_RESOLVER, ACCESS_TOKEN_VERIFIER, Service } from "nodefony";
|
|
10
|
+
// nodefony/service/accessTokenVerifier.ts
|
|
5
11
|
const serviceName = ACCESS_TOKEN_VERIFIER;
|
|
6
12
|
/**
|
|
7
|
-
* Pose (ou non) le vérificateur de jetons d'accès
|
|
13
|
+
* Pose (ou non) le vérificateur de jetons d'accès dans le conteneur — celui
|
|
14
|
+
* qu'une ressource protégée (porte MCP, zone `external-jwt`) interroge.
|
|
8
15
|
*
|
|
9
|
-
* Ce service est une **décision de câblage**, pas de la cryptographie :
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* Ce service est une **décision de câblage**, pas de la cryptographie : la
|
|
17
|
+
* vérification d'un jeton TIERS vit dans {@link RemoteJwtVerifier}, celle d'un
|
|
18
|
+
* jeton de CETTE application dans {@link JwtAuthenticator}, et la doctrine de
|
|
19
|
+
* refus dans le cœur (`nodefony/src/oauth/`). Ici, on tranche deux questions :
|
|
20
|
+
* l'application signe-t-elle ses jetons, et en accepte-t-elle d'ailleurs ?
|
|
13
21
|
*
|
|
14
|
-
* **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
22
|
+
* ⭐ **Un jeton signé par cette application est TOUJOURS vérifiable**, dès que
|
|
23
|
+
* sa capacité JWT est active (`security.jwt.enabled`, le défaut) — sans rien
|
|
24
|
+
* déclarer dans `resourceServer.issuers`. Une application qui émet un jeton
|
|
25
|
+
* pour sa propre porte (`nodefony security:mcp:token --resource …`) doit pouvoir
|
|
26
|
+
* le lire : sans cette règle, la porte refusait un jeton parfaitement valide,
|
|
27
|
+
* et l'application était un serveur d'autorisation dont aucune ressource ne
|
|
28
|
+
* reconnaissait la signature. Rien de nouveau ne devient valide : l'audience
|
|
29
|
+
* reste exigée, et seules celles de `jwt.audiences` sont émises.
|
|
30
|
+
*
|
|
31
|
+
* 🔴 **Il est vérifié par les règles de l'authentificateur `jwt`, pas par son
|
|
32
|
+
* jeu de clés seul** : révocation ciblée et en masse, compte encore actif. Lu
|
|
33
|
+
* comme un jeton tiers, un jeton RÉVOQUÉ — ou celui d'un compte désactivé —
|
|
34
|
+
* ouvrait encore la porte jusqu'à son expiration.
|
|
35
|
+
*
|
|
36
|
+
* L'aiguillage se fait sur l'émetteur revendiqué (`peekIssuer`), comme dans le
|
|
37
|
+
* pare-feu : l'émetteur de l'application va à la vérification locale, tout
|
|
38
|
+
* autre à la liste fermée des émetteurs tiers. Un émetteur inconnu est refusé
|
|
39
|
+
* sans la moindre requête sortante.
|
|
40
|
+
*
|
|
41
|
+
* **Rien à vérifier = rien n'est posé** (capacité JWT coupée, aucun émetteur
|
|
42
|
+
* tiers) : une porte protégée qui ne trouve pas de vérificateur refuse alors de
|
|
43
|
+
* servir en le disant (503 + CRITIC), là où un vérificateur vide refuserait
|
|
17
44
|
* chaque jeton un par un — même résultat pour l'appelant, diagnostic beaucoup
|
|
18
45
|
* plus difficile pour l'exploitant.
|
|
19
46
|
*
|
|
20
|
-
* Le coût est nul
|
|
21
|
-
*
|
|
22
|
-
*
|
|
47
|
+
* Le coût est nul tant qu'aucun jeton n'est présenté : aucune requête au
|
|
48
|
+
* démarrage, clés locales lues au premier jeton, découverte d'un émetteur tiers
|
|
49
|
+
* au premier jeton qui s'en réclame.
|
|
23
50
|
*/
|
|
24
51
|
var AccessTokenVerifierService = class extends Service {
|
|
25
52
|
module;
|
|
@@ -29,7 +56,7 @@ var AccessTokenVerifierService = class extends Service {
|
|
|
29
56
|
this.module = module;
|
|
30
57
|
this.kernel?.once("onBoot", () => this.#build());
|
|
31
58
|
}
|
|
32
|
-
/** Le vérificateur
|
|
59
|
+
/** Le vérificateur posé, ou `null` si rien n'est vérifiable. */
|
|
33
60
|
get verifier() {
|
|
34
61
|
return this.#verifier;
|
|
35
62
|
}
|
|
@@ -40,23 +67,13 @@ var AccessTokenVerifierService = class extends Service {
|
|
|
40
67
|
} catch {
|
|
41
68
|
return;
|
|
42
69
|
}
|
|
70
|
+
const runtime = config.jwt.enabled ? resolveJwtRuntime(config.jwt) : null;
|
|
71
|
+
const local = runtime === null ? null : new JwtAuthenticator(this.container, runtime);
|
|
43
72
|
const { issuers, ...tuning } = config.resourceServer;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const localIssuer = config.jwt.issuer;
|
|
49
|
-
const armed = issuers.map((trusted) => localIssuer && canonicalIssuer(trusted.issuer) === canonicalIssuer(localIssuer) ? {
|
|
50
|
-
...trusted,
|
|
51
|
-
localJwks: async () => {
|
|
52
|
-
const keystore = this.container?.get("jwtKeystore");
|
|
53
|
-
if (!keystore) throw new Error(`l'émetteur « ${trusted.issuer} » est cette application, mais sa capacité JWT n'est pas armée (security.jwt) — aucune clé locale à présenter`);
|
|
54
|
-
return keystore.getPublicJWKS();
|
|
55
|
-
}
|
|
56
|
-
} : trusted);
|
|
57
|
-
try {
|
|
58
|
-
this.#verifier = new RemoteJwtVerifier({
|
|
59
|
-
issuers: armed,
|
|
73
|
+
let remote = null;
|
|
74
|
+
if (issuers.length > 0) try {
|
|
75
|
+
remote = new RemoteJwtVerifier({
|
|
76
|
+
issuers,
|
|
60
77
|
timeoutMs: tuning.timeoutMs,
|
|
61
78
|
cooldownMs: tuning.cooldownMs,
|
|
62
79
|
cacheMaxAgeMs: tuning.cacheMaxAgeMs,
|
|
@@ -67,11 +84,61 @@ var AccessTokenVerifierService = class extends Service {
|
|
|
67
84
|
this.log(`vérificateur de jetons non armé — ${error.message}`, "CRITIC");
|
|
68
85
|
return;
|
|
69
86
|
}
|
|
70
|
-
|
|
87
|
+
if (local === null && remote === null) {
|
|
88
|
+
this.log("rien à vérifier — capacité JWT coupée (security.jwt.enabled) et aucun émetteur tiers (security.resourceServer.issuers) : une porte protégée refusera de servir.", "DEBUG");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const localIssuer = runtime?.issuer ?? null;
|
|
92
|
+
const verify = (token, audience) => {
|
|
93
|
+
if (local !== null && peekIssuer(token) === localIssuer) return local.verifyForResource(token, audience);
|
|
94
|
+
return remote === null ? Promise.resolve(null) : remote.verify(token, audience);
|
|
95
|
+
};
|
|
96
|
+
this.#verifier = verify;
|
|
71
97
|
this.container?.set(serviceName, verify);
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
this.container?.set(ACCESS_ACCOUNT_RESOLVER, accountResolver(local, issuers.length > 0 ? new ExternalSubjectResolver(this.container, externalSubjectOptions(config)) : null));
|
|
99
|
+
const trusted = [...localIssuer === null ? [] : [`${localIssuer} (cette application)`], ...issuers.map((i) => i.issuer)];
|
|
100
|
+
this.log(`vérificateur de jetons armé — ${trusted.length} émetteur(s) de confiance : ` + trusted.join(", "), "INFO");
|
|
74
101
|
}
|
|
75
102
|
};
|
|
76
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Le résolveur de compte posé à côté du vérificateur : il retrouve, pour un
|
|
105
|
+
* principal que celui-ci vient d'établir, le compte de l'application qui agit.
|
|
106
|
+
*
|
|
107
|
+
* Aiguillé par l'émetteur, comme le vérificateur : un jeton maison rejoue la
|
|
108
|
+
* règle de la zone `jwt` (`JwtAuthenticator.resolveAccount`), un jeton tiers
|
|
109
|
+
* celle de la zone `external-jwt` (`ExternalSubjectResolver`). Aucune n'est
|
|
110
|
+
* recopiée ici. Un refus rend `null` ; une panne (annuaire non câblé,
|
|
111
|
+
* émetteur absent de la table de rattachement) traverse.
|
|
112
|
+
*
|
|
113
|
+
* Le jeton rendu est un `UserToken` de type `"mcp"` — hors des types humains
|
|
114
|
+
* que le voteur de scopes laisse passer : les scopes du jeton bornent les
|
|
115
|
+
* attributs `api:action`, et les rôles restent ceux du compte, comme dans une
|
|
116
|
+
* zone `jwt`.
|
|
117
|
+
*/
|
|
118
|
+
function accountResolver(local, external) {
|
|
119
|
+
return async (principal) => {
|
|
120
|
+
let user = null;
|
|
121
|
+
if (local !== null) user = await local.resolveAccount(principal);
|
|
122
|
+
if (user === null && external !== null && principal.subject) {
|
|
123
|
+
if (!external.recognizes(principal.issuer)) return null;
|
|
124
|
+
try {
|
|
125
|
+
user = await external.resolve(principal.issuer, principal.subject, principal.claims);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof AuthenticationError) return null;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (user === null) return null;
|
|
132
|
+
const token = new UserToken("mcp").promote(user);
|
|
133
|
+
token.setAttribute("scopes", [...principal.scopes]);
|
|
134
|
+
token.setAttribute("issuer", principal.issuer);
|
|
135
|
+
token.setAttribute("subject", principal.subject);
|
|
136
|
+
if (principal.tokenId !== void 0) token.setAttribute("jti", principal.tokenId);
|
|
137
|
+
if (principal.claims !== void 0) token.setAttribute("claims", principal.claims);
|
|
138
|
+
return {
|
|
139
|
+
token,
|
|
140
|
+
user
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
}
|
|
77
144
|
export { AccessTokenVerifierService, AccessTokenVerifierService as default };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { ScopeGrantPolicy, refusedScopes } from "../src/token/scopeGrants.js";
|
|
1
2
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
3
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
|
|
2
4
|
import { generateApiKey } from "../src/apikey/apiKeyFormat.js";
|
|
3
5
|
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
4
6
|
import { TOKEN_FACETS } from "../src/token/tokenFilters.js";
|
|
5
7
|
import { ApiKeyError } from "../errors/ApiKeyError.js";
|
|
6
8
|
import { Service, countFacets } from "nodefony";
|
|
7
9
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
|
|
10
|
+
// nodefony/service/apiKeys.ts
|
|
9
11
|
const serviceName = "apiKeys";
|
|
10
12
|
const MAX_NAME_LEN = 100;
|
|
11
13
|
const MS_PER_DAY = 864e5;
|
|
@@ -32,6 +34,8 @@ var ApiKeyService = class extends Service {
|
|
|
32
34
|
#defaultExpiryDays = 90;
|
|
33
35
|
#maxPerSubject = 100;
|
|
34
36
|
#allowedScopes = null;
|
|
37
|
+
/** `grantableScopes` compilé au boot ; `null` tant que la config n'est pas lue. */
|
|
38
|
+
#scopeGrants = null;
|
|
35
39
|
constructor(module) {
|
|
36
40
|
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
37
41
|
this.module = module;
|
|
@@ -54,6 +58,7 @@ var ApiKeyService = class extends Service {
|
|
|
54
58
|
this.#defaultExpiryDays = ak.defaultExpiryDays;
|
|
55
59
|
this.#maxPerSubject = ak.maxPerSubject;
|
|
56
60
|
this.#allowedScopes = ak.allowedScopes;
|
|
61
|
+
this.#scopeGrants = new ScopeGrantPolicy(config.grantableScopes);
|
|
57
62
|
this.log(`api keys ready — prefix "${ak.prefix}_", max ${ak.maxPerSubject}/porteur`, "DEBUG");
|
|
58
63
|
}
|
|
59
64
|
/** `true` si les clés API sont activées en config. */
|
|
@@ -83,17 +88,31 @@ var ApiKeyService = class extends Service {
|
|
|
83
88
|
* Émet une nouvelle clé API pour un porteur — renvoie sa vue publique **+ le
|
|
84
89
|
* token en clair** (affiché une seule fois).
|
|
85
90
|
*
|
|
91
|
+
* Un scope que le porteur ne peut pas OBTENIR — réservé par
|
|
92
|
+
* `grantableScopes` à des rôles qu'il n'a pas, ou `admin:*` sans le rôle
|
|
93
|
+
* d'administration — refuse la création entière : une clé émise avec moins
|
|
94
|
+
* que demandé surprendrait sans qu'aucune réponse ne le dise. Même juge que
|
|
95
|
+
* l'émission des jetons ({@link refusedScopes}).
|
|
96
|
+
*
|
|
97
|
+
* @param roles - rôles du porteur tels que son compte les porte ; omis =
|
|
98
|
+
* aucun rôle, donc tout scope réservé est refusé (fail-closed).
|
|
86
99
|
* @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
|
|
100
|
+
* @throws ApiKeyError 403 — scope réservé à des rôles que le porteur n'a pas.
|
|
87
101
|
* @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
|
|
88
|
-
* @throws ApiKeyError
|
|
102
|
+
* @throws ApiKeyError 403 — aucun compte sous cet identifiant (la clé est liée
|
|
103
|
+
* à l'`id` immuable du compte, lu dans l'annuaire).
|
|
104
|
+
* @throws ApiKeyError 503 — store ou annuaire `users` indisponible.
|
|
89
105
|
*/
|
|
90
|
-
async createForSubject(subjectId, subjectType, opts) {
|
|
106
|
+
async createForSubject(subjectId, subjectType, opts, roles = []) {
|
|
91
107
|
const store = this.#resolveStore();
|
|
92
108
|
const name = this.#normalizeName(opts.name);
|
|
93
109
|
const scopes = this.#normalizeScopes(opts.scopes);
|
|
110
|
+
const refusals = refusedScopes(scopes, roles, this.#scopeGrants ?? new ScopeGrantPolicy(), this.container?.get("roleHierarchy") ?? null);
|
|
111
|
+
if (refusals.length > 0) throw new ApiKeyError(`scope not grantable to this account: ${refusals.map((r) => r.scope).join(", ")}`, 403);
|
|
94
112
|
const now = Date.now();
|
|
95
113
|
const expiresAt = this.#resolveExpiry(opts.expiresInDays, now);
|
|
96
114
|
if ((await store.findBySubject(subjectId)).filter((r) => r.kind === "pat" && this.#isActive(r, now)).length >= this.#maxPerSubject) throw new ApiKeyError(`API key limit reached (${this.#maxPerSubject})`, 409);
|
|
115
|
+
const holder = await this.#resolveHolder(subjectId);
|
|
97
116
|
const generated = generateApiKey(this.#prefix);
|
|
98
117
|
const record = {
|
|
99
118
|
id: randomUUID(),
|
|
@@ -119,7 +138,10 @@ var ApiKeyService = class extends Service {
|
|
|
119
138
|
lastUsedUserAgent: null,
|
|
120
139
|
revokedAt: null,
|
|
121
140
|
revokedReason: null,
|
|
122
|
-
metadata: {
|
|
141
|
+
metadata: {
|
|
142
|
+
[ACCOUNT_ID_KEY]: holder.id,
|
|
143
|
+
[CREDENTIALS_VERSION_KEY]: credentialsVersionOf(holder)
|
|
144
|
+
}
|
|
123
145
|
};
|
|
124
146
|
await store.put(record);
|
|
125
147
|
this.log(`api key created — id=${record.id} subject=${subjectId} scopes=[${scopes.join(",")}]`, "INFO");
|
|
@@ -253,6 +275,25 @@ var ApiKeyService = class extends Service {
|
|
|
253
275
|
});
|
|
254
276
|
return true;
|
|
255
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Le compte qui reçoit la clé — son identifiant IMMUABLE et sa version
|
|
280
|
+
* d'identifiants sont la liaison qu'elle portera, contrôlée à chaque
|
|
281
|
+
* présentation (`isBoundToAccount`). Lu dans
|
|
282
|
+
* l'annuaire, jamais reçu de l'appelant : la clé appartient au compte qui
|
|
283
|
+
* existe MAINTENANT sous cet identifiant.
|
|
284
|
+
*
|
|
285
|
+
* @throws ApiKeyError 503 — annuaire `users` absent (câblage).
|
|
286
|
+
* @throws ApiKeyError 403 — aucun compte sous cet identifiant.
|
|
287
|
+
*/
|
|
288
|
+
async #resolveHolder(subjectId) {
|
|
289
|
+
const users = this.get("users");
|
|
290
|
+
if (!users) throw new ApiKeyError("user directory unavailable", 503);
|
|
291
|
+
try {
|
|
292
|
+
return await users.loadUserByIdentifier(subjectId);
|
|
293
|
+
} catch {
|
|
294
|
+
throw new ApiKeyError("API key holder unknown", 403);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
256
297
|
#resolveStore() {
|
|
257
298
|
if (this.#store === null) {
|
|
258
299
|
const store = this.get("tokenStore");
|
|
@@ -306,5 +347,4 @@ var ApiKeyService = class extends Service {
|
|
|
306
347
|
};
|
|
307
348
|
}
|
|
308
349
|
};
|
|
309
|
-
//#endregion
|
|
310
350
|
export { ApiKeyService, ApiKeyService as default };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
2
2
|
import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
|
|
3
|
-
import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore,
|
|
3
|
+
import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
|
|
5
|
+
// nodefony/service/auditService.ts
|
|
6
6
|
const serviceName = "auditService";
|
|
7
7
|
const GC_INTERVAL_MS = 36e5;
|
|
8
8
|
/**
|
|
@@ -50,7 +50,7 @@ var AuditService = class extends Service {
|
|
|
50
50
|
let storeName = config.audit.store;
|
|
51
51
|
let reason = `store explicitement configuré ("${storeName}")`;
|
|
52
52
|
if (storeName === AUTO_STORE) {
|
|
53
|
-
const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory",
|
|
53
|
+
const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory", storeConnectivity(this.kernel));
|
|
54
54
|
storeName = auto.store;
|
|
55
55
|
reason = auto.reason;
|
|
56
56
|
this.log(`audit.store "auto" → "${storeName}" (${auto.reason})`, "INFO");
|
|
@@ -62,7 +62,7 @@ var AuditService = class extends Service {
|
|
|
62
62
|
this.log(`${msg} — audit désactivé (journal de sécurité non collecté)`, "CRITIC");
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. " + durableStoreRemedy(
|
|
65
|
+
if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. " + durableStoreRemedy(storeConnectivity(this.kernel)), "WARNING");
|
|
66
66
|
this.#enabled = true;
|
|
67
67
|
this.#idPrefix = randomBytes(4).toString("hex");
|
|
68
68
|
this.#store = factory({
|
|
@@ -142,5 +142,4 @@ var AuditService = class extends Service {
|
|
|
142
142
|
return this.#enabled;
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
|
-
//#endregion
|
|
146
145
|
export { AuditService as default };
|
|
@@ -2,13 +2,14 @@ import { securityConfigSchema } from "../config/config.js";
|
|
|
2
2
|
import { InsecureTransportError } from "../errors/InsecureTransportError.js";
|
|
3
3
|
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
4
4
|
import { ThrottledError } from "../errors/ThrottledError.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
|
|
6
|
+
import { resolveBoundSessionIdentity, resolveSessionIdentity } from "../src/sessionIdentity.js";
|
|
6
7
|
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
7
8
|
import { readAuditContext } from "../src/audit/readAuditContext.js";
|
|
8
9
|
import { amrForFactor } from "../src/sessionAuthentication.js";
|
|
9
10
|
import { LOGIN_PAGE_SKIN_SPECS, MFA_METHOD_TOTP, SESSION_AMR_KEY, SESSION_AUTH_AT_KEY, Service } from "nodefony";
|
|
10
11
|
import { requestUser } from "@nodefony/user";
|
|
11
|
-
|
|
12
|
+
// nodefony/service/authFlow.ts
|
|
12
13
|
const MFA_METHODS = Object.freeze([MFA_METHOD_TOTP]);
|
|
13
14
|
const serviceName = "authFlow";
|
|
14
15
|
const INVALID_CREDENTIALS = "Invalid credentials";
|
|
@@ -127,7 +128,7 @@ var AuthFlow = class extends Service {
|
|
|
127
128
|
methods: MFA_METHODS
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
|
-
await this.#openSession(context, user
|
|
131
|
+
await this.#openSession(context, user, "password");
|
|
131
132
|
recordAudit(this.container, {
|
|
132
133
|
category: "auth",
|
|
133
134
|
action: "login.success",
|
|
@@ -157,7 +158,7 @@ var AuthFlow = class extends Service {
|
|
|
157
158
|
async establishSessionFor(context, identifier, reason = "federated") {
|
|
158
159
|
if (typeof identifier !== "string" || identifier.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
|
|
159
160
|
const user = await resolveSessionIdentity(this.#resolveUsers(), identifier);
|
|
160
|
-
await this.#openSession(context, user
|
|
161
|
+
await this.#openSession(context, user, reason);
|
|
161
162
|
recordAudit(this.container, {
|
|
162
163
|
category: "auth",
|
|
163
164
|
action: "login.success",
|
|
@@ -292,7 +293,7 @@ var AuthFlow = class extends Service {
|
|
|
292
293
|
const current = requestUser(identifier);
|
|
293
294
|
if (current !== null) return toSafeUser(current);
|
|
294
295
|
try {
|
|
295
|
-
return toSafeUser(await
|
|
296
|
+
return toSafeUser(await resolveBoundSessionIdentity(this.#resolveUsers(), identifier, context.session?.getMetaBag(ACCOUNT_ID_KEY), context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)));
|
|
296
297
|
} catch (error) {
|
|
297
298
|
if (error instanceof AuthenticationError) return null;
|
|
298
299
|
throw error;
|
|
@@ -350,7 +351,7 @@ var AuthFlow = class extends Service {
|
|
|
350
351
|
}
|
|
351
352
|
return this.#loginPage;
|
|
352
353
|
}
|
|
353
|
-
async #openSession(context,
|
|
354
|
+
async #openSession(context, user, factor) {
|
|
354
355
|
const session = context.session ?? await this.#resolveSessions().start(context);
|
|
355
356
|
if (session === null) {
|
|
356
357
|
this.log("login: session service returned no session", "ERROR");
|
|
@@ -370,7 +371,9 @@ var AuthFlow = class extends Service {
|
|
|
370
371
|
} catch {}
|
|
371
372
|
session.setMetaBag(SESSION_AUTH_AT_KEY, Date.now());
|
|
372
373
|
session.setMetaBag(SESSION_AMR_KEY, amrForFactor(factor));
|
|
373
|
-
|
|
374
|
+
session.setMetaBag(ACCOUNT_ID_KEY, user.id);
|
|
375
|
+
session.setMetaBag(CREDENTIALS_VERSION_KEY, credentialsVersionOf(user));
|
|
376
|
+
await session.save(user.identifier);
|
|
374
377
|
}
|
|
375
378
|
#resolveSessions() {
|
|
376
379
|
if (this.#sessions === null) {
|
|
@@ -428,5 +431,4 @@ function toSafeUser(user) {
|
|
|
428
431
|
roles: [...user.roles]
|
|
429
432
|
};
|
|
430
433
|
}
|
|
431
|
-
//#endregion
|
|
432
434
|
export { AuthFlow, AuthFlow as default };
|
|
@@ -2,7 +2,7 @@ import { recordAudit } from "../src/audit/recordAudit.js";
|
|
|
2
2
|
import "../contracts/IAccessVoter.js";
|
|
3
3
|
import { listVoterFactories } from "../src/voter/voterRegistry.js";
|
|
4
4
|
import { Service, logColor } from "nodefony";
|
|
5
|
-
|
|
5
|
+
// nodefony/service/authorization.ts
|
|
6
6
|
const serviceName = "authorization";
|
|
7
7
|
/**
|
|
8
8
|
* Service d'autorisation Nodefony (niveau C, P6 J6) — décide d'un accès via un
|
|
@@ -90,5 +90,4 @@ function describeSubject(subject) {
|
|
|
90
90
|
if (subject === null || subject === void 0) return String(subject);
|
|
91
91
|
return subject.constructor?.name ?? typeof subject;
|
|
92
92
|
}
|
|
93
|
-
//#endregion
|
|
94
93
|
export { Authorization, Authorization as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/service/cors.ts
|
|
2
2
|
/**
|
|
3
3
|
* Politique CORS de Nodefony — Same-Origin Policy assouplie côté serveur
|
|
4
4
|
* (Fetch Standard / W3C CORS protocol). Logique PURE et synchrone : décide les
|
|
@@ -77,5 +77,4 @@ var Cors = class {
|
|
|
77
77
|
return h;
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
-
//#endregion
|
|
81
80
|
export { Cors, Cors as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CsrfError } from "../errors/CsrfError.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/service/csrf.ts
|
|
3
3
|
/**
|
|
4
4
|
* Méthodes HTTP « sûres » (RFC 9110 §9.2.1) — sans effet de bord par contrat,
|
|
5
5
|
* donc hors du vecteur CSRF. Tout le reste (POST/PUT/PATCH/DELETE…) est traité
|
|
@@ -93,5 +93,4 @@ var Csrf = class {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
//#endregion
|
|
97
96
|
export { Csrf, Csrf as default };
|
|
@@ -5,6 +5,7 @@ import { LoginThrottler } from "../src/throttle/LoginThrottler.js";
|
|
|
5
5
|
import { InsecureTransportError } from "../errors/InsecureTransportError.js";
|
|
6
6
|
import { CredentialTransportPolicy, shouldEnforceCredentialTransport } from "../src/transport/CredentialTransportPolicy.js";
|
|
7
7
|
import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
|
|
8
|
+
import { rolesWithin } from "../src/rolesWithin.js";
|
|
8
9
|
import { mergeCspFragments } from "../src/csp.js";
|
|
9
10
|
import { CsrfTokenManager } from "../src/csrfToken.js";
|
|
10
11
|
import { CsrfError } from "../errors/CsrfError.js";
|
|
@@ -22,10 +23,10 @@ import { recordAudit } from "../src/audit/recordAudit.js";
|
|
|
22
23
|
import { readAuditContext } from "../src/audit/readAuditContext.js";
|
|
23
24
|
import { SECURITY_AUDIT_CHANNEL, createAuditBridge } from "../src/audit/auditBridge.js";
|
|
24
25
|
import { findZonesOpenToSignup } from "../src/openToSignup.js";
|
|
25
|
-
import { RequestContext, Service, logColor } from "nodefony";
|
|
26
|
+
import { ROLES_WITHIN, RequestContext, Service, logColor } from "nodefony";
|
|
26
27
|
import { randomBytes } from "node:crypto";
|
|
27
28
|
import { encoderFromConfig } from "@nodefony/user";
|
|
28
|
-
|
|
29
|
+
// nodefony/service/firewall.ts
|
|
29
30
|
const serviceName = "firewall";
|
|
30
31
|
/**
|
|
31
32
|
* Réponse partagée de {@link Firewall.publishedProtectedResources} quand il n'y
|
|
@@ -108,6 +109,7 @@ var Firewall = class extends Service {
|
|
|
108
109
|
}
|
|
109
110
|
this.#roleHierarchy = new RoleHierarchyWalker(this.#config.roleHierarchy);
|
|
110
111
|
this.container?.set("roleHierarchy", this.#roleHierarchy);
|
|
112
|
+
this.container?.set(ROLES_WITHIN, rolesWithin(() => this.container?.get("users") ?? null, this.#roleHierarchy));
|
|
111
113
|
this.#provisionSharedServices(this.#config);
|
|
112
114
|
if (this.#config.csrf.enabled) {
|
|
113
115
|
this.#csrf = new Csrf(this.#config.csrf, [...this.#config.csrf.trustedOrigins, ...this.#config.cors.origins]);
|
|
@@ -144,7 +146,7 @@ var Firewall = class extends Service {
|
|
|
144
146
|
pattern: area.pattern,
|
|
145
147
|
host: area.host
|
|
146
148
|
} : { pattern: area.pattern };
|
|
147
|
-
realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null));
|
|
149
|
+
realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null, () => this.container?.get("users") ?? null));
|
|
148
150
|
wired = true;
|
|
149
151
|
}
|
|
150
152
|
const configRules = (this.#config?.realtimeChannels ?? []).map((r) => ({
|
|
@@ -797,5 +799,4 @@ var Firewall = class extends Service {
|
|
|
797
799
|
return super.log(pci, severity, msgid, msg);
|
|
798
800
|
}
|
|
799
801
|
};
|
|
800
|
-
//#endregion
|
|
801
802
|
export { Firewall, Firewall as default };
|
|
@@ -6,7 +6,7 @@ import { KeycloakAdminClient, keycloakAdminTarget } from "../keycloak/keycloakAd
|
|
|
6
6
|
import { Service } from "nodefony";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
9
|
-
|
|
9
|
+
// nodefony/service/keycloak.ts
|
|
10
10
|
const serviceName = "keycloak";
|
|
11
11
|
/** Où le compose d'une application importe ses realms (chemin natif). */
|
|
12
12
|
const IMPORT_DIR = path.join("docker", "keycloak", "import");
|
|
@@ -205,5 +205,4 @@ var KeycloakService = class extends Service {
|
|
|
205
205
|
return this.admin(options.provider).applyLoginTheme(options.skin ?? this.config().loginPage.skin);
|
|
206
206
|
}
|
|
207
207
|
};
|
|
208
|
-
//#endregion
|
|
209
208
|
export { KeycloakService, KeycloakService as default };
|
|
@@ -7,7 +7,7 @@ import { generateCodeVerifier, generateState } from "../src/oauth/oauth2Client.j
|
|
|
7
7
|
import { getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer } from "../src/oauth/oauthProviderRegistry.js";
|
|
8
8
|
import { diagnoseOAuthProvider } from "../src/oauth/providerDiagnosis.js";
|
|
9
9
|
import { BootConfigurationError, LOGIN_PAGE_PATH, Service, canonicalIssuer } from "nodefony";
|
|
10
|
-
|
|
10
|
+
// nodefony/service/oauth2.ts
|
|
11
11
|
const serviceName = "oauth2";
|
|
12
12
|
/**
|
|
13
13
|
* Clé de session de l'indice de déconnexion. Lue et écrite par CE service
|
|
@@ -133,6 +133,31 @@ function checkProviderIssuer(name, issuer) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
|
+
* Annonce qu'un annuaire accorde des rôles de PLATEFORME, à la sévérité de
|
|
137
|
+
* l'environnement — ou `null` quand il n'y a rien à dire.
|
|
138
|
+
*
|
|
139
|
+
* L'ouverture est ÉCRITE (`allowPlatformRoles`), donc voulue : l'annonce ne
|
|
140
|
+
* la conteste pas, elle la rappelle. Le risque — un annuaire compromis
|
|
141
|
+
* administre l'instance — n'est réel qu'en production : là, `WARNING` ;
|
|
142
|
+
* ailleurs le même fait est dit en `INFO`, sans sonner une alarme à chaque
|
|
143
|
+
* démarrage d'un décor de développement.
|
|
144
|
+
*
|
|
145
|
+
* @param name - nom du fournisseur (`oauth2.providers.<name>`).
|
|
146
|
+
* @param allowed - `allowPlatformRoles` tel que déclaré.
|
|
147
|
+
* @param granted - rôles de l'application que la table accorde.
|
|
148
|
+
* @param production - l'application tourne en production.
|
|
149
|
+
* @returns le message et sa sévérité, ou `null`.
|
|
150
|
+
*/
|
|
151
|
+
function platformRolesNotice(name, allowed, granted, production) {
|
|
152
|
+
if (allowed !== true) return null;
|
|
153
|
+
const platform = [...new Set(granted.filter(isPlatformRole))];
|
|
154
|
+
if (platform.length === 0) return null;
|
|
155
|
+
return {
|
|
156
|
+
message: `oauth2 "${name}" : l'annuaire accorde des rôles de PLATEFORME (${platform.join(", ")}) — son administrateur administre cette instance (allowPlatformRoles)`,
|
|
157
|
+
severity: production ? "WARNING" : "INFO"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
136
161
|
* Claims du jeton d'ACCÈS, quand c'est un JWT — `null` sinon (jeton opaque).
|
|
137
162
|
*
|
|
138
163
|
* Keycloak pose les rôles dans le jeton d'accès, et seulement là par défaut
|
|
@@ -209,10 +234,8 @@ var OAuth2Service = class extends Service {
|
|
|
209
234
|
checkProviderIssuer(name, provider?.issuer);
|
|
210
235
|
const mapping = provider === void 0 ? null : compileProviderRoleMapping(provider);
|
|
211
236
|
if (mapping !== null) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (platform.length > 0) this.log(`oauth2 "${name}" : l'annuaire accorde des rôles de PLATEFORME (${[...new Set(platform)].join(", ")}) — son administrateur administre cette instance (allowPlatformRoles)`, "WARNING");
|
|
215
|
-
}
|
|
237
|
+
const notice = platformRolesNotice(name, provider?.allowPlatformRoles, Object.values(mapping.table), this.kernel?.environment === "production");
|
|
238
|
+
if (notice !== null) this.log(notice.message, notice.severity);
|
|
216
239
|
this.#roleMappings ??= Object.create(null);
|
|
217
240
|
this.#roleMappings[name] = mapping;
|
|
218
241
|
}
|
|
@@ -590,5 +613,4 @@ var OAuth2Service = class extends Service {
|
|
|
590
613
|
return this.#config;
|
|
591
614
|
}
|
|
592
615
|
};
|
|
593
|
-
|
|
594
|
-
export { OAuth2Service, OAuth2Service as default, checkProviderIssuer, oauthDisplayLabel };
|
|
616
|
+
export { OAuth2Service, OAuth2Service as default, checkProviderIssuer, oauthDisplayLabel, platformRolesNotice };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mergeCspFragments } from "../src/csp.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/service/securityHeaders.ts
|
|
3
3
|
/** Marqueur substitué par le nonce CSP de la requête (cf `Context.cspNonce`). */
|
|
4
4
|
const NONCE_PLACEHOLDER = "{{nonce}}";
|
|
5
5
|
/**
|
|
@@ -87,5 +87,4 @@ var SecurityHeaders = class {
|
|
|
87
87
|
return this.#cspParts !== null ? merged.split(NONCE_PLACEHOLDER).join(nonce) : merged.replace(/\s*'nonce-\{\{nonce\}\}'/g, "");
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
-
//#endregion
|
|
91
90
|
export { SecurityHeaders, SecurityHeaders as default };
|