@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.4
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 +1 -2
- 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/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/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/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 +127 -124
- package/package.json +13 -13
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { RemoteJwtVerifier } from "../../token/RemoteJwtVerifier.js";
|
|
2
2
|
import { OAuth2Client } from "../oauth2Client.js";
|
|
3
3
|
import { discoverAuthorizationServer } from "../metadata.js";
|
|
4
|
-
|
|
4
|
+
import { isPlainObject } from "nodefony";
|
|
5
|
+
// nodefony/src/oauth/providers/oidc.ts
|
|
5
6
|
const DEFAULT_OIDC_SCOPES = [
|
|
6
7
|
"openid",
|
|
7
8
|
"profile",
|
|
@@ -35,7 +36,7 @@ function readLogoutTokenClaims(issuer, payload) {
|
|
|
35
36
|
const sid = typeof payload.sid === "string" ? payload.sid : null;
|
|
36
37
|
if ((sub === null || sub === "") && (sid === null || sid === "")) return null;
|
|
37
38
|
const events = payload.events;
|
|
38
|
-
if (
|
|
39
|
+
if (!isPlainObject(events)) return null;
|
|
39
40
|
const event = events[BACKCHANNEL_LOGOUT_EVENT];
|
|
40
41
|
if (typeof event !== "object" || event === null || Array.isArray(event)) return null;
|
|
41
42
|
if ("nonce" in payload) return null;
|
|
@@ -70,14 +71,17 @@ async function decodeIdTokenClaims(idToken) {
|
|
|
70
71
|
* écarts : un jeton d'un autre émetteur (point 2), délivré à une autre
|
|
71
72
|
* application (point 3), ou périmé (point 9), n'a rien à faire ici.
|
|
72
73
|
*
|
|
74
|
+
* @returns le `sub` du jeton, éprouvé non vide.
|
|
73
75
|
* @throws Error - un claim obligatoire est absent, discordant ou périmé.
|
|
74
76
|
*/
|
|
75
77
|
function assertIdTokenClaims(claims, opts) {
|
|
76
|
-
|
|
78
|
+
const sub = claims.sub;
|
|
79
|
+
if (typeof sub !== "string" || sub.length === 0) throw new Error(`${opts.name}: ID token sans claim 'sub'.`);
|
|
77
80
|
if (claims.iss !== opts.issuer) throw new Error(`${opts.name}: ID token émis par « ${String(claims.iss)} », attendu « ${opts.issuer} ».`);
|
|
78
81
|
const aud = claims.aud;
|
|
79
82
|
if (!(aud === opts.clientId || Array.isArray(aud) && aud.includes(opts.clientId))) throw new Error(`${opts.name}: ID token délivré à une autre application.`);
|
|
80
83
|
if (typeof claims.exp !== "number" || claims.exp * 1e3 <= Date.now()) throw new Error(`${opts.name}: ID token périmé ou sans 'exp'.`);
|
|
84
|
+
return sub;
|
|
81
85
|
}
|
|
82
86
|
/**
|
|
83
87
|
* Fabrique un {@link IOAuthProvider} **générique OIDC** — couvre TOUT fournisseur
|
|
@@ -113,8 +117,8 @@ function createOidcProvider(opts) {
|
|
|
113
117
|
},
|
|
114
118
|
async fetchProfile(tokens) {
|
|
115
119
|
const claims = await opts.decodeIdToken(tokens.idToken());
|
|
116
|
-
|
|
117
|
-
const sub = claims
|
|
120
|
+
if (!isPlainObject(claims)) throw new Error(`${opts.name}: ID token sans charge utile objet.`);
|
|
121
|
+
const sub = assertIdTokenClaims(claims, opts);
|
|
118
122
|
return {
|
|
119
123
|
provider: opts.name,
|
|
120
124
|
providerId: sub,
|
|
@@ -197,5 +201,4 @@ async function createDiscoveredOidcProvider(name, ctx, options = {}) {
|
|
|
197
201
|
})
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
|
-
//#endregion
|
|
201
204
|
export { createDiscoveredOidcProvider, createOidcProvider, readLogoutTokenClaims };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/openToSignup.ts
|
|
2
2
|
/**
|
|
3
3
|
* Les zones protégées qui n'exigent aucun rôle alors que la connexion OAuth
|
|
4
4
|
* crée les comptes à la volée — c'est-à-dire ouvertes à tout compte que le
|
|
@@ -43,5 +43,4 @@ function findZonesOpenToSignup(config) {
|
|
|
43
43
|
}
|
|
44
44
|
return found;
|
|
45
45
|
}
|
|
46
|
-
//#endregion
|
|
47
46
|
export { findZonesOpenToSignup };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/realtime/UserRealtimeToken.ts
|
|
2
2
|
/** Aucun scope — figé et partagé (0 allocation pour le cas humain, le plus courant). */
|
|
3
3
|
const EMPTY_SCOPES = Object.freeze([]);
|
|
4
4
|
/**
|
|
@@ -41,12 +41,20 @@ var UserRealtimeToken = class {
|
|
|
41
41
|
*/
|
|
42
42
|
#scopes;
|
|
43
43
|
/**
|
|
44
|
+
* Confirmation du COMPTE (annuaire relu), fournie par le
|
|
45
|
+
* {@link FirewallRealtimeAuthenticator} pour les modes dont l'application
|
|
46
|
+
* émet elle-même l'artefact. `null` = rien à confirmer.
|
|
47
|
+
*/
|
|
48
|
+
#confirmAccount;
|
|
49
|
+
/**
|
|
44
50
|
* @param user - identité authentifiée résolue par le firewall.
|
|
45
51
|
* @param revalidate - preuve de vie de l'identité, adaptée à son mode.
|
|
46
52
|
* @param type - mode d'authentification réel (défaut `"session"`).
|
|
47
53
|
* @param scopes - scopes délégués du jeton d'origine (défaut aucun).
|
|
54
|
+
* @param confirmAccount - relecture du compte, payée sur `api.request` seul.
|
|
48
55
|
*/
|
|
49
|
-
constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES) {
|
|
56
|
+
constructor(user, revalidate = null, type = "session", scopes = EMPTY_SCOPES, confirmAccount = null) {
|
|
57
|
+
this.#confirmAccount = confirmAccount;
|
|
50
58
|
this.#user = user;
|
|
51
59
|
this.#revalidate = revalidate;
|
|
52
60
|
this.type = type;
|
|
@@ -67,6 +75,13 @@ var UserRealtimeToken = class {
|
|
|
67
75
|
isValid(nowMs) {
|
|
68
76
|
return this.#revalidate ? this.#revalidate(nowMs) : Promise.resolve(true);
|
|
69
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Le compte qui a ouvert la socket existe-t-il toujours, actif, et sous le
|
|
80
|
+
* même `id` immuable ? `true` si aucune confirmation n'est câblée.
|
|
81
|
+
*/
|
|
82
|
+
confirmAccount() {
|
|
83
|
+
return this.#confirmAccount ? this.#confirmAccount() : Promise.resolve(true);
|
|
84
|
+
}
|
|
70
85
|
getUserIdentifier() {
|
|
71
86
|
return this.#user.identifier;
|
|
72
87
|
}
|
|
@@ -90,5 +105,4 @@ var UserRealtimeToken = class {
|
|
|
90
105
|
return key === "user" ? this.#user : void 0;
|
|
91
106
|
}
|
|
92
107
|
};
|
|
93
|
-
//#endregion
|
|
94
108
|
export { UserRealtimeToken, UserRealtimeToken as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NODEFONY_CHANNEL_NAMESPACE, PLATFORM_CHANNELS, PLATFORM_INBOUND, startsWithCI } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/realtime/frameAuthorizer.ts
|
|
3
3
|
/**
|
|
4
4
|
* Politique système par défaut des canaux d'**introspection serveur** : réservés
|
|
5
5
|
* à l'exploitant de l'INSTANCE, pas à l'administrateur d'une application.
|
|
@@ -289,5 +289,4 @@ function buildFrameAuthorizer(firewall, options) {
|
|
|
289
289
|
return true;
|
|
290
290
|
};
|
|
291
291
|
}
|
|
292
|
-
//#endregion
|
|
293
292
|
export { DEFAULT_SYSTEM_PREFIXES, DEFAULT_SYSTEM_RULES, RESERVED_FLOOR_PREFIXES, SECURITY_CHANNEL_POLICY, SYSTEM_CHANNEL_POLICY, UPLINK_CHANNEL_POLICY, buildFrameAuthorizer, buildFrameAuthorizer as default, buildSystemRules };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// nodefony/src/rolesWithin.ts
|
|
2
|
+
/**
|
|
3
|
+
* Lit un compte de l'annuaire, sans faire confiance au fournisseur : une
|
|
4
|
+
* lecture qui lève et une lecture qui rend `null` disent toutes deux
|
|
5
|
+
* « introuvable ».
|
|
6
|
+
*/
|
|
7
|
+
async function loadAccount(users, identifier) {
|
|
8
|
+
try {
|
|
9
|
+
return await users.loadUserByIdentifier(identifier) ?? null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Rôles chaîne d'un compte lu, sans lui faire confiance. */
|
|
15
|
+
function rolesOf(user) {
|
|
16
|
+
return Array.isArray(user.roles) ? user.roles.filter((r) => typeof r === "string") : [];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* La règle « jouer un autre compte » : les rôles de `target`, hiérarchie
|
|
20
|
+
* comprise, sont-ils TOUS atteints par ceux de `owner` ? Jouer un compte ne
|
|
21
|
+
* monte jamais en droits.
|
|
22
|
+
*
|
|
23
|
+
* Fermée en cas de doute : un annuaire absent, un compte introuvable — y
|
|
24
|
+
* compris un sujet externe éphémère, qui n'a pas de compte local — rendent un
|
|
25
|
+
* refus, jamais un accord. L'annuaire est résolu à l'appel : le service `users`
|
|
26
|
+
* peut être posé après le pare-feu.
|
|
27
|
+
*
|
|
28
|
+
* @param resolveUsers - rend l'annuaire des comptes (`users`), ou `null`
|
|
29
|
+
* @param hierarchy - la hiérarchie de rôles du pare-feu
|
|
30
|
+
* @returns la règle, à poser au conteneur sous `ROLES_WITHIN`
|
|
31
|
+
*/
|
|
32
|
+
function rolesWithin(resolveUsers, hierarchy) {
|
|
33
|
+
return async (owner, target) => {
|
|
34
|
+
const users = resolveUsers();
|
|
35
|
+
if (users === null) return {
|
|
36
|
+
ok: false,
|
|
37
|
+
reason: "annuaire des comptes indisponible (@nodefony/user)"
|
|
38
|
+
};
|
|
39
|
+
const ownerUser = await loadAccount(users, owner);
|
|
40
|
+
if (ownerUser === null) return {
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: `\`${owner}\` n'a pas de compte dans l'annuaire de l'application (un sujet externe éphémère ne peut jouer aucun compte)`
|
|
43
|
+
};
|
|
44
|
+
const targetUser = await loadAccount(users, target);
|
|
45
|
+
if (targetUser === null) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: `compte \`${target}\` introuvable`
|
|
48
|
+
};
|
|
49
|
+
const granted = hierarchy.reachableRoles(rolesOf(ownerUser));
|
|
50
|
+
const extra = [...hierarchy.reachableRoles(rolesOf(targetUser))].filter((role) => !granted.has(role));
|
|
51
|
+
return extra.length === 0 ? { ok: true } : {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: `\`${target}\` a des rôles que \`${owner}\` n'a pas : ${extra.sort().join(", ")}`
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export { rolesWithin };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/sessionAuthentication.ts
|
|
2
2
|
/**
|
|
3
3
|
* Facteurs d'authentification de Nodefony → méthodes RFC 8176, posées dans la
|
|
4
4
|
* session à son ouverture. Les clés et leur lecteur sont au cœur
|
|
@@ -22,5 +22,4 @@ const NO_LOCAL_FACTOR = Object.freeze([]);
|
|
|
22
22
|
function amrForFactor(factor) {
|
|
23
23
|
return Object.hasOwn(FACTOR_AMR, factor) ? FACTOR_AMR[factor] ?? NO_LOCAL_FACTOR : NO_LOCAL_FACTOR;
|
|
24
24
|
}
|
|
25
|
-
//#endregion
|
|
26
25
|
export { amrForFactor };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
2
|
+
import { isBoundToAccount } from "./accountBinding.js";
|
|
2
3
|
import { UserNotFoundError } from "@nodefony/user";
|
|
3
|
-
|
|
4
|
+
// nodefony/src/sessionIdentity.ts
|
|
4
5
|
const INVALID_SESSION = "Invalid session";
|
|
5
6
|
/**
|
|
6
7
|
* Résout l'identité portée par une session (l'identifiant stocké dans le blob)
|
|
@@ -31,5 +32,30 @@ async function resolveSessionIdentity(provider, identifier) {
|
|
|
31
32
|
if (user.isLocked() || !user.isActive()) throw new AuthenticationError(INVALID_SESSION);
|
|
32
33
|
return user;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Résout l'identité d'une session DÉJÀ ouverte, en exigeant qu'elle ait été
|
|
37
|
+
* ouverte pour CE compte — {@link resolveSessionIdentity} plus la liaison à
|
|
38
|
+
* l'identifiant immuable (`isBoundToAccount`).
|
|
39
|
+
*
|
|
40
|
+
* La session range son principal sous l'`identifier`, qu'un compte recréé
|
|
41
|
+
* (suppression hors framework, restauration, import) peut reprendre : sans la
|
|
42
|
+
* liaison, la session de l'ancien titulaire ouvrirait le compte du nouveau.
|
|
43
|
+
* Une session sans liaison est refusée au même titre.
|
|
44
|
+
*
|
|
45
|
+
* @param provider - source d'identité (`UserService` via le container).
|
|
46
|
+
* @param identifier - identifiant fonctionnel stocké en session.
|
|
47
|
+
* @param boundAccountId - identifiant immuable inscrit à l'ouverture
|
|
48
|
+
* (`metaBag`, clé `ACCOUNT_ID_KEY`).
|
|
49
|
+
* @param boundCredentialsVersion - version des identifiants inscrite à
|
|
50
|
+
* l'ouverture (`metaBag`, clé `CREDENTIALS_VERSION_KEY`).
|
|
51
|
+
* @returns l'utilisateur actif, titulaire de la session.
|
|
52
|
+
* @throws AuthenticationError (401, message uniforme) — identifiant inconnu,
|
|
53
|
+
* compte verrouillé ou désactivé, session ouverte pour un autre compte ou
|
|
54
|
+
* avant le dernier changement de mot de passe.
|
|
55
|
+
*/
|
|
56
|
+
async function resolveBoundSessionIdentity(provider, identifier, boundAccountId, boundCredentialsVersion) {
|
|
57
|
+
const user = await resolveSessionIdentity(provider, identifier);
|
|
58
|
+
if (!isBoundToAccount(user, boundAccountId, boundCredentialsVersion)) throw new AuthenticationError(INVALID_SESSION);
|
|
59
|
+
return user;
|
|
60
|
+
}
|
|
61
|
+
export { resolveBoundSessionIdentity, resolveSessionIdentity };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/throttle/LoginThrottler.ts
|
|
2
2
|
const DEFAULT_FREE_ATTEMPTS = 3;
|
|
3
3
|
const DEFAULT_BASE_DELAY_S = 1;
|
|
4
4
|
const DEFAULT_CAP_DELAY_S = 900;
|
|
@@ -93,5 +93,4 @@ var LoginThrottler = class {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
//#endregion
|
|
97
96
|
export { LoginThrottler, LoginThrottler as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { anonymousUser } from "@nodefony/user";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/token/AnonymousToken.ts
|
|
3
3
|
/**
|
|
4
4
|
* Token du visiteur non authentifié — Zero Trust : un visiteur EST un utilisateur
|
|
5
5
|
* anonyme (jamais `null`).
|
|
@@ -36,5 +36,4 @@ var AnonymousToken = class {
|
|
|
36
36
|
(this.#attributes ??= /* @__PURE__ */ new Map()).set(key, value);
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
|
-
//#endregion
|
|
40
39
|
export { AnonymousToken, AnonymousToken as default };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSecretExclusive, messageNonRestreint, modeNonRestreintAsync, readIfPresent } from "./secretFile.js";
|
|
2
|
+
import { isPlainObject } from "nodefony";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
+
// nodefony/src/token/JwtKeystore.ts
|
|
4
5
|
/**
|
|
5
6
|
* Dossiers que le `Dockerfile` généré EFFACE à l'étage de construction.
|
|
6
7
|
*
|
|
@@ -54,9 +55,18 @@ function parseKeySet(json) {
|
|
|
54
55
|
} catch {
|
|
55
56
|
throw new Error("JwtKeystore: keyset JSON invalide");
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
if (!isStoredKeySet(parsed)) throw new Error("JwtKeystore: keyset malformé (champ `keys` non vide avec `kid` requis)");
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Forme minimale d'un jeu de clés relu : un objet dont `keys` est un tableau non
|
|
63
|
+
* vide d'objets portant chacun un `kid` textuel. Le reste de chaque JWK est
|
|
64
|
+
* éprouvé par `jose` à l'import de la clé.
|
|
65
|
+
*/
|
|
66
|
+
function isStoredKeySet(value) {
|
|
67
|
+
if (!isPlainObject(value)) return false;
|
|
68
|
+
const keys = value.keys;
|
|
69
|
+
return Array.isArray(keys) && keys.length > 0 && keys.every((k) => isPlainObject(k) && typeof k.kid === "string");
|
|
60
70
|
}
|
|
61
71
|
/** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
|
|
62
72
|
async function buildKeySet(jose) {
|
|
@@ -225,5 +235,4 @@ var JwtKeystore = class {
|
|
|
225
235
|
this.#log(`JWT keystore: ${messageNonRestreint(file, mode)} La clé PRIVÉE y réside : provisionnez-la plutôt hors-bande via jwt.keystore.keySetJson (recommandé en production).`, "WARNING");
|
|
226
236
|
}
|
|
227
237
|
};
|
|
228
|
-
//#endregion
|
|
229
238
|
export { JwtKeystore, JwtKeystore as default, generateKeySet, keystoreLeaksIntoImage, parseKeySet };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./tokenSort.js";
|
|
2
2
|
import { matchesTokenStatus } from "./tokenStatus.js";
|
|
3
3
|
import { assertPageQuery, compareByOrder, pickOrder } from "nodefony";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/token/MemoryTokenStore.ts
|
|
5
5
|
/**
|
|
6
6
|
* Filtre un record contre une requête de listing — prédicat de RÉFÉRENCE du
|
|
7
7
|
* contrat, réutilisé par les implémentations qui évaluent en mémoire.
|
|
@@ -245,5 +245,4 @@ var MemoryTokenStore = class {
|
|
|
245
245
|
for (const [jti, expiresAt] of this.#deniedJti) if (expiresAt <= now) this.#deniedJti.delete(jti);
|
|
246
246
|
}
|
|
247
247
|
};
|
|
248
|
-
//#endregion
|
|
249
248
|
export { MemoryTokenStore, matchesTokenQuery };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
|
|
1
2
|
import { canonicalIssuer, extractScopes, metadataCandidateUrls, parseDiscoveryUrl, validateIssuerMetadata } from "nodefony";
|
|
2
|
-
|
|
3
|
+
// nodefony/src/token/RemoteJwtVerifier.ts
|
|
3
4
|
/**
|
|
4
5
|
* Codes d'erreur `jose` qui désignent un **jeton fautif** — la liste est
|
|
5
6
|
* BLANCHE, et c'est le point important.
|
|
@@ -163,7 +164,10 @@ var RemoteJwtVerifier = class {
|
|
|
163
164
|
};
|
|
164
165
|
} catch (error) {
|
|
165
166
|
const code = error.code;
|
|
166
|
-
if (!code || !TOKEN_FAULT_CODES.has(code))
|
|
167
|
+
if (!code || !TOKEN_FAULT_CODES.has(code)) {
|
|
168
|
+
const hint = untrustedAuthorityHint(error);
|
|
169
|
+
throw new Error(`vérification impossible : l'émetteur « ${trusted.issuer} » n'a pas fourni de jeu de clés utilisable (${code ?? describeTransportFailure(error)}).` + (hint === null ? "" : ` ${hint}.`), { cause: error });
|
|
170
|
+
}
|
|
167
171
|
this.#audit(`jeton refusé (${trusted.issuer}) : ${code}`);
|
|
168
172
|
return null;
|
|
169
173
|
}
|
|
@@ -254,5 +258,4 @@ function assertAsymmetric(issuer, algorithms) {
|
|
|
254
258
|
if (algorithms.length === 0) throw new Error(`émetteur « ${issuer} » : aucun algorithme accepté — la liste est la garde principale (RFC 8725 §3.1), elle ne peut pas être vide.`);
|
|
255
259
|
for (const alg of algorithms) if (alg.startsWith("HS") || alg.toLowerCase() === "none") throw new Error(`émetteur « ${issuer} » : algorithme « ${alg} » refusé. Les clés proviennent d'un jeu PUBLIC : un algorithme à secret partagé y transformerait la clé publique en secret de signature.`);
|
|
256
260
|
}
|
|
257
|
-
//#endregion
|
|
258
261
|
export { RemoteJwtVerifier, RemoteJwtVerifier as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { anonymousUser } from "@nodefony/user";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/token/UserToken.ts
|
|
3
3
|
/**
|
|
4
4
|
* Jeton porteur d'un utilisateur réel — produit par les authenticators à
|
|
5
5
|
* credential (`userpassword`, puis `session`/`jwt`...).
|
|
@@ -63,5 +63,4 @@ var UserToken = class {
|
|
|
63
63
|
(this.#attributes ??= /* @__PURE__ */ new Map()).set(key, value);
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
-
//#endregion
|
|
67
66
|
export { UserToken, UserToken as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/token/jwtRuntime.ts
|
|
2
2
|
/**
|
|
3
3
|
* Dérive les paramètres effectifs depuis la config sécurité. `issuer` omis →
|
|
4
4
|
* `"nodefony"` (DEVRAIT être surchargé en prod) ; `audiences` vide → l'app est sa
|
|
@@ -27,5 +27,4 @@ function resolveJwtRuntime(jwt) {
|
|
|
27
27
|
function defaultAudience(runtime) {
|
|
28
28
|
return runtime.audiences[0] ?? runtime.issuer;
|
|
29
29
|
}
|
|
30
|
-
//#endregion
|
|
31
30
|
export { defaultAudience, resolveJwtRuntime };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ADMIN_SCOPE_READ, ADMIN_SCOPE_WRITE, refusedAdminScopes } from "nodefony";
|
|
2
|
+
// nodefony/src/token/scopeGrants.ts
|
|
3
|
+
const SCOPE_TOKEN = /^[\x21\x23-\x5B\x5D-\x7E]+$/u;
|
|
4
|
+
const WILDCARD_SUFFIX = ":*";
|
|
5
|
+
/**
|
|
6
|
+
* Clés que `grantableScopes` refuse : le plan d'administration est jugé par le
|
|
7
|
+
* framework, et une déclaration ici serait soit sans effet, soit un
|
|
8
|
+
* assouplissement — deux pièges que rien ne signalerait.
|
|
9
|
+
*/
|
|
10
|
+
const RESERVED_SCOPE_GRANT_KEYS = [
|
|
11
|
+
ADMIN_SCOPE_READ,
|
|
12
|
+
ADMIN_SCOPE_WRITE,
|
|
13
|
+
"admin:*"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Dit pourquoi une clé de `grantableScopes` est invalide, ou `null` si elle
|
|
17
|
+
* est acceptable.
|
|
18
|
+
*
|
|
19
|
+
* Une clé est un `scope-token` (RFC 6749 §3.3) ; un `*` n'y est admis qu'en
|
|
20
|
+
* dernière position, juste après `:` (une famille `préfixe:*`).
|
|
21
|
+
*
|
|
22
|
+
* @param key - la clé déclarée.
|
|
23
|
+
* @returns le motif du refus, ou `null`.
|
|
24
|
+
*/
|
|
25
|
+
function scopeGrantKeyProblem(key) {
|
|
26
|
+
if (!SCOPE_TOKEN.test(key)) return "n'est pas un scope valide (RFC 6749 §3.3 : ni espace, ni guillemet, ni barre oblique inverse)";
|
|
27
|
+
const star = key.indexOf("*");
|
|
28
|
+
if (star !== -1 && (star !== key.length - 1 || !key.endsWith(WILDCARD_SUFFIX) || key.length === 2)) return "porte un `*` ailleurs qu'en fin de famille (`préfixe:*`)";
|
|
29
|
+
if (RESERVED_SCOPE_GRANT_KEYS.includes(key)) return "est réservé au plan d'administration du framework (les scopes `admin:read`/`admin:write` sont accordés au seul rôle ROLE_NODEFONY_ADMIN)";
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Politique d'attribution des scopes d'application, compilée une fois au boot.
|
|
34
|
+
*
|
|
35
|
+
* Résolution : une clé EXACTE l'emporte sur une famille, et la famille la plus
|
|
36
|
+
* longue sur une plus courte (`shop:admin:*` > `shop:*`). Le résultat ne dépend
|
|
37
|
+
* donc pas de l'ordre des clés. Un scope qu'aucune clé ne couvre reste accordé
|
|
38
|
+
* à tout porteur (comportement historique).
|
|
39
|
+
*/
|
|
40
|
+
var ScopeGrantPolicy = class {
|
|
41
|
+
#exact = /* @__PURE__ */ new Map();
|
|
42
|
+
#families = [];
|
|
43
|
+
/**
|
|
44
|
+
* @param grants - la section `grantableScopes`, déjà validée par le schéma.
|
|
45
|
+
*/
|
|
46
|
+
constructor(grants = {}) {
|
|
47
|
+
for (const [key, roles] of Object.entries(grants)) if (key.endsWith(WILDCARD_SUFFIX)) this.#families.push([key.slice(0, -1), roles]);
|
|
48
|
+
else this.#exact.set(key, roles);
|
|
49
|
+
this.#families.sort((a, b) => b[0].length - a[0].length);
|
|
50
|
+
}
|
|
51
|
+
/** `true` si aucune clé n'est déclarée — tout scope d'application est accordé. */
|
|
52
|
+
get isEmpty() {
|
|
53
|
+
return this.#exact.size === 0 && this.#families.length === 0;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Les rôles dont UN suffit pour obtenir ce scope.
|
|
57
|
+
*
|
|
58
|
+
* @param scope - un scope demandé.
|
|
59
|
+
* @returns les rôles de la clé la plus spécifique, ou `null` si aucune clé ne
|
|
60
|
+
* le couvre (scope libre).
|
|
61
|
+
*/
|
|
62
|
+
rolesFor(scope) {
|
|
63
|
+
const exact = this.#exact.get(scope);
|
|
64
|
+
if (exact !== void 0) return exact;
|
|
65
|
+
for (const [prefix, roles] of this.#families) if (scope.startsWith(prefix)) return roles;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Les scopes demandés que ce porteur ne peut pas obtenir, avec la règle qui
|
|
71
|
+
* refuse chacun.
|
|
72
|
+
*
|
|
73
|
+
* Seul juge des DEUX familles, appelé par chaque émetteur de l'application
|
|
74
|
+
* (jeton d'accès, clé API) : la règle d'administration du framework
|
|
75
|
+
* ({@link refusedAdminScopes}, jugée sur les rôles PLATS du compte, comme le
|
|
76
|
+
* plan d'administration) et la politique de l'application
|
|
77
|
+
* ({@link ScopeGrantPolicy}, jugée hiérarchie de rôles comprise, comme un
|
|
78
|
+
* `@IsGranted`).
|
|
79
|
+
*
|
|
80
|
+
* @param requested - scopes demandés (RFC 6749 §3.3).
|
|
81
|
+
* @param roles - rôles du porteur, tels que son compte les porte.
|
|
82
|
+
* @param policy - politique compilée de `grantableScopes`.
|
|
83
|
+
* @param hierarchy - hiérarchie de rôles ; absente, seuls les rôles plats comptent.
|
|
84
|
+
* @returns les refus, dans l'ordre de la demande — vide si tout est accordé.
|
|
85
|
+
*/
|
|
86
|
+
function refusedScopes(requested, roles, policy, hierarchy) {
|
|
87
|
+
const admin = refusedAdminScopes(requested, roles);
|
|
88
|
+
if (admin.length === 0 && policy.isEmpty) return [];
|
|
89
|
+
let reachable = null;
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const scope of requested) {
|
|
92
|
+
if (admin.includes(scope)) {
|
|
93
|
+
out.push({
|
|
94
|
+
scope,
|
|
95
|
+
rule: "admin"
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const allowed = policy.rolesFor(scope);
|
|
100
|
+
if (allowed === null) continue;
|
|
101
|
+
reachable ??= hierarchy ? hierarchy.reachableRoles(roles) : new Set(roles);
|
|
102
|
+
if (!allowed.some((role) => reachable?.has(role))) out.push({
|
|
103
|
+
scope,
|
|
104
|
+
rule: "application"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
export { RESERVED_SCOPE_GRANT_KEYS, ScopeGrantPolicy, refusedScopes, scopeGrantKeyProblem };
|
|
@@ -3,7 +3,7 @@ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, write
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { link, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import process from "node:process";
|
|
6
|
-
|
|
6
|
+
// nodefony/src/token/secretFile.ts
|
|
7
7
|
/**
|
|
8
8
|
* Écrire un SECRET sur disque — la seule implémentation du dépôt.
|
|
9
9
|
*
|
|
@@ -187,5 +187,4 @@ function writeSecretSync(file, content) {
|
|
|
187
187
|
chmodSync(file, 384);
|
|
188
188
|
} catch {}
|
|
189
189
|
}
|
|
190
|
-
//#endregion
|
|
191
190
|
export { MODE_SECRET, createSecretExclusive, messageNonRestreint, modeNonRestreint, modeNonRestreintAsync, readIfPresent, readIfPresentSync, writeSecret, writeSecretSync };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/token/tokenCriteria.ts
|
|
2
2
|
/**
|
|
3
3
|
* Fragment de critère **portable** exprimant l'état de vie d'un jeton — la
|
|
4
4
|
* traduction que les adapters SQL et Mongo partagent au lieu de la réécrire.
|
|
@@ -32,5 +32,4 @@ function tokenStatusCriteria(status, now) {
|
|
|
32
32
|
default: return {};
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { tokenStatusCriteria };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/token/tokenFilters.ts
|
|
2
2
|
/**
|
|
3
3
|
* **Le vocabulaire de filtre des jetons**, en noms PUBLICS — ceux qu'un client
|
|
4
4
|
* écrit dans l'URL (`?status=revoked`), jamais des noms de colonne.
|
|
@@ -68,5 +68,4 @@ const TOKEN_FACETS = {
|
|
|
68
68
|
* Un test verrouille l'accord entre cette liste et {@link TOKEN_FACETS}.
|
|
69
69
|
*/
|
|
70
70
|
const TOKEN_STATS_FILTERS = { subjectId: "string" };
|
|
71
|
-
//#endregion
|
|
72
71
|
export { TOKEN_FACETS, TOKEN_FILTERS, TOKEN_STATS_FILTERS };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/token/tokenSort.ts
|
|
2
2
|
/**
|
|
3
3
|
* **Le vocabulaire de tri des jetons**, en noms PUBLICS — ceux qu'un client écrit
|
|
4
4
|
* dans l'URL (`?order=createdAt:DESC`), jamais des noms de colonne.
|
|
@@ -36,5 +36,4 @@ const TOKEN_SORTABLE_FIELDS = [
|
|
|
36
36
|
* ligne entre deux pages).
|
|
37
37
|
*/
|
|
38
38
|
const TOKEN_DEFAULT_ORDER = [["createdAt", "DESC"], ["id", "DESC"]];
|
|
39
|
-
//#endregion
|
|
40
39
|
export { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/token/tokenStatus.ts
|
|
2
2
|
/**
|
|
3
3
|
* **La** définition de l'état d'un jeton — un seul exemplaire, pour les backends
|
|
4
4
|
* qui évaluent en mémoire (store mémoire, filtrage inline d'un batch `SCAN`).
|
|
@@ -31,5 +31,4 @@ function tokenStatusOf(token, now) {
|
|
|
31
31
|
function matchesTokenStatus(token, status, now) {
|
|
32
32
|
return status === void 0 || tokenStatusOf(token, now) === status;
|
|
33
33
|
}
|
|
34
|
-
//#endregion
|
|
35
34
|
export { matchesTokenStatus, tokenStatusOf };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MemoryTokenStore } from "./MemoryTokenStore.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/token/tokenStoreRegistry.ts
|
|
3
3
|
const factories = /* @__PURE__ */ new Map();
|
|
4
4
|
/**
|
|
5
5
|
* Enregistre (ou remplace) la fabrique d'un store de jetons. Appelée par le
|
|
@@ -21,5 +21,4 @@ registerTokenStore("memory", (ctx) => {
|
|
|
21
21
|
const days = ctx?.config?.tokenStore?.retentionRevokedDays;
|
|
22
22
|
return typeof days === "number" ? new MemoryTokenStore(Date.now, days * 864e5) : new MemoryTokenStore();
|
|
23
23
|
});
|
|
24
|
-
//#endregion
|
|
25
24
|
export { getTokenStoreFactory, listTokenStores, registerTokenStore };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertPageQuery } from "nodefony";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/totp/MemoryTotpSecretStore.ts
|
|
3
3
|
/**
|
|
4
4
|
* Projette un secret en vue d'introspection — **le seul endroit** où l'on
|
|
5
5
|
* décide ce qui sort d'un store TOTP en mémoire. `secretEnc` et les condensats
|
|
@@ -105,5 +105,4 @@ var MemoryTotpSecretStore = class {
|
|
|
105
105
|
for (const secret of snapshot.secrets) this.#byUser.set(secret.userId, secret);
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
|
-
//#endregion
|
|
109
108
|
export { MemoryTotpSecretStore, MemoryTotpSecretStore as default, matchesTotpQuery, toTotpEnrollment };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey } from "../crypto/secretCipher.js";
|
|
2
2
|
import { Buffer } from "node:buffer";
|
|
3
|
-
|
|
3
|
+
// nodefony/src/totp/totpCipher.ts
|
|
4
4
|
/**
|
|
5
5
|
* Chiffrement réversible du secret TOTP au repos — façade de domaine au-dessus
|
|
6
6
|
* de la brique générique {@link ../crypto/secretCipher}.
|
|
@@ -26,5 +26,4 @@ const TOTP_DERIVATION = {
|
|
|
26
26
|
function deriveTotpKey(material) {
|
|
27
27
|
return deriveKey(material, TOTP_DERIVATION);
|
|
28
28
|
}
|
|
29
|
-
//#endregion
|
|
30
29
|
export { decryptSecret, deriveTotpKey, encryptSecret, generateEphemeralKey };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/totp/totpCrypto.ts
|
|
3
3
|
/** Paramètres TOTP — défauts interopérables (Google Authenticator). */
|
|
4
4
|
const TOTP_DEFAULTS = {
|
|
5
5
|
/** Taille du secret en octets (RFC 4226 R6 : 160 bits recommandés). */
|
|
@@ -222,5 +222,4 @@ function matchRecoveryCode(presented, hashes) {
|
|
|
222
222
|
}
|
|
223
223
|
return found;
|
|
224
224
|
}
|
|
225
|
-
//#endregion
|
|
226
225
|
export { TOTP_DEFAULTS, base32Decode, base32Encode, buildOtpauthUri, generateRecoveryCodes, generateTotpSecret, hashRecoveryCode, hotp, matchRecoveryCode, totpCode, totpCounter, unbiasedIndices, verifyTotp };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decryptSecret, encryptSecret } from "../crypto/secretCipher.js";
|
|
2
2
|
import "./totpCipher.js";
|
|
3
3
|
import { base32Encode, buildOtpauthUri, generateRecoveryCodes, generateTotpSecret, hashRecoveryCode, matchRecoveryCode, verifyTotp } from "./totpCrypto.js";
|
|
4
|
-
|
|
4
|
+
// nodefony/src/totp/totpOperations.ts
|
|
5
5
|
/**
|
|
6
6
|
* Démarre l'enrôlement : génère un secret aléatoire, le **chiffre** au repos et
|
|
7
7
|
* l'enregistre en attente de confirmation (`confirmedAt: null`). Retourne le
|
|
@@ -125,5 +125,4 @@ async function totpStatus(deps, userId) {
|
|
|
125
125
|
recoveryCodesRemaining: record.recoveryCodes.length
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
-
//#endregion
|
|
129
128
|
export { beginTotpEnrollment, confirmTotpEnrollment, disableTotp, totpStatus, verifyTotpLogin };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MemoryTotpSecretStore } from "./MemoryTotpSecretStore.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/totp/totpSecretStoreRegistry.ts
|
|
3
3
|
const factories = /* @__PURE__ */ new Map();
|
|
4
4
|
/** Enregistre (ou remplace) la fabrique d'un store de secrets TOTP. */
|
|
5
5
|
function registerTotpStore(name, factory) {
|
|
@@ -14,5 +14,4 @@ function listTotpStores() {
|
|
|
14
14
|
return [...factories.keys()];
|
|
15
15
|
}
|
|
16
16
|
registerTotpStore("memory", () => new MemoryTotpSecretStore());
|
|
17
|
-
//#endregion
|
|
18
17
|
export { getTotpStoreFactory, listTotpStores, registerTotpStore };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InsecureTransportError } from "../../errors/InsecureTransportError.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/transport/CredentialTransportPolicy.ts
|
|
3
3
|
/**
|
|
4
4
|
* Dit si un secret de connexion se refuse : en production, sans échappement
|
|
5
5
|
* explicite. Seule source de la décision — le firewall la compose au boot.
|
|
@@ -47,5 +47,4 @@ var CredentialTransportPolicy = class {
|
|
|
47
47
|
throw new InsecureTransportError();
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
//#endregion
|
|
51
50
|
export { CredentialTransportPolicy, shouldEnforceCredentialTransport };
|