@nodefony/security 10.0.0-beta.2 → 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.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +48 -16
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
- package/dist/nodefony/command/security-oauth-doctor.js +68 -0
- package/dist/nodefony/command/security-secrets.js +52 -29
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +10 -7
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +9 -14
- package/dist/nodefony/config/config.js +99 -8
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/contracts/index.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 +23 -0
- 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/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
- package/dist/nodefony/scaffold/contribute.js +19 -0
- 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 +118 -16
- 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 +116 -25
- package/dist/nodefony/service/keycloak.js +208 -0
- package/dist/nodefony/service/oauth2.js +403 -28
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +94 -27
- package/dist/nodefony/service/totp.js +55 -11
- package/dist/nodefony/service/webAuthn.js +24 -7
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +15 -5
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -3
- package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
- 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 +14 -59
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- 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 +26 -0
- 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 +38 -0
- 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 +39 -0
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +28 -12
- package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
- package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
- package/dist/nodefony/src/oauth/providerRoles.js +78 -0
- package/dist/nodefony/src/oauth/providers/github.js +48 -12
- package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
- package/dist/nodefony/src/openToSignup.js +46 -0
- 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 +25 -0
- 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 +89 -48
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
- 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 +60 -4
- 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 +50 -0
- 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/index.d.ts +26 -6
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -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-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 +64 -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/ApiKeyError.d.ts +3 -1
- 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/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- 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 +145 -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/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- 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/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- 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/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/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -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/scopeGrants.d.ts +77 -0
- 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 +182 -175
- package/docs/audit.md +141 -141
- package/docs/authenticators.md +129 -96
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +77 -72
- package/docs/external-jwt.md +83 -11
- package/docs/firewall.md +119 -83
- package/docs/headers.md +82 -82
- package/docs/index.md +16 -8
- package/docs/keycloak.md +835 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +291 -166
- package/docs/obtenir-un-jeton.md +131 -34
- package/docs/tokens.md +197 -140
- package/docs/totp.md +177 -136
- package/docs/webauthn.md +157 -154
- package/docs/webhooks.md +128 -125
- 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 +19 -17
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -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 };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// nodefony/src/sessionAuthentication.ts
|
|
2
|
+
/**
|
|
3
|
+
* Facteurs d'authentification de Nodefony → méthodes RFC 8176, posées dans la
|
|
4
|
+
* session à son ouverture. Les clés et leur lecteur sont au cœur
|
|
5
|
+
* (`readSessionAuthentication`, `nodefony`) ; seule la POLITIQUE de
|
|
6
|
+
* correspondance vit ici.
|
|
7
|
+
*/
|
|
8
|
+
const FACTOR_AMR = Object.freeze({
|
|
9
|
+
password: Object.freeze(["pwd"]),
|
|
10
|
+
totp: Object.freeze(["pwd", "otp"]),
|
|
11
|
+
recovery: Object.freeze(["pwd", "otp"]),
|
|
12
|
+
webauthn: Object.freeze(["pop"])
|
|
13
|
+
});
|
|
14
|
+
const NO_LOCAL_FACTOR = Object.freeze([]);
|
|
15
|
+
/**
|
|
16
|
+
* Valeurs RFC 8176 d'un facteur nommé par un chemin d'authentification.
|
|
17
|
+
*
|
|
18
|
+
* @param factor - le facteur tel que l'audit le nomme (`password`, `totp`,
|
|
19
|
+
* `recovery`, `webauthn`, `oauth`…)
|
|
20
|
+
* @returns les méthodes vérifiées localement ; vide pour un facteur délégué
|
|
21
|
+
*/
|
|
22
|
+
function amrForFactor(factor) {
|
|
23
|
+
return Object.hasOwn(FACTOR_AMR, factor) ? FACTOR_AMR[factor] ?? NO_LOCAL_FACTOR : NO_LOCAL_FACTOR;
|
|
24
|
+
}
|
|
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
|
-
import { messageNonRestreint, modeNonRestreintAsync, readIfPresent
|
|
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
|
*
|
|
@@ -36,6 +37,67 @@ function keystoreLeaksIntoImage(dir) {
|
|
|
36
37
|
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
38
|
}
|
|
38
39
|
/**
|
|
40
|
+
* Lit un jeu de clés sérialisé et en vérifie la FORME — la seule lecture du
|
|
41
|
+
* dépôt, appelée par le keystore ET par la validation de configuration.
|
|
42
|
+
*
|
|
43
|
+
* Ne vérifie que la structure (`keys` non vide, un `kid` par clé) : importer
|
|
44
|
+
* les clés exige `jose`, chargé paresseusement au premier jeton. Les messages
|
|
45
|
+
* ne recopient JAMAIS la valeur reçue — elle porte une clé privée.
|
|
46
|
+
*
|
|
47
|
+
* @param json - le jeu de clés, tel que `NF_JWT_KEYSET` ou `keyset.json` le porte.
|
|
48
|
+
* @returns le jeu de clés lu.
|
|
49
|
+
* @throws Error si le JSON est illisible ou la structure incomplète.
|
|
50
|
+
*/
|
|
51
|
+
function parseKeySet(json) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(json);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error("JwtKeystore: keyset JSON invalide");
|
|
57
|
+
}
|
|
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");
|
|
70
|
+
}
|
|
71
|
+
/** Génère une nouvelle paire Ed25519 (extractable pour persistance JWK). */
|
|
72
|
+
async function buildKeySet(jose) {
|
|
73
|
+
const { publicKey, privateKey } = await jose.generateKeyPair("Ed25519", { extractable: true });
|
|
74
|
+
const publicJwk = await jose.exportJWK(publicKey);
|
|
75
|
+
const kid = await jose.calculateJwkThumbprint(publicJwk, "sha256");
|
|
76
|
+
return {
|
|
77
|
+
active: kid,
|
|
78
|
+
keys: [{
|
|
79
|
+
...await jose.exportJWK(privateKey),
|
|
80
|
+
kid,
|
|
81
|
+
alg: "EdDSA",
|
|
82
|
+
use: "sig",
|
|
83
|
+
createdAt: Date.now()
|
|
84
|
+
}]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Génère un jeu de clés de signature à partager entre tous les process d'une
|
|
89
|
+
* application — la valeur de `NF_JWT_KEYSET`.
|
|
90
|
+
*
|
|
91
|
+
* Même générateur que celui du keystore (une seule implémentation), sérialisé
|
|
92
|
+
* en JSON COMPACT : une ligne, sans quote simple, qu'un `.env` porte entre
|
|
93
|
+
* quotes simples et qu'un gestionnaire de secrets stocke tel quel.
|
|
94
|
+
*
|
|
95
|
+
* @returns le jeu de clés sérialisé — il contient la clé PRIVÉE.
|
|
96
|
+
*/
|
|
97
|
+
async function generateKeySet() {
|
|
98
|
+
return JSON.stringify(await buildKeySet(await import("jose")));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
39
101
|
* Keystore Ed25519 — implémentation de référence d'{@link IJwtKeystore}.
|
|
40
102
|
*
|
|
41
103
|
* Résout la clé de signature selon une **priorité** (jamais d'auto-génération en
|
|
@@ -55,10 +117,15 @@ function keystoreLeaksIntoImage(dir) {
|
|
|
55
117
|
* usage ; le boot ne paie rien si le JWT n'est jamais sollicité. Le chargement
|
|
56
118
|
* est mémoïsé (une seule résolution concurrente).
|
|
57
119
|
*
|
|
58
|
-
* @remarks
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
120
|
+
* @remarks Plusieurs process qui servent la MÊME application doivent signer
|
|
121
|
+
* avec la même clé, sinon un jeton signé par l'un est refusé par l'autre :
|
|
122
|
+
* - **pods** (aucun disque partagé) → source `env` obligatoire, la même valeur
|
|
123
|
+
* injectée partout (`NF_JWT_KEYSET`, générée par `security:secrets`) ;
|
|
124
|
+
* - **workers de `nodefony cluster`** → la source `env` est héritée au fork ; à
|
|
125
|
+
* défaut, la source `fichier` est sûre : la création de `keyset.json` est
|
|
126
|
+
* EXCLUSIVE ({@link createSecretExclusive}), un seul worker la génère et les
|
|
127
|
+
* autres relisent la sienne ;
|
|
128
|
+
* - source `mémoire` → une clé par process, jamais cohérente.
|
|
62
129
|
*/
|
|
63
130
|
var JwtKeystore = class {
|
|
64
131
|
#source;
|
|
@@ -91,7 +158,7 @@ var JwtKeystore = class {
|
|
|
91
158
|
async #load() {
|
|
92
159
|
const jose = await import("jose");
|
|
93
160
|
if (this.#source.keySetJson) {
|
|
94
|
-
await this.#importKeyset(jose,
|
|
161
|
+
await this.#importKeyset(jose, parseKeySet(this.#source.keySetJson));
|
|
95
162
|
return;
|
|
96
163
|
}
|
|
97
164
|
if (this.#source.dir) {
|
|
@@ -101,32 +168,24 @@ var JwtKeystore = class {
|
|
|
101
168
|
const existing = await this.#readFile(file);
|
|
102
169
|
if (existing) {
|
|
103
170
|
await this.#checkRestricted(file);
|
|
104
|
-
await this.#importKeyset(jose,
|
|
171
|
+
await this.#importKeyset(jose, parseKeySet(existing));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const keyset = await buildKeySet(jose);
|
|
175
|
+
if (await createSecretExclusive(file, JSON.stringify(keyset))) {
|
|
176
|
+
this.#log(`JWT keystore: clé Ed25519 générée et persistée (${file}).`, "INFO");
|
|
177
|
+
await this.#checkRestricted(file);
|
|
178
|
+
await this.#importKeyset(jose, keyset);
|
|
105
179
|
return;
|
|
106
180
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
await this.#
|
|
181
|
+
const winner = await this.#readFile(file);
|
|
182
|
+
if (winner === null) throw new Error(`JwtKeystore: ${file} a disparu juste après sa création`);
|
|
183
|
+
await this.#checkRestricted(file);
|
|
184
|
+
await this.#importKeyset(jose, parseKeySet(winner));
|
|
110
185
|
return;
|
|
111
186
|
}
|
|
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
|
-
};
|
|
187
|
+
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");
|
|
188
|
+
await this.#importKeyset(jose, await buildKeySet(jose));
|
|
130
189
|
}
|
|
131
190
|
/** Importe un keyset persisté : privée → `CryptoKey`, public → JWK sans `d`. */
|
|
132
191
|
async #importKeyset(jose, keyset) {
|
|
@@ -153,27 +212,10 @@ var JwtKeystore = class {
|
|
|
153
212
|
this.#keys = loaded;
|
|
154
213
|
this.#activeKid = keyset.active && loaded.some((k) => k.kid === keyset.active) ? keyset.active : firstKey.kid;
|
|
155
214
|
}
|
|
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
215
|
/** Lit un fichier ; `null` si absent (ENOENT), relance toute autre erreur. */
|
|
168
216
|
async #readFile(file) {
|
|
169
217
|
return readIfPresent(file);
|
|
170
218
|
}
|
|
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
219
|
/**
|
|
178
220
|
* Constate le mode EFFECTIF du fichier de clés et avertit s'il n'est pas 0600.
|
|
179
221
|
*
|
|
@@ -193,5 +235,4 @@ var JwtKeystore = class {
|
|
|
193
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");
|
|
194
236
|
}
|
|
195
237
|
};
|
|
196
|
-
|
|
197
|
-
export { JwtKeystore, JwtKeystore as default, keystoreLeaksIntoImage };
|
|
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 {
|
|
2
|
-
|
|
1
|
+
import { describeTransportFailure, untrustedAuthorityHint } from "../net/transportFailure.js";
|
|
2
|
+
import { canonicalIssuer, extractScopes, metadataCandidateUrls, parseDiscoveryUrl, validateIssuerMetadata } from "nodefony";
|
|
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.
|
|
@@ -65,8 +66,8 @@ var RemoteJwtVerifier = class {
|
|
|
65
66
|
#jose = null;
|
|
66
67
|
/**
|
|
67
68
|
* @param options - émetteurs de confiance et réglages réseau
|
|
68
|
-
* @throws Error si un émetteur est invalide, dupliqué,
|
|
69
|
-
* algorithme à secret partagé
|
|
69
|
+
* @throws Error si un émetteur est invalide, dupliqué, déclare un
|
|
70
|
+
* algorithme à secret partagé ou une `discoveryUrl` invalide
|
|
70
71
|
*/
|
|
71
72
|
constructor(options) {
|
|
72
73
|
this.#options = options;
|
|
@@ -75,6 +76,7 @@ var RemoteJwtVerifier = class {
|
|
|
75
76
|
const issuer = canonicalIssuer(trusted.issuer);
|
|
76
77
|
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
78
|
assertAsymmetric(issuer, trusted.algorithms);
|
|
79
|
+
if (trusted.discoveryUrl !== void 0) parseDiscoveryUrl(trusted.discoveryUrl);
|
|
78
80
|
this.#issuers.set(issuer, {
|
|
79
81
|
...trusted,
|
|
80
82
|
issuer
|
|
@@ -101,6 +103,36 @@ var RemoteJwtVerifier = class {
|
|
|
101
103
|
* « jeton invalide »
|
|
102
104
|
*/
|
|
103
105
|
async verify(token, audience) {
|
|
106
|
+
const verified = await this.verifyClaims(token, audience);
|
|
107
|
+
if (verified === null) return null;
|
|
108
|
+
const { issuer, payload } = verified;
|
|
109
|
+
return {
|
|
110
|
+
issuer,
|
|
111
|
+
subject: typeof payload.sub === "string" ? payload.sub : void 0,
|
|
112
|
+
scopes: extractScopes(payload),
|
|
113
|
+
expiresAt: typeof payload.exp === "number" ? payload.exp : void 0,
|
|
114
|
+
issuedAt: typeof payload.iat === "number" ? payload.iat : void 0,
|
|
115
|
+
tokenId: typeof payload.jti === "string" ? payload.jti : void 0,
|
|
116
|
+
claims: payload
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Vérifie un jeton et rend ses claims BRUTS — pour un jeton qui n'est pas un
|
|
121
|
+
* jeton d'accès (jeton de déconnexion OpenID Connect, par exemple), dont le
|
|
122
|
+
* sens se lit dans des claims que {@link verify} ne remonte pas.
|
|
123
|
+
*
|
|
124
|
+
* Mêmes garanties et même contrat d'erreur que {@link verify} : signature,
|
|
125
|
+
* émetteur de la liste fermée, algorithme imposé, audience obligatoire,
|
|
126
|
+
* claims exigés par l'émetteur ; un refus est un `null`, une panne lève.
|
|
127
|
+
*
|
|
128
|
+
* @param token - le jeton brut, tel que présenté
|
|
129
|
+
* @param audience - audience que le jeton DOIT porter dans `aud`
|
|
130
|
+
* @returns l'émetteur canonique et les claims vérifiés, ou `null` si le jeton
|
|
131
|
+
* est refusé
|
|
132
|
+
* @throws Error si l'émetteur ne peut pas être joint ou publie un jeu de clés
|
|
133
|
+
* inutilisable
|
|
134
|
+
*/
|
|
135
|
+
async verifyClaims(token, audience) {
|
|
104
136
|
if (typeof token !== "string" || token.length === 0) return null;
|
|
105
137
|
const jose = this.#jose ??= await import("jose");
|
|
106
138
|
let claimedIssuer;
|
|
@@ -126,18 +158,16 @@ var RemoteJwtVerifier = class {
|
|
|
126
158
|
...trusted.typ ? { typ: trusted.typ } : {},
|
|
127
159
|
...trusted.requiredClaims?.length ? { requiredClaims: [...trusted.requiredClaims] } : {}
|
|
128
160
|
});
|
|
129
|
-
const subject = typeof payload.sub === "string" ? payload.sub : void 0;
|
|
130
161
|
return {
|
|
131
162
|
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
|
|
163
|
+
payload
|
|
137
164
|
};
|
|
138
165
|
} catch (error) {
|
|
139
166
|
const code = error.code;
|
|
140
|
-
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
|
+
}
|
|
141
171
|
this.#audit(`jeton refusé (${trusted.issuer}) : ${code}`);
|
|
142
172
|
return null;
|
|
143
173
|
}
|
|
@@ -167,7 +197,7 @@ var RemoteJwtVerifier = class {
|
|
|
167
197
|
trusted,
|
|
168
198
|
getKey: jose.createLocalJWKSet(await trusted.localJwks())
|
|
169
199
|
};
|
|
170
|
-
const jwksUri = trusted.jwksUri ?? await this.#discover(trusted
|
|
200
|
+
const jwksUri = trusted.jwksUri ?? await this.#discover(trusted);
|
|
171
201
|
const options = {
|
|
172
202
|
timeoutDuration: this.#options.timeoutMs ?? 5e3,
|
|
173
203
|
cooldownDuration: this.#options.cooldownMs ?? 3e4,
|
|
@@ -188,11 +218,12 @@ var RemoteJwtVerifier = class {
|
|
|
188
218
|
* « meilleure réponse que rien » : il est écarté comme s'il n'avait pas
|
|
189
219
|
* répondu, et la recherche continue.
|
|
190
220
|
*/
|
|
191
|
-
async #discover(
|
|
221
|
+
async #discover(trusted) {
|
|
222
|
+
const { issuer, discoveryUrl } = trusted;
|
|
192
223
|
const doFetch = this.#options.fetch ?? globalThis.fetch;
|
|
193
224
|
const timeout = this.#options.timeoutMs ?? 5e3;
|
|
194
225
|
const failures = [];
|
|
195
|
-
for (const url of
|
|
226
|
+
for (const url of metadataCandidateUrls(issuer, discoveryUrl)) try {
|
|
196
227
|
const response = await doFetch(url, {
|
|
197
228
|
signal: AbortSignal.timeout(timeout),
|
|
198
229
|
redirect: "manual",
|
|
@@ -202,7 +233,7 @@ var RemoteJwtVerifier = class {
|
|
|
202
233
|
failures.push(`${url} → HTTP ${response.status}`);
|
|
203
234
|
continue;
|
|
204
235
|
}
|
|
205
|
-
const metadata = validateIssuerMetadata(await response.json(), issuer);
|
|
236
|
+
const metadata = validateIssuerMetadata(await response.json(), issuer, discoveryUrl);
|
|
206
237
|
this.#audit(`émetteur « ${issuer} » découvert : clés sur ${metadata.jwksUri}.`);
|
|
207
238
|
return metadata.jwksUri;
|
|
208
239
|
} catch (error) {
|
|
@@ -227,5 +258,4 @@ function assertAsymmetric(issuer, algorithms) {
|
|
|
227
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.`);
|
|
228
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.`);
|
|
229
260
|
}
|
|
230
|
-
//#endregion
|
|
231
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 };
|