@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { messageNonRestreint, modeNonRestreintAsync, readIfPresent
|
|
1
|
+
import { createSecretExclusive, messageNonRestreint, modeNonRestreintAsync, readIfPresent } from "./secretFile.js";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
//#region nodefony/src/token/JwtKeystore.ts
|
|
4
4
|
/**
|
|
@@ -36,6 +36,58 @@ function keystoreLeaksIntoImage(dir) {
|
|
|
36
36
|
return `JWT keystore: « ${dir} » n'est PAS sous ${CLEANED_DIRS.map((d) => `\`${d}/\``).join(" ni ")}, les seuls dossiers que le Dockerfile généré efface avant de fabriquer l'image. La clé privée de signature partira donc dans ton image de conteneur, où elle reste lisible par quiconque la télécharge — même effacée par une couche suivante. Deux issues : déplacer le dossier sous \`var/\` (ex. \`var/keys\`), ou injecter la clé par jwt.keystore.keySetJson depuis l'environnement, ce qui est la voie de production.`;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
+
* Lit un jeu de clés sérialisé et en vérifie la FORME — la seule lecture du
|
|
40
|
+
* dépôt, appelée par le keystore ET par la validation de configuration.
|
|
41
|
+
*
|
|
42
|
+
* Ne vérifie que la structure (`keys` non vide, un `kid` par clé) : importer
|
|
43
|
+
* les clés exige `jose`, chargé paresseusement au premier jeton. Les messages
|
|
44
|
+
* ne recopient JAMAIS la valeur reçue — elle porte une clé privée.
|
|
45
|
+
*
|
|
46
|
+
* @param json - le jeu de clés, tel que `NF_JWT_KEYSET` ou `keyset.json` le porte.
|
|
47
|
+
* @returns le jeu de clés lu.
|
|
48
|
+
* @throws Error si le JSON est illisible ou la structure incomplète.
|
|
49
|
+
*/
|
|
50
|
+
function parseKeySet(json) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(json);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error("JwtKeystore: keyset JSON invalide");
|
|
56
|
+
}
|
|
57
|
+
const keyset = parsed;
|
|
58
|
+
if (!keyset || !Array.isArray(keyset.keys) || keyset.keys.length === 0 || keyset.keys.some((k) => typeof k.kid !== "string")) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
|
|
59
|
+
return keyset;
|
|
60
|
+
}
|
|
61
|
+
/** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
|
|
62
|
+
async function buildKeySet(jose) {
|
|
63
|
+
const { publicKey, privateKey } = await jose.generateKeyPair("Ed25519", { extractable: true });
|
|
64
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
65
|
+
const kid = await jose.calculateJwkThumbprint(publicJwk, "sha256");
|
|
66
|
+
return {
|
|
67
|
+
active: kid,
|
|
68
|
+
keys: [{
|
|
69
|
+
...await jose.exportJWK(privateKey),
|
|
70
|
+
kid,
|
|
71
|
+
alg: "EdDSA",
|
|
72
|
+
use: "sig",
|
|
73
|
+
createdAt: Date.now()
|
|
74
|
+
}]
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Génère un jeu de clés de signature à partager entre tous les process d'une
|
|
79
|
+
* application — la valeur de `NF_JWT_KEYSET`.
|
|
80
|
+
*
|
|
81
|
+
* Même générateur que celui du keystore (une seule implémentation), sérialisé
|
|
82
|
+
* en JSON COMPACT : une ligne, sans quote simple, qu'un `.env` porte entre
|
|
83
|
+
* quotes simples et qu'un gestionnaire de secrets stocke tel quel.
|
|
84
|
+
*
|
|
85
|
+
* @returns le jeu de clés sérialisé — il contient la clé PRIVÉE.
|
|
86
|
+
*/
|
|
87
|
+
async function generateKeySet() {
|
|
88
|
+
return JSON.stringify(await buildKeySet(await import("jose")));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
39
91
|
* Keystore Ed25519 — implémentation de référence d'{@link IJwtKeystore}.
|
|
40
92
|
*
|
|
41
93
|
* Résout la clé de signature selon une **priorité** (jamais d'auto-génération en
|
|
@@ -55,10 +107,15 @@ function keystoreLeaksIntoImage(dir) {
|
|
|
55
107
|
* usage ; le boot ne paie rien si le JWT n'est jamais sollicité. Le chargement
|
|
56
108
|
* est mémoïsé (une seule résolution concurrente).
|
|
57
109
|
*
|
|
58
|
-
* @remarks
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
110
|
+
* @remarks Plusieurs process qui servent la MÊME application doivent signer
|
|
111
|
+
* avec la même clé, sinon un jeton signé par l'un est refusé par l'autre :
|
|
112
|
+
* - **pods** (aucun disque partagé) → source `env` obligatoire, la même valeur
|
|
113
|
+
* injectée partout (`NF_JWT_KEYSET`, générée par `security:secrets`) ;
|
|
114
|
+
* - **workers de `nodefony cluster`** → la source `env` est héritée au fork ; à
|
|
115
|
+
* défaut, la source `fichier` est sûre : la création de `keyset.json` est
|
|
116
|
+
* EXCLUSIVE ({@link createSecretExclusive}), un seul worker la génère et les
|
|
117
|
+
* autres relisent la sienne ;
|
|
118
|
+
* - source `mémoire` → une clé par process, jamais cohérente.
|
|
62
119
|
*/
|
|
63
120
|
var JwtKeystore = class {
|
|
64
121
|
#source;
|
|
@@ -91,7 +148,7 @@ var JwtKeystore = class {
|
|
|
91
148
|
async #load() {
|
|
92
149
|
const jose = await import("jose");
|
|
93
150
|
if (this.#source.keySetJson) {
|
|
94
|
-
await this.#importKeyset(jose,
|
|
151
|
+
await this.#importKeyset(jose, parseKeySet(this.#source.keySetJson));
|
|
95
152
|
return;
|
|
96
153
|
}
|
|
97
154
|
if (this.#source.dir) {
|
|
@@ -101,32 +158,24 @@ var JwtKeystore = class {
|
|
|
101
158
|
const existing = await this.#readFile(file);
|
|
102
159
|
if (existing) {
|
|
103
160
|
await this.#checkRestricted(file);
|
|
104
|
-
await this.#importKeyset(jose,
|
|
161
|
+
await this.#importKeyset(jose, parseKeySet(existing));
|
|
105
162
|
return;
|
|
106
163
|
}
|
|
107
|
-
const keyset = await
|
|
108
|
-
await
|
|
109
|
-
|
|
164
|
+
const keyset = await buildKeySet(jose);
|
|
165
|
+
if (await createSecretExclusive(file, JSON.stringify(keyset))) {
|
|
166
|
+
this.#log(`JWT keystore: clé Ed25519 générée et persistée (${file}).`, "INFO");
|
|
167
|
+
await this.#checkRestricted(file);
|
|
168
|
+
await this.#importKeyset(jose, keyset);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const winner = await this.#readFile(file);
|
|
172
|
+
if (winner === null) throw new Error(`JwtKeystore: ${file} a disparu juste après sa création`);
|
|
173
|
+
await this.#checkRestricted(file);
|
|
174
|
+
await this.#importKeyset(jose, parseKeySet(winner));
|
|
110
175
|
return;
|
|
111
176
|
}
|
|
112
|
-
this.#log("JWT keystore: clé de signature ÉPHÉMÈRE en mémoire
|
|
113
|
-
await this.#importKeyset(jose, await
|
|
114
|
-
}
|
|
115
|
-
/** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
|
|
116
|
-
async #generate(jose) {
|
|
117
|
-
const { publicKey, privateKey } = await jose.generateKeyPair("Ed25519", { extractable: true });
|
|
118
|
-
const publicJwk = await jose.exportJWK(publicKey);
|
|
119
|
-
const kid = await jose.calculateJwkThumbprint(publicJwk, "sha256");
|
|
120
|
-
return {
|
|
121
|
-
active: kid,
|
|
122
|
-
keys: [{
|
|
123
|
-
...await jose.exportJWK(privateKey),
|
|
124
|
-
kid,
|
|
125
|
-
alg: "EdDSA",
|
|
126
|
-
use: "sig",
|
|
127
|
-
createdAt: Date.now()
|
|
128
|
-
}]
|
|
129
|
-
};
|
|
177
|
+
this.#log("JWT keystore: clé de signature ÉPHÉMÈRE en mémoire — perdue au redémarrage (jetons en vol invalidés) et propre à CE process : derrière plusieurs pods ou workers, un jeton signé par l'un est refusé par l'autre (401 au hasard). Production : poser jwt.keystore.keySetJson depuis l'environnement (NF_JWT_KEYSET dans une application générée — `npx nodefony security:secrets --jwt-keyset`). Développement : jwt.keystore.dir.", "WARNING");
|
|
178
|
+
await this.#importKeyset(jose, await buildKeySet(jose));
|
|
130
179
|
}
|
|
131
180
|
/** Importe un keyset persisté : privée → `CryptoKey`, public → JWK sans `d`. */
|
|
132
181
|
async #importKeyset(jose, keyset) {
|
|
@@ -153,27 +202,10 @@ var JwtKeystore = class {
|
|
|
153
202
|
this.#keys = loaded;
|
|
154
203
|
this.#activeKid = keyset.active && loaded.some((k) => k.kid === keyset.active) ? keyset.active : firstKey.kid;
|
|
155
204
|
}
|
|
156
|
-
#parseKeyset(json) {
|
|
157
|
-
let parsed;
|
|
158
|
-
try {
|
|
159
|
-
parsed = JSON.parse(json);
|
|
160
|
-
} catch {
|
|
161
|
-
throw new Error("JwtKeystore: keyset JSON invalide");
|
|
162
|
-
}
|
|
163
|
-
const keyset = parsed;
|
|
164
|
-
if (!keyset || !Array.isArray(keyset.keys) || keyset.keys.length === 0 || keyset.keys.some((k) => typeof k.kid !== "string")) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
|
|
165
|
-
return keyset;
|
|
166
|
-
}
|
|
167
205
|
/** Lit un fichier ; `null` si absent (ENOENT), relance toute autre erreur. */
|
|
168
206
|
async #readFile(file) {
|
|
169
207
|
return readIfPresent(file);
|
|
170
208
|
}
|
|
171
|
-
/** Écriture atomique (tmp + rename) en mode 600 — pas de fichier partiel lu. */
|
|
172
|
-
async #writeAtomic(file, data) {
|
|
173
|
-
await writeSecret(file, data);
|
|
174
|
-
this.#log(`JWT keystore: clé Ed25519 générée et persistée (${file}).`, "INFO");
|
|
175
|
-
await this.#checkRestricted(file);
|
|
176
|
-
}
|
|
177
209
|
/**
|
|
178
210
|
* Constate le mode EFFECTIF du fichier de clés et avertit s'il n'est pas 0600.
|
|
179
211
|
*
|
|
@@ -194,4 +226,4 @@ var JwtKeystore = class {
|
|
|
194
226
|
}
|
|
195
227
|
};
|
|
196
228
|
//#endregion
|
|
197
|
-
export { JwtKeystore, JwtKeystore as default, keystoreLeaksIntoImage };
|
|
229
|
+
export { JwtKeystore, JwtKeystore as default, generateKeySet, keystoreLeaksIntoImage, parseKeySet };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalIssuer, extractScopes,
|
|
1
|
+
import { canonicalIssuer, extractScopes, metadataCandidateUrls, parseDiscoveryUrl, validateIssuerMetadata } from "nodefony";
|
|
2
2
|
//#region nodefony/src/token/RemoteJwtVerifier.ts
|
|
3
3
|
/**
|
|
4
4
|
* Codes d'erreur `jose` qui désignent un **jeton fautif** — la liste est
|
|
@@ -65,8 +65,8 @@ var RemoteJwtVerifier = class {
|
|
|
65
65
|
#jose = null;
|
|
66
66
|
/**
|
|
67
67
|
* @param options - émetteurs de confiance et réglages réseau
|
|
68
|
-
* @throws Error si un émetteur est invalide, dupliqué,
|
|
69
|
-
* algorithme à secret partagé
|
|
68
|
+
* @throws Error si un émetteur est invalide, dupliqué, déclare un
|
|
69
|
+
* algorithme à secret partagé ou une `discoveryUrl` invalide
|
|
70
70
|
*/
|
|
71
71
|
constructor(options) {
|
|
72
72
|
this.#options = options;
|
|
@@ -75,6 +75,7 @@ var RemoteJwtVerifier = class {
|
|
|
75
75
|
const issuer = canonicalIssuer(trusted.issuer);
|
|
76
76
|
if (this.#issuers.has(issuer)) throw new Error(`émetteur « ${issuer} » déclaré deux fois — deux politiques pour un même émetteur ne peuvent pas coexister : la seconde serait ignorée en silence.`);
|
|
77
77
|
assertAsymmetric(issuer, trusted.algorithms);
|
|
78
|
+
if (trusted.discoveryUrl !== void 0) parseDiscoveryUrl(trusted.discoveryUrl);
|
|
78
79
|
this.#issuers.set(issuer, {
|
|
79
80
|
...trusted,
|
|
80
81
|
issuer
|
|
@@ -101,6 +102,36 @@ var RemoteJwtVerifier = class {
|
|
|
101
102
|
* « jeton invalide »
|
|
102
103
|
*/
|
|
103
104
|
async verify(token, audience) {
|
|
105
|
+
const verified = await this.verifyClaims(token, audience);
|
|
106
|
+
if (verified === null) return null;
|
|
107
|
+
const { issuer, payload } = verified;
|
|
108
|
+
return {
|
|
109
|
+
issuer,
|
|
110
|
+
subject: typeof payload.sub === "string" ? payload.sub : void 0,
|
|
111
|
+
scopes: extractScopes(payload),
|
|
112
|
+
expiresAt: typeof payload.exp === "number" ? payload.exp : void 0,
|
|
113
|
+
issuedAt: typeof payload.iat === "number" ? payload.iat : void 0,
|
|
114
|
+
tokenId: typeof payload.jti === "string" ? payload.jti : void 0,
|
|
115
|
+
claims: payload
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Vérifie un jeton et rend ses claims BRUTS — pour un jeton qui n'est pas un
|
|
120
|
+
* jeton d'accès (jeton de déconnexion OpenID Connect, par exemple), dont le
|
|
121
|
+
* sens se lit dans des claims que {@link verify} ne remonte pas.
|
|
122
|
+
*
|
|
123
|
+
* Mêmes garanties et même contrat d'erreur que {@link verify} : signature,
|
|
124
|
+
* émetteur de la liste fermée, algorithme imposé, audience obligatoire,
|
|
125
|
+
* claims exigés par l'émetteur ; un refus est un `null`, une panne lève.
|
|
126
|
+
*
|
|
127
|
+
* @param token - le jeton brut, tel que présenté
|
|
128
|
+
* @param audience - audience que le jeton DOIT porter dans `aud`
|
|
129
|
+
* @returns l'émetteur canonique et les claims vérifiés, ou `null` si le jeton
|
|
130
|
+
* est refusé
|
|
131
|
+
* @throws Error si l'émetteur ne peut pas être joint ou publie un jeu de clés
|
|
132
|
+
* inutilisable
|
|
133
|
+
*/
|
|
134
|
+
async verifyClaims(token, audience) {
|
|
104
135
|
if (typeof token !== "string" || token.length === 0) return null;
|
|
105
136
|
const jose = this.#jose ??= await import("jose");
|
|
106
137
|
let claimedIssuer;
|
|
@@ -126,14 +157,9 @@ var RemoteJwtVerifier = class {
|
|
|
126
157
|
...trusted.typ ? { typ: trusted.typ } : {},
|
|
127
158
|
...trusted.requiredClaims?.length ? { requiredClaims: [...trusted.requiredClaims] } : {}
|
|
128
159
|
});
|
|
129
|
-
const subject = typeof payload.sub === "string" ? payload.sub : void 0;
|
|
130
160
|
return {
|
|
131
161
|
issuer: claimedIssuer,
|
|
132
|
-
|
|
133
|
-
scopes: extractScopes(payload),
|
|
134
|
-
expiresAt: typeof payload.exp === "number" ? payload.exp : void 0,
|
|
135
|
-
issuedAt: typeof payload.iat === "number" ? payload.iat : void 0,
|
|
136
|
-
tokenId: typeof payload.jti === "string" ? payload.jti : void 0
|
|
162
|
+
payload
|
|
137
163
|
};
|
|
138
164
|
} catch (error) {
|
|
139
165
|
const code = error.code;
|
|
@@ -167,7 +193,7 @@ var RemoteJwtVerifier = class {
|
|
|
167
193
|
trusted,
|
|
168
194
|
getKey: jose.createLocalJWKSet(await trusted.localJwks())
|
|
169
195
|
};
|
|
170
|
-
const jwksUri = trusted.jwksUri ?? await this.#discover(trusted
|
|
196
|
+
const jwksUri = trusted.jwksUri ?? await this.#discover(trusted);
|
|
171
197
|
const options = {
|
|
172
198
|
timeoutDuration: this.#options.timeoutMs ?? 5e3,
|
|
173
199
|
cooldownDuration: this.#options.cooldownMs ?? 3e4,
|
|
@@ -188,11 +214,12 @@ var RemoteJwtVerifier = class {
|
|
|
188
214
|
* « meilleure réponse que rien » : il est écarté comme s'il n'avait pas
|
|
189
215
|
* répondu, et la recherche continue.
|
|
190
216
|
*/
|
|
191
|
-
async #discover(
|
|
217
|
+
async #discover(trusted) {
|
|
218
|
+
const { issuer, discoveryUrl } = trusted;
|
|
192
219
|
const doFetch = this.#options.fetch ?? globalThis.fetch;
|
|
193
220
|
const timeout = this.#options.timeoutMs ?? 5e3;
|
|
194
221
|
const failures = [];
|
|
195
|
-
for (const url of
|
|
222
|
+
for (const url of metadataCandidateUrls(issuer, discoveryUrl)) try {
|
|
196
223
|
const response = await doFetch(url, {
|
|
197
224
|
signal: AbortSignal.timeout(timeout),
|
|
198
225
|
redirect: "manual",
|
|
@@ -202,7 +229,7 @@ var RemoteJwtVerifier = class {
|
|
|
202
229
|
failures.push(`${url} → HTTP ${response.status}`);
|
|
203
230
|
continue;
|
|
204
231
|
}
|
|
205
|
-
const metadata = validateIssuerMetadata(await response.json(), issuer);
|
|
232
|
+
const metadata = validateIssuerMetadata(await response.json(), issuer, discoveryUrl);
|
|
206
233
|
this.#audit(`émetteur « ${issuer} » découvert : clés sur ${metadata.jwksUri}.`);
|
|
207
234
|
return metadata.jwksUri;
|
|
208
235
|
} catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { link, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
//#region nodefony/src/token/secretFile.ts
|
|
6
7
|
/**
|
|
@@ -113,6 +114,62 @@ async function writeSecret(file, content) {
|
|
|
113
114
|
throw e;
|
|
114
115
|
}
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Codes d'un système de fichiers qui ne sait pas poser de lien dur (FAT, exFAT,
|
|
119
|
+
* certains montages réseau) — et non d'une cible déjà présente.
|
|
120
|
+
*/
|
|
121
|
+
const NO_HARD_LINK = /* @__PURE__ */ new Set([
|
|
122
|
+
"EPERM",
|
|
123
|
+
"ENOTSUP",
|
|
124
|
+
"ENOSYS",
|
|
125
|
+
"EOPNOTSUPP"
|
|
126
|
+
]);
|
|
127
|
+
/**
|
|
128
|
+
* Crée un secret SEULEMENT s'il n'existe pas encore : 0600, atomique, et un
|
|
129
|
+
* seul gagnant quand plusieurs process le tentent en même temps.
|
|
130
|
+
*
|
|
131
|
+
* ⭐ **Pourquoi pas {@link writeSecret}.** `rename` REMPLACE la cible : deux
|
|
132
|
+
* workers de `nodefony cluster` qui génèrent chacun une clé au premier boot
|
|
133
|
+
* l'écrivent tous les deux, le dernier gagne sur le disque — et l'autre garde
|
|
134
|
+
* en mémoire une clé que plus personne ne connaît. Ici, le temporaire est
|
|
135
|
+
* publié par un LIEN DUR, que le système refuse (`EEXIST`) si le nom existe :
|
|
136
|
+
* le perdant le sait, et relit la clé du gagnant. Le lecteur ne voit jamais un
|
|
137
|
+
* fichier à demi écrit, puisque le lien n'apparaît qu'une fois le contenu complet.
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ Un système de fichiers sans liens durs (FAT, exFAT) retombe sur une
|
|
140
|
+
* création exclusive directe (`wx`) : toujours un seul gagnant, mais un lecteur
|
|
141
|
+
* concurrent peut lire le fichier pendant son écriture — il lève alors, au lieu
|
|
142
|
+
* de signer avec une clé divergente.
|
|
143
|
+
*
|
|
144
|
+
* @param file - chemin du secret.
|
|
145
|
+
* @param content - contenu à écrire.
|
|
146
|
+
* @returns `true` si ce process l'a créé, `false` s'il existait déjà.
|
|
147
|
+
*/
|
|
148
|
+
async function createSecretExclusive(file, content) {
|
|
149
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
150
|
+
const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
151
|
+
await writeFile(tmp, content, { mode: 384 });
|
|
152
|
+
try {
|
|
153
|
+
await link(tmp, file);
|
|
154
|
+
return true;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
const code = e.code ?? "";
|
|
157
|
+
if (code === "EEXIST") return false;
|
|
158
|
+
if (!NO_HARD_LINK.has(code)) throw e;
|
|
159
|
+
try {
|
|
160
|
+
await writeFile(file, content, {
|
|
161
|
+
mode: 384,
|
|
162
|
+
flag: "wx"
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
} catch (fallback) {
|
|
166
|
+
if (fallback.code === "EEXIST") return false;
|
|
167
|
+
throw fallback;
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
116
173
|
/** Forme synchrone de {@link writeSecret}. */
|
|
117
174
|
function writeSecretSync(file, content) {
|
|
118
175
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -131,4 +188,4 @@ function writeSecretSync(file, content) {
|
|
|
131
188
|
} catch {}
|
|
132
189
|
}
|
|
133
190
|
//#endregion
|
|
134
|
-
export { MODE_SECRET, messageNonRestreint, modeNonRestreint, modeNonRestreintAsync, readIfPresent, readIfPresentSync, writeSecret, writeSecretSync };
|
|
191
|
+
export { MODE_SECRET, createSecretExclusive, messageNonRestreint, modeNonRestreint, modeNonRestreintAsync, readIfPresent, readIfPresentSync, writeSecret, writeSecretSync };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { InsecureTransportError } from "../../errors/InsecureTransportError.js";
|
|
2
|
+
//#region nodefony/src/transport/CredentialTransportPolicy.ts
|
|
3
|
+
/**
|
|
4
|
+
* Dit si un secret de connexion se refuse : en production, sans échappement
|
|
5
|
+
* explicite. Seule source de la décision — le firewall la compose au boot.
|
|
6
|
+
*
|
|
7
|
+
* @param environment - environnement normalisé du kernel.
|
|
8
|
+
* @param allowInsecureCredentials - échappement `security.allowInsecureCredentials`.
|
|
9
|
+
*/
|
|
10
|
+
function shouldEnforceCredentialTransport(environment, allowInsecureCredentials) {
|
|
11
|
+
return environment === "production" && !allowInsecureCredentials;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Refuse, avant toute vérification, un mot de passe ou un code de second
|
|
15
|
+
* facteur reçu en clair. Une instance partagée au conteneur (`credentialTransport`)
|
|
16
|
+
* sert TOUTES les portes qui reçoivent un secret : formulaire de session, second
|
|
17
|
+
* facteur, Basic, émission de jeton — une porte oubliée serait le canal clair.
|
|
18
|
+
*
|
|
19
|
+
* La décision se prend sur le scheme EFFECTIF (`context.scheme`) : derrière un
|
|
20
|
+
* proxy qui termine TLS et déclaré de confiance, il vaut `https` alors que le
|
|
21
|
+
* pod écoute en HTTP. C'est le cas de la plupart des déploiements ; c'est donc
|
|
22
|
+
* `trustProxy` que nomme le journal, pas « passez en HTTPS ».
|
|
23
|
+
*/
|
|
24
|
+
var CredentialTransportPolicy = class {
|
|
25
|
+
enforce;
|
|
26
|
+
#log;
|
|
27
|
+
#warned = false;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.enforce = options.enforce;
|
|
30
|
+
this.#log = options.log ?? null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Laisse passer un secret reçu sur TLS, ou n'importe lequel hors production.
|
|
34
|
+
*
|
|
35
|
+
* @param context - contexte de la requête qui porte le secret.
|
|
36
|
+
* @param door - porte qui le reçoit, nommée dans le journal.
|
|
37
|
+
* @throws InsecureTransportError (403) — secret reçu en clair en production.
|
|
38
|
+
*/
|
|
39
|
+
assert(context, door) {
|
|
40
|
+
if (!this.enforce) return;
|
|
41
|
+
const scheme = context.scheme;
|
|
42
|
+
if (scheme === "https" || scheme === "wss") return;
|
|
43
|
+
if (!this.#warned && this.#log !== null) {
|
|
44
|
+
this.#warned = true;
|
|
45
|
+
this.#log(`Secret de connexion REFUSÉ (${door}) : la requête est arrivée en ${scheme ?? "scheme inconnu"} en production. Derrière un proxy qui termine TLS, déclarez-le de confiance (\`trustProxy\`, config @nodefony/http) pour que \`X-Forwarded-Proto\` soit lu ; sinon servez l'application en HTTPS. Échappement explicite : \`security.allowInsecureCredentials\`. Les refus suivants ne sont plus journalisés ici (voir l'audit).`);
|
|
46
|
+
}
|
|
47
|
+
throw new InsecureTransportError();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CredentialTransportPolicy, shouldEnforceCredentialTransport };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import ApiKeyService from "./nodefony/service/apiKeys.js";
|
|
|
10
10
|
import AuditService from "./nodefony/service/auditService.js";
|
|
11
11
|
import TotpService from "./nodefony/service/totp.js";
|
|
12
12
|
import WebhookService from "./nodefony/service/webhooks.js";
|
|
13
|
+
import KeycloakService from "./nodefony/service/keycloak.js";
|
|
13
14
|
import type { ISecurityConfigInput } from "./nodefony/config/defineModuleConfig.js";
|
|
14
15
|
declare module "nodefony" {
|
|
15
16
|
interface NodefonyModuleConfig {
|
|
@@ -25,7 +26,7 @@ declare module "nodefony" {
|
|
|
25
26
|
* + Zod validée au boot (fail-closed si invalide). Consomme `@nodefony/user`
|
|
26
27
|
* (IUser/IUserProvider/IPasswordVerifier) — jamais l'inverse.
|
|
27
28
|
*
|
|
28
|
-
* Livré : firewall (zones, mode first|all, challenge RFC
|
|
29
|
+
* Livré : firewall (zones, mode first|all, challenge RFC 9110 §11), authenticators
|
|
29
30
|
* `anonymous`/`userpassword` (Basic RFC 7617)/`session` (BFF, J3), flux
|
|
30
31
|
* login/logout/me (`AuthFlow`, anti-fixation + throttling partagé).
|
|
31
32
|
* Jwt/oauth2/mtls/apikey, CORS, CSRF, autorisation par décorateurs et data
|
|
@@ -48,6 +49,14 @@ declare class Security extends Module {
|
|
|
48
49
|
* No-op si le broker est absent (Studio non chargé) ou déjà enregistré.
|
|
49
50
|
*/
|
|
50
51
|
onKernelBoot(): Promise<this>;
|
|
52
|
+
/**
|
|
53
|
+
* Déclare au bilan de démarrage les routes métier laissées PUBLIQUES.
|
|
54
|
+
*
|
|
55
|
+
* `onReady` et pas `onBoot` : les zones sont compilées par le pare-feu à son
|
|
56
|
+
* propre `onBoot`, posé APRÈS celui du module — à `onBoot` la liste est encore
|
|
57
|
+
* vide. `onReady` précède l'écriture du bilan (`var/last-boot.json`).
|
|
58
|
+
*/
|
|
59
|
+
onKernelReady(): Promise<this>;
|
|
51
60
|
}
|
|
52
61
|
export default Security;
|
|
53
62
|
export { Firewall };
|
|
@@ -63,8 +72,11 @@ export { OAuth2Service };
|
|
|
63
72
|
export { AuditService };
|
|
64
73
|
export { TotpService };
|
|
65
74
|
export { WebhookService };
|
|
75
|
+
export { KeycloakService };
|
|
76
|
+
export type { IKeycloakRealmPlan, IKeycloakRealmRequest, } from "./nodefony/service/keycloak.js";
|
|
66
77
|
export type { IWebhookRegisterInput, IWebhookSecretReveal, IWebhookDeliveryPolicy, } from "./nodefony/service/webhooks.js";
|
|
67
78
|
export type { ISafeUser, ILoginOutcome } from "./nodefony/service/authFlow.js";
|
|
79
|
+
export { amrForFactor } from "./nodefony/src/sessionAuthentication.js";
|
|
68
80
|
export type { ITokenResponse } from "./nodefony/service/tokenService.js";
|
|
69
81
|
export type { IApiKeyView, IApiKeyCreated, IApiKeyCapabilities, ICreateApiKeyOptions, } from "./nodefony/contracts/IApiKey.js";
|
|
70
82
|
export { ApiKeyError } from "./nodefony/errors/ApiKeyError.js";
|
|
@@ -114,14 +126,20 @@ export type { IWebAuthnUser, IWebAuthnAssertionResult, } from "./nodefony/servic
|
|
|
114
126
|
export type { IWebAuthnCredential } from "./nodefony/contracts/IWebAuthnCredential.js";
|
|
115
127
|
export type { IWebAuthnCredentialStore, IWebAuthnCredentialSummary, IWebAuthnListQuery, WebAuthnAuthUpdate, } from "./nodefony/contracts/IWebAuthnCredentialStore.js";
|
|
116
128
|
export type { IOAuthAuthorization } from "./nodefony/service/oauth2.js";
|
|
117
|
-
export type { IOAuthProvider, IIssuerPolicy, } from "./nodefony/contracts/IOAuthProvider.js";
|
|
118
|
-
export { registerOAuthProvider, getOAuthProviderFactory, listOAuthProviders, } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
119
|
-
export type { OAuthProviderFactory, IOAuthProviderContext, } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
129
|
+
export type { IOAuthProvider, IIssuerPolicy, ILogoutRequest, ILogoutTokenClaims, } from "./nodefony/contracts/IOAuthProvider.js";
|
|
130
|
+
export { registerOAuthProvider, getOAuthProviderFactory, listOAuthProviders, oauthProviderRequiresIssuer, } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
131
|
+
export type { OAuthProviderFactory, IOAuthProviderContext, IOAuthProviderRegistration, } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
120
132
|
export { OAuth2Client, OAuth2Tokens, OAuth2RequestError, generateState, generateCodeVerifier, createCodeChallenge, } from "./nodefony/src/oauth/oauth2Client.js";
|
|
121
133
|
export type { IOAuth2ClientOptions } from "./nodefony/src/oauth/oauth2Client.js";
|
|
122
134
|
export { discoverAuthorizationServer } from "./nodefony/src/oauth/metadata.js";
|
|
135
|
+
export { requestClientCredentialsToken } from "./nodefony/src/oauth/clientCredentials.js";
|
|
136
|
+
export type { IClientCredentialsRequest } from "./nodefony/src/oauth/clientCredentials.js";
|
|
137
|
+
export { diagnoseOAuthProvider } from "./nodefony/src/oauth/providerDiagnosis.js";
|
|
138
|
+
export type { IOAuthDiagnosis, IOAuthDiagnosisInput, IOAuthCheck, OAuthCheckName, OAuthCheckStatus, OAuthFailureKind, } from "./nodefony/src/oauth/providerDiagnosis.js";
|
|
139
|
+
export { KeycloakAdminClient, KeycloakAdminError, keycloakAdminTarget, keycloakLoginThemeCheck, readKeycloakLoginTheme, } from "./nodefony/keycloak/keycloakAdmin.js";
|
|
140
|
+
export type { IKeycloakAdminClientOptions, IKeycloakAdminCredentials, IKeycloakAdminTarget, IKeycloakLoginThemeResult, KeycloakLoginThemeOutcome, } from "./nodefony/keycloak/keycloakAdmin.js";
|
|
123
141
|
export type { IDiscoveredAuthorizationServer, IDiscoveryOptions, } from "./nodefony/src/oauth/metadata.js";
|
|
124
|
-
export { createOidcProvider, createDiscoveredOidcProvider, } from "./nodefony/src/oauth/providers/oidc.js";
|
|
142
|
+
export { createOidcProvider, createDiscoveredOidcProvider, readLogoutTokenClaims, } from "./nodefony/src/oauth/providers/oidc.js";
|
|
125
143
|
export type { IOidcPkceClient, IOidcProviderOptions, } from "./nodefony/src/oauth/providers/oidc.js";
|
|
126
144
|
export { createGithubProvider } from "./nodefony/src/oauth/providers/github.js";
|
|
127
145
|
export { MemoryAuditStore } from "./nodefony/src/audit/MemoryAuditStore.js";
|
|
@@ -159,6 +177,8 @@ export { defineSecurityConfig, securityConfigJsonSchema, } from "./nodefony/conf
|
|
|
159
177
|
export type { ISecurityConfig, ISecurityConfigInput, ISecurityAreaConfig, } from "./nodefony/config/defineModuleConfig.js";
|
|
160
178
|
export { LoginThrottler } from "./nodefony/src/throttle/LoginThrottler.js";
|
|
161
179
|
export type { ILoginThrottleOptions } from "./nodefony/src/throttle/LoginThrottler.js";
|
|
162
|
-
export {
|
|
180
|
+
export { CredentialTransportPolicy, shouldEnforceCredentialTransport, } from "./nodefony/src/transport/CredentialTransportPolicy.js";
|
|
181
|
+
export type { ICredentialTransportContext, ICredentialTransportPolicyOptions, } from "./nodefony/src/transport/CredentialTransportPolicy.js";
|
|
182
|
+
export { AuthenticationError, AccessDeniedError, ThrottledError, UnverifiableTokenError, InvalidTargetError, CsrfError, SsrfError, InsecureTransportError, } from "./nodefony/errors/index.js";
|
|
163
183
|
export { VoterVote } from "./nodefony/contracts/index.js";
|
|
164
184
|
export type { IToken, IAuthenticator, ISecuredArea, IFirewall, IAccessVoter, IAuthorizationService, IAccessTokenRecord, ITokenStore, ITokenListQuery, ITokenUsage, IResourcePermission, TokenRevokeReason, TokenStatus, IJwtKeystore, IJwtSigningKey, } from "./nodefony/contracts/index.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CliKernel, Command } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* `nodefony security:oauth:doctor` — dit POURQUOI un fournisseur OAuth refuse
|
|
4
|
+
* l'application, sans qu'un humain ait à se connecter.
|
|
5
|
+
*
|
|
6
|
+
* Trois verdicts distincts là où le journal ne montre qu'un 401 : l'émetteur
|
|
7
|
+
* se déclare autrement que la configuration ne l'écrit, l'URL de retour n'est
|
|
8
|
+
* pas enregistrée, le secret est faux. La même analyse alimente
|
|
9
|
+
* `nodefony doctor --live` (famille « Fournisseurs OAuth ») et l'endpoint
|
|
10
|
+
* d'administration `security/oauth/diagnosis` : une seule implémentation, celle
|
|
11
|
+
* du service `oauth2`.
|
|
12
|
+
*
|
|
13
|
+
* Valable pour tout fournisseur : les sondes passent par le fournisseur que le
|
|
14
|
+
* login construit, Keycloak n'y a rien de particulier.
|
|
15
|
+
*/
|
|
16
|
+
declare class SecurityOAuthDoctor extends Command {
|
|
17
|
+
constructor(cli: CliKernel);
|
|
18
|
+
generate(opts: {
|
|
19
|
+
provider?: string;
|
|
20
|
+
json?: boolean;
|
|
21
|
+
}): Promise<this>;
|
|
22
|
+
}
|
|
23
|
+
export default SecurityOAuthDoctor;
|
|
@@ -10,6 +10,15 @@ import { CliKernel, Command } from "nodefony";
|
|
|
10
10
|
* @returns les secondes, `undefined` si rien n'est demandé, une `Error` sinon
|
|
11
11
|
*/
|
|
12
12
|
export declare function ttlSeconds(raw: string | undefined): number | undefined | Error;
|
|
13
|
+
/**
|
|
14
|
+
* Durée demandée à l'émetteur : `--ttl` explicite, sinon 8 h quand le jeton
|
|
15
|
+
* part chez les agents, sinon rien — la configuration décide.
|
|
16
|
+
*
|
|
17
|
+
* @param raw - la valeur de `--ttl` telle que tapée, ou rien
|
|
18
|
+
* @param write - le jeton sera posé dans la configuration des agents
|
|
19
|
+
* @returns les secondes, `undefined` pour le défaut de configuration, une `Error` sinon
|
|
20
|
+
*/
|
|
21
|
+
export declare function tokenTtlSeconds(raw: string | undefined, write: boolean): number | undefined | Error;
|
|
13
22
|
/**
|
|
14
23
|
* `nodefony security:token` — émet un jeton d'accès pour une porte de cette
|
|
15
24
|
* application (la porte MCP par défaut).
|
|
@@ -12,9 +12,10 @@ import { CliKernel, Command } from "nodefony";
|
|
|
12
12
|
* seul recours est une écriture directe en base. Le même garde-fou existe côté
|
|
13
13
|
* data plane (`UserAdminApi`) — ici il est constaté, pas supposé.
|
|
14
14
|
*
|
|
15
|
-
* La
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* La suppression passe par `users.delete`, qui émet `onUserRevoked`
|
|
16
|
+
* (`reason: "deleted"`) et ATTEND la cascade de `@nodefony/security` : sessions
|
|
17
|
+
* et jetons éjectés, secret TOTP et passkeys retirés. La commande n'émet rien
|
|
18
|
+
* elle-même — le service est le seul émetteur du cycle de vie d'un compte.
|
|
18
19
|
*/
|
|
19
20
|
declare class SecurityUserDelete extends Command {
|
|
20
21
|
constructor(cli: CliKernel);
|
|
@@ -20,10 +20,10 @@ import { CliKernel, Command } from "nodefony";
|
|
|
20
20
|
*
|
|
21
21
|
* 🔒 **Les sessions et les jetons du compte sont RÉVOQUÉS**, et ce n'est pas une
|
|
22
22
|
* option : on change un mot de passe parce qu'il est compromis ou perdu, et
|
|
23
|
-
* laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* laisser vivre les sessions ouvertes laisserait l'accès à qui l'a volé.
|
|
24
|
+
* `UserService.changePassword` émet la révocation lui-même — la même que pour
|
|
25
|
+
* le plan d'administration et la page « mon mot de passe » : un seul émetteur,
|
|
26
|
+
* aucun transport ne peut l'oublier.
|
|
27
27
|
*/
|
|
28
28
|
declare class SecurityUserPassword extends Command {
|
|
29
29
|
constructor(cli: CliKernel);
|