@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
|
@@ -3,10 +3,9 @@ import { UnverifiableTokenError } from "../../errors/UnverifiableTokenError.js";
|
|
|
3
3
|
import { UserToken } from "../token/UserToken.js";
|
|
4
4
|
import { bearerToken } from "./bearer.js";
|
|
5
5
|
import peekIssuer from "./peekIssuer.js";
|
|
6
|
-
import {
|
|
7
|
-
import { ACCESS_TOKEN_VERIFIER, buildBearerChallenge,
|
|
8
|
-
|
|
9
|
-
//#region nodefony/src/authenticator/ExternalJwtAuthenticator.ts
|
|
6
|
+
import { ExternalSubjectResolver } from "./ExternalSubjectResolver.js";
|
|
7
|
+
import { ACCESS_TOKEN_VERIFIER, buildBearerChallenge, protectedResourceMetadataUrl } from "nodefony";
|
|
8
|
+
// nodefony/src/authenticator/ExternalJwtAuthenticator.ts
|
|
10
9
|
/**
|
|
11
10
|
* Message UNIFORME de refus — la cause fine (expiré, audience, signature, sujet
|
|
12
11
|
* inconnu) part au journal, jamais au client : un refus détaillé est un oracle
|
|
@@ -16,6 +15,11 @@ const INVALID_TOKEN = "Invalid token";
|
|
|
16
15
|
/** Clé de transport de l'audience entre `createToken` et `authenticate`. */
|
|
17
16
|
const AUDIENCE = "audience";
|
|
18
17
|
/**
|
|
18
|
+
* Clé de transport de la zone qui SURCHARGE la politique de rattachement.
|
|
19
|
+
* Posée seulement par ces zones-là : les autres ne paient rien de plus.
|
|
20
|
+
*/
|
|
21
|
+
const SUBJECT_AREA = "subjectArea";
|
|
22
|
+
/**
|
|
19
23
|
* Authentification par **jeton d'accès émis par un serveur d'autorisation
|
|
20
24
|
* TIERS** (Keycloak, Auth0, Entra, ou l'émetteur d'une flotte d'agents).
|
|
21
25
|
*
|
|
@@ -55,25 +59,15 @@ const AUDIENCE = "audience";
|
|
|
55
59
|
var ExternalJwtAuthenticator = class {
|
|
56
60
|
name = "external-jwt";
|
|
57
61
|
#container;
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
#ephemeralRoles;
|
|
61
|
-
#userProvider = null;
|
|
62
|
+
/** Rattachement du sujet à un compte local — partagé, jamais recopié. */
|
|
63
|
+
#subjects;
|
|
62
64
|
/**
|
|
63
65
|
* @param container - container DI (résolution lazy du vérificateur et de `users`)
|
|
64
66
|
* @param options - émetteurs reconnus et politique de rattachement
|
|
65
67
|
*/
|
|
66
68
|
constructor(container, options) {
|
|
67
69
|
this.#container = container;
|
|
68
|
-
|
|
69
|
-
for (const binding of options.issuers) try {
|
|
70
|
-
issuers.set(canonicalIssuer(binding.issuer), binding.subjectMapping);
|
|
71
|
-
} catch {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
this.#issuers = issuers;
|
|
75
|
-
this.#policy = options.subjectPolicy;
|
|
76
|
-
this.#ephemeralRoles = options.ephemeralRoles;
|
|
70
|
+
this.#subjects = new ExternalSubjectResolver(container, options);
|
|
77
71
|
}
|
|
78
72
|
/**
|
|
79
73
|
* La requête porte-t-elle un jeton se réclamant d'un émetteur de confiance ?
|
|
@@ -89,12 +83,7 @@ var ExternalJwtAuthenticator = class {
|
|
|
89
83
|
if (raw === null) return false;
|
|
90
84
|
const issuer = peekIssuer(raw);
|
|
91
85
|
if (issuer === null) return false;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
return this.#issuers.has(canonicalIssuer(issuer));
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
86
|
+
return this.#subjects.recognizes(issuer);
|
|
98
87
|
}
|
|
99
88
|
/**
|
|
100
89
|
* Extrait le jeton brut ET l'audience de la zone.
|
|
@@ -108,6 +97,7 @@ var ExternalJwtAuthenticator = class {
|
|
|
108
97
|
const token = new UserToken("external-jwt", bearerToken(auth) ?? "");
|
|
109
98
|
const area = context.security;
|
|
110
99
|
if (area?.resource) token.setAttribute(AUDIENCE, area.resource);
|
|
100
|
+
if (area?.subjectPolicy !== void 0) token.setAttribute(SUBJECT_AREA, area);
|
|
111
101
|
return Promise.resolve(token);
|
|
112
102
|
}
|
|
113
103
|
/**
|
|
@@ -134,9 +124,7 @@ var ExternalJwtAuthenticator = class {
|
|
|
134
124
|
if (principal === null) throw new AuthenticationError(INVALID_TOKEN);
|
|
135
125
|
const subject = principal.subject;
|
|
136
126
|
if (!subject) throw new AuthenticationError(INVALID_TOKEN);
|
|
137
|
-
const
|
|
138
|
-
if (mapping === void 0) throw new UnverifiableTokenError(`émetteur « ${principal.issuer} » vérifié mais absent de la table de rattachement des sujets — configuration incohérente.`);
|
|
139
|
-
const user = await this.#resolveUser(principal.issuer, subject, mapping);
|
|
127
|
+
const user = await this.#subjects.resolve(principal.issuer, subject, principal.claims, token.getAttribute(SUBJECT_AREA));
|
|
140
128
|
const ut = token;
|
|
141
129
|
ut.promote(user);
|
|
142
130
|
ut.setAttribute("scopes", [...principal.scopes]);
|
|
@@ -187,38 +175,5 @@ var ExternalJwtAuthenticator = class {
|
|
|
187
175
|
validateArea(area) {
|
|
188
176
|
if (!area.resource) throw new Error(`area "${area.name}": l'authenticator "${this.name}" exige que la zone déclare sa ressource (\`resource\`) — c'est l'audience que le jeton doit porter (RFC 8707 §2). Sans elle, un jeton valide délivré au même porteur pour un AUTRE service serait accepté ici.`);
|
|
189
177
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Établit l'utilisateur applicatif à partir du sujet du jeton.
|
|
192
|
-
*
|
|
193
|
-
* Les deux politiques répondent à deux questions différentes : « qui, chez
|
|
194
|
-
* nous, est cette personne ? » et « quel pouvoir accorde-t-on à une machine
|
|
195
|
-
* que l'annuaire a authentifiée ? ».
|
|
196
|
-
*/
|
|
197
|
-
async #resolveUser(issuer, subject, mapping) {
|
|
198
|
-
const identifier = localIdentifierFor(issuer, subject, mapping);
|
|
199
|
-
if (this.#policy === "ephemeral") return new BaseUser({
|
|
200
|
-
id: identifier,
|
|
201
|
-
identifier,
|
|
202
|
-
roles: [...this.#ephemeralRoles]
|
|
203
|
-
});
|
|
204
|
-
const provider = this.#resolveUserProvider();
|
|
205
|
-
let user;
|
|
206
|
-
try {
|
|
207
|
-
user = await provider.loadUserByIdentifier(identifier);
|
|
208
|
-
} catch {
|
|
209
|
-
throw new AuthenticationError(INVALID_TOKEN);
|
|
210
|
-
}
|
|
211
|
-
if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
|
|
212
|
-
return user;
|
|
213
|
-
}
|
|
214
|
-
#resolveUserProvider() {
|
|
215
|
-
if (this.#userProvider === null) {
|
|
216
|
-
const provider = this.#container.get("users");
|
|
217
|
-
if (!provider) throw new Error("ExternalJwtAuthenticator: aucun service \"users\" (IUserProvider) dans le container — enregistrer un UserService au boot, ou passer la politique de sujet à \"ephemeral\" si l'appelant est une machine sans compte local.");
|
|
218
|
-
this.#userProvider = provider;
|
|
219
|
-
}
|
|
220
|
-
return this.#userProvider;
|
|
221
|
-
}
|
|
222
178
|
};
|
|
223
|
-
//#endregion
|
|
224
179
|
export { ExternalJwtAuthenticator, ExternalJwtAuthenticator as default };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { compileProviderRoleMapping, mapProviderRoles } from "../oauth/providerRoles.js";
|
|
2
|
+
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
3
|
+
import { UnverifiableTokenError } from "../../errors/UnverifiableTokenError.js";
|
|
4
|
+
import { localIdentifierFor } from "./externalSubject.js";
|
|
5
|
+
import { canonicalIssuer } from "nodefony";
|
|
6
|
+
import { BaseUser } from "@nodefony/user";
|
|
7
|
+
// nodefony/src/authenticator/ExternalSubjectResolver.ts
|
|
8
|
+
/**
|
|
9
|
+
* Message UNIFORME de refus — la cause fine (sujet inconnu, compte verrouillé)
|
|
10
|
+
* part au journal, jamais au client : un refus détaillé est un oracle.
|
|
11
|
+
*/
|
|
12
|
+
const INVALID_TOKEN = "Invalid token";
|
|
13
|
+
/**
|
|
14
|
+
* Les options de rattachement des sujets externes, lues dans la configuration
|
|
15
|
+
* de sécurité — UNE lecture, pour la zone `external-jwt` comme pour le
|
|
16
|
+
* résolveur de compte d'une ressource protégée.
|
|
17
|
+
*
|
|
18
|
+
* @param config - configuration de sécurité validée
|
|
19
|
+
* @returns émetteurs, politique de sujet, fournisseurs de connexion liables
|
|
20
|
+
*/
|
|
21
|
+
function externalSubjectOptions(config) {
|
|
22
|
+
const rs = config.resourceServer;
|
|
23
|
+
return {
|
|
24
|
+
issuers: rs.issuers.map((i) => ({
|
|
25
|
+
issuer: i.issuer,
|
|
26
|
+
subjectMapping: i.subjectMapping
|
|
27
|
+
})),
|
|
28
|
+
subjectPolicy: rs.subjectPolicy,
|
|
29
|
+
ephemeralRoles: rs.ephemeralRoles,
|
|
30
|
+
oauthProviders: Object.entries(config.oauth2.providers).flatMap(([name, p]) => p.issuer ? [{
|
|
31
|
+
name,
|
|
32
|
+
issuer: p.issuer,
|
|
33
|
+
roleMapping: compileProviderRoleMapping(p)
|
|
34
|
+
}] : [])
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Établit l'utilisateur applicatif que désigne le sujet d'un jeton TIERS —
|
|
39
|
+
* la décision qui transforme « un annuaire a authentifié ce sujet » en « ce
|
|
40
|
+
* compte agit chez nous ».
|
|
41
|
+
*
|
|
42
|
+
* UNE implémentation, deux consommateurs : la zone `external-jwt` du pare-feu
|
|
43
|
+
* (`ExternalJwtAuthenticator`) et le résolveur de compte d'une ressource
|
|
44
|
+
* protégée (porte MCP). Recopiée, la règle d'espace de noms aurait divergé dès
|
|
45
|
+
* le premier émetteur ajouté — et c'est elle qui empêche un annuaire tiers de
|
|
46
|
+
* réclamer le compte local `admin`.
|
|
47
|
+
*/
|
|
48
|
+
var ExternalSubjectResolver = class {
|
|
49
|
+
#container;
|
|
50
|
+
#issuers;
|
|
51
|
+
#policy;
|
|
52
|
+
#ephemeralRoles;
|
|
53
|
+
/** Émetteur canonique → fournisseur de connexion ; `null` = aucun lien. */
|
|
54
|
+
#linkedProviders;
|
|
55
|
+
/** Fournisseur de connexion → table des rôles ; `null` = aucune table. */
|
|
56
|
+
#roleMappings;
|
|
57
|
+
#userProvider = null;
|
|
58
|
+
/**
|
|
59
|
+
* @param container - container DI (résolution lazy de `users`)
|
|
60
|
+
* @param options - émetteurs reconnus et politique de rattachement
|
|
61
|
+
*/
|
|
62
|
+
constructor(container, options) {
|
|
63
|
+
this.#container = container;
|
|
64
|
+
const issuers = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const binding of options.issuers) try {
|
|
66
|
+
issuers.set(canonicalIssuer(binding.issuer), binding.subjectMapping);
|
|
67
|
+
} catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
this.#issuers = issuers;
|
|
71
|
+
this.#policy = options.subjectPolicy;
|
|
72
|
+
this.#ephemeralRoles = options.ephemeralRoles;
|
|
73
|
+
this.#linkedProviders = linkProviders(issuers, options.oauthProviders);
|
|
74
|
+
this.#roleMappings = collectRoleMappings(options.oauthProviders);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* L'émetteur revendiqué est-il l'un des nôtres ? Ne lève JAMAIS : la valeur
|
|
78
|
+
* vient d'un jeton que n'importe qui peut forger.
|
|
79
|
+
*
|
|
80
|
+
* @param issuer - émetteur lu dans le jeton, non vérifié
|
|
81
|
+
*/
|
|
82
|
+
recognizes(issuer) {
|
|
83
|
+
if (this.#issuers.has(issuer)) return true;
|
|
84
|
+
try {
|
|
85
|
+
return this.#issuers.has(canonicalIssuer(issuer));
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Établit l'utilisateur applicatif à partir du sujet d'un jeton VÉRIFIÉ.
|
|
92
|
+
*
|
|
93
|
+
* Les deux politiques répondent à deux questions différentes : « qui, chez
|
|
94
|
+
* nous, est cette personne ? » et « quel pouvoir accorde-t-on à une machine
|
|
95
|
+
* que l'annuaire a authentifiée ? ».
|
|
96
|
+
*
|
|
97
|
+
* @param issuer - émetteur canonique, tel que le vérificateur l'a établi
|
|
98
|
+
* @param subject - sujet brut (`sub`) chez cet émetteur
|
|
99
|
+
* @param claims - revendications du jeton (rôles d'annuaire)
|
|
100
|
+
* @param area - zone qui SURCHARGE la politique, s'il y en a une
|
|
101
|
+
* @throws AuthenticationError (401) — sujet sans compte local utilisable
|
|
102
|
+
* @throws UnverifiableTokenError (503) — émetteur vérifié mais absent de la
|
|
103
|
+
* table de rattachement : configuration incohérente
|
|
104
|
+
*/
|
|
105
|
+
async resolve(issuer, subject, claims, area) {
|
|
106
|
+
const mapping = this.#issuers.get(issuer);
|
|
107
|
+
if (mapping === void 0) throw new UnverifiableTokenError(`émetteur « ${issuer} » vérifié mais absent de la table de rattachement des sujets — configuration incohérente.`);
|
|
108
|
+
const identifier = localIdentifierFor(issuer, subject, mapping);
|
|
109
|
+
if ((area?.subjectPolicy ?? this.#policy) === "ephemeral") return new BaseUser({
|
|
110
|
+
id: identifier,
|
|
111
|
+
identifier,
|
|
112
|
+
roles: [...area?.ephemeralRoles ?? this.#ephemeralRoles]
|
|
113
|
+
});
|
|
114
|
+
const provider = this.#resolveUserProvider();
|
|
115
|
+
let user = null;
|
|
116
|
+
const linked = this.#linkedProviders?.get(issuer);
|
|
117
|
+
if (linked !== void 0) try {
|
|
118
|
+
const roleMapping = this.#roleMappings?.get(linked);
|
|
119
|
+
user = roleMapping !== void 0 && canSyncRoles(provider) ? await provider.syncOAuthRoles(linked, subject, mapProviderRoles(roleMapping, claims)) : await provider.loadUserByOAuth(linked, subject);
|
|
120
|
+
} catch {}
|
|
121
|
+
if (user === null) try {
|
|
122
|
+
user = await provider.loadUserByIdentifier(identifier);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new AuthenticationError(INVALID_TOKEN);
|
|
125
|
+
}
|
|
126
|
+
if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
|
|
127
|
+
return user;
|
|
128
|
+
}
|
|
129
|
+
#resolveUserProvider() {
|
|
130
|
+
if (this.#userProvider === null) {
|
|
131
|
+
const provider = this.#container.get("users");
|
|
132
|
+
if (!provider) throw new Error("ExternalJwtAuthenticator: aucun service \"users\" (IUserProvider) dans le container — enregistrer un UserService au boot, ou passer la politique de sujet à \"ephemeral\" si l'appelant est une machine sans compte local.");
|
|
133
|
+
this.#userProvider = provider;
|
|
134
|
+
}
|
|
135
|
+
return this.#userProvider;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
/** Le fournisseur d'utilisateurs sait-il recalculer les rôles gérés par un annuaire ? */
|
|
139
|
+
function canSyncRoles(provider) {
|
|
140
|
+
return typeof provider.syncOAuthRoles === "function";
|
|
141
|
+
}
|
|
142
|
+
/** Tables de rôles par fournisseur de connexion — `null` quand aucun n'en déclare. */
|
|
143
|
+
function collectRoleMappings(providers) {
|
|
144
|
+
if (!providers) return null;
|
|
145
|
+
let mappings = null;
|
|
146
|
+
for (const { name, roleMapping } of providers) {
|
|
147
|
+
if (roleMapping == null) continue;
|
|
148
|
+
mappings ??= /* @__PURE__ */ new Map();
|
|
149
|
+
mappings.set(name, roleMapping);
|
|
150
|
+
}
|
|
151
|
+
return mappings;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Associe chaque émetteur de confiance au fournisseur de connexion qui partage
|
|
155
|
+
* son émetteur canonique.
|
|
156
|
+
*
|
|
157
|
+
* Un émetteur revendiqué par DEUX fournisseurs n'est lié à aucun : choisir l'un
|
|
158
|
+
* des deux ferait dépendre l'identité de l'ordre de la configuration.
|
|
159
|
+
*
|
|
160
|
+
* @returns la table, ou `null` quand aucun émetteur n'est lié (rien à chercher).
|
|
161
|
+
*/
|
|
162
|
+
function linkProviders(trusted, providers) {
|
|
163
|
+
if (!providers || providers.length === 0) return null;
|
|
164
|
+
const links = /* @__PURE__ */ new Map();
|
|
165
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const { name, issuer } of providers) {
|
|
167
|
+
let canonical;
|
|
168
|
+
try {
|
|
169
|
+
canonical = canonicalIssuer(issuer);
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!trusted.has(canonical)) continue;
|
|
174
|
+
if (links.has(canonical)) ambiguous.add(canonical);
|
|
175
|
+
links.set(canonical, name);
|
|
176
|
+
}
|
|
177
|
+
for (const issuer of ambiguous) links.delete(issuer);
|
|
178
|
+
return links.size > 0 ? links : null;
|
|
179
|
+
}
|
|
180
|
+
export { ExternalSubjectResolver, externalSubjectOptions };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
|
+
import { isBoundToAccount } from "../accountBinding.js";
|
|
2
3
|
import { UserRealtimeToken } from "../realtime/UserRealtimeToken.js";
|
|
3
4
|
import { Nodefony, RequestContext } from "nodefony";
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { authenticatedUser } from "@nodefony/user";
|
|
6
|
+
// nodefony/src/authenticator/FirewallRealtimeAuthenticator.ts
|
|
6
7
|
/**
|
|
7
8
|
* Authenticator realtime des identités résolues par le **firewall** — équivalent
|
|
8
9
|
* WS de tout ce que le pipeline HTTP sait authentifier.
|
|
@@ -57,16 +58,25 @@ var FirewallRealtimeAuthenticator = class {
|
|
|
57
58
|
*/
|
|
58
59
|
#resolveStore;
|
|
59
60
|
/**
|
|
61
|
+
* Accès paresseux à l'annuaire des comptes, pour confirmer avant chaque
|
|
62
|
+
* `api.request` que le compte de la socket existe toujours sous le même `id`.
|
|
63
|
+
* Absent (`null`) → aucune confirmation (seul `isValid` fait foi).
|
|
64
|
+
*/
|
|
65
|
+
#resolveUsers;
|
|
66
|
+
/**
|
|
60
67
|
* @param resolveStore - fournit le store de révocation des jetons (le firewall
|
|
61
68
|
* passe une closure sur son container). Omis → mode dégradé documenté :
|
|
62
69
|
* seules les bornes portées par le jeton lui-même sont vérifiables.
|
|
70
|
+
* @param resolveUsers - fournit l'annuaire des comptes (`users`). Omis → pas
|
|
71
|
+
* de confirmation du compte avant `api.request`.
|
|
63
72
|
*/
|
|
64
|
-
constructor(resolveStore = null) {
|
|
73
|
+
constructor(resolveStore = null, resolveUsers = null) {
|
|
65
74
|
this.#resolveStore = resolveStore;
|
|
75
|
+
this.#resolveUsers = resolveUsers;
|
|
66
76
|
}
|
|
67
77
|
/** Une identité authentifiée a-t-elle été résolue (par le firewall) au handshake ? */
|
|
68
78
|
supports(_handshake) {
|
|
69
|
-
return
|
|
79
|
+
return authenticatedUser() !== null;
|
|
70
80
|
}
|
|
71
81
|
/**
|
|
72
82
|
* Promeut l'identité déjà résolue (ALS) en jeton realtime — 0 lecture base.
|
|
@@ -76,13 +86,61 @@ var FirewallRealtimeAuthenticator = class {
|
|
|
76
86
|
* filet défensif fail-closed → le hub ferme la socket en 4001).
|
|
77
87
|
*/
|
|
78
88
|
async authenticate(_handshake) {
|
|
79
|
-
const user =
|
|
80
|
-
if (
|
|
89
|
+
const user = authenticatedUser();
|
|
90
|
+
if (user === null) throw new AuthenticationError("Invalid realtime session");
|
|
81
91
|
const issued = readFirewallToken();
|
|
82
|
-
const type =
|
|
83
|
-
const revalidate =
|
|
92
|
+
const type = issuedType(issued);
|
|
93
|
+
const revalidate = this.#revalidatorFor(user, issued, type);
|
|
84
94
|
const scopes = issued ? issued.getScopes() : void 0;
|
|
85
|
-
return new UserRealtimeToken(user, revalidate, type, scopes);
|
|
95
|
+
return new UserRealtimeToken(user, revalidate, type, scopes, this.#accountConfirmationFor(user, type));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Le revalidateur de l'identité que le firewall a posée sur la requête
|
|
99
|
+
* COURANTE (ALS), pour toute connexion longue — socket WebSocket ou flux SSE.
|
|
100
|
+
*
|
|
101
|
+
* Une seule règle décide qu'une identité est morte, quel que soit le
|
|
102
|
+
* transport qui la porte : c'est celle-ci, et le hub comme le noyau HTTP
|
|
103
|
+
* l'appellent au lieu de la recopier.
|
|
104
|
+
*
|
|
105
|
+
* @returns `null` pour un anonyme (rien à révoquer), sinon la fonction qui
|
|
106
|
+
* dit si l'identité est TOUJOURS valable — fail-closed : `false` quand elle
|
|
107
|
+
* ne peut pas le prouver.
|
|
108
|
+
*/
|
|
109
|
+
currentRevalidator() {
|
|
110
|
+
const user = authenticatedUser();
|
|
111
|
+
if (user === null) return null;
|
|
112
|
+
const issued = readFirewallToken();
|
|
113
|
+
return this.#revalidatorFor(user, issued, issuedType(issued));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Choisit la preuve de révocation selon le mode d'authentification : la
|
|
117
|
+
* session se relit, un jeton porteur se borne par `exp` et la denylist.
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* Confirmation du compte, pour les modes dont l'APPLICATION émet l'artefact
|
|
121
|
+
* (session, JWT maison, clé d'API) : le compte relu doit exister, être actif
|
|
122
|
+
* et porter le même `id` immuable qu'au handshake — sinon il a disparu ou un
|
|
123
|
+
* autre compte a repris son identifiant hors du framework (#139). Un sujet
|
|
124
|
+
* `external-jwt` peut être éphémère, sans compte local : il n'est pas
|
|
125
|
+
* confirmé. `null` = rien à confirmer.
|
|
126
|
+
*/
|
|
127
|
+
#accountConfirmationFor(user, type) {
|
|
128
|
+
const resolveUsers = this.#resolveUsers;
|
|
129
|
+
if (resolveUsers === null || !ACCOUNT_CONFIRMED_TYPES.has(type)) return null;
|
|
130
|
+
return async () => {
|
|
131
|
+
const users = resolveUsers();
|
|
132
|
+
if (users === null) return true;
|
|
133
|
+
let current;
|
|
134
|
+
try {
|
|
135
|
+
current = await users.loadUserByIdentifier(user.identifier);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return current.isActive() && !current.isLocked() && isBoundToAccount(current, user.id, user.credentialsVersion);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
#revalidatorFor(user, issued, type) {
|
|
143
|
+
return type === "session" ? buildSessionRevalidator(user.identifier) : this.#buildBearerRevalidator(issued, user.identifier, type);
|
|
86
144
|
}
|
|
87
145
|
/**
|
|
88
146
|
* Re-validation d'une identité portée par un **jeton** (JWT, clé API, OAuth) :
|
|
@@ -125,10 +183,23 @@ var FirewallRealtimeAuthenticator = class {
|
|
|
125
183
|
}
|
|
126
184
|
};
|
|
127
185
|
const EMPTY_CLAIMS = Object.freeze({});
|
|
186
|
+
/** Modes dont l'application émet l'artefact : leur compte est local, donc relisible. */
|
|
187
|
+
const ACCOUNT_CONFIRMED_TYPES = /* @__PURE__ */ new Set([
|
|
188
|
+
"session",
|
|
189
|
+
"jwt",
|
|
190
|
+
"apikey"
|
|
191
|
+
]);
|
|
128
192
|
/** Claim temporel JWT (secondes, RFC 7519) → epoch ms, ou `null` si absent/invalide. */
|
|
129
193
|
function secondsToMs(value) {
|
|
130
194
|
return typeof value === "number" && Number.isFinite(value) ? value * 1e3 : null;
|
|
131
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Le mode d'authentification du jeton du firewall. Absent (zone historique) →
|
|
198
|
+
* le mode le plus strict, la session : un repli sûr, jamais permissif.
|
|
199
|
+
*/
|
|
200
|
+
function issuedType(issued) {
|
|
201
|
+
return typeof issued?.type === "string" && issued.type ? issued.type : "session";
|
|
202
|
+
}
|
|
132
203
|
/** Le jeton posé dans l'ALS par `firewall.handleSecurity`, s'il y en a un. */
|
|
133
204
|
function readFirewallToken() {
|
|
134
205
|
return RequestContext.get()?.token;
|
|
@@ -161,14 +232,4 @@ function buildSessionRevalidator(identifier) {
|
|
|
161
232
|
}
|
|
162
233
|
};
|
|
163
234
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Vrai si `user` est un utilisateur AUTHENTIFIÉ (≠ anonyme). Le firewall pose
|
|
166
|
-
* `token.getUser()` dans l'ALS : soit l'`IUser` réel, soit `anonymousUser`
|
|
167
|
-
* (singleton). Tout ce qui n'est pas l'anonyme et porte un `identifier` est
|
|
168
|
-
* une identité valide.
|
|
169
|
-
*/
|
|
170
|
-
function isAuthenticatedUser(user) {
|
|
171
|
-
return !!user && user !== anonymousUser && typeof user.identifier === "string";
|
|
172
|
-
}
|
|
173
|
-
//#endregion
|
|
174
235
|
export { FirewallRealtimeAuthenticator, FirewallRealtimeAuthenticator as default };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
|
+
import { loadBoundAccount } from "../accountBinding.js";
|
|
2
3
|
import { UserToken } from "../token/UserToken.js";
|
|
3
4
|
import { bearerToken } from "./bearer.js";
|
|
4
5
|
import peekIssuer from "./peekIssuer.js";
|
|
5
|
-
|
|
6
|
+
import { canonicalIssuer } from "nodefony";
|
|
7
|
+
// nodefony/src/authenticator/JwtAuthenticator.ts
|
|
6
8
|
const COMPACT_JWS = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
|
|
7
9
|
const INVALID_TOKEN = "Invalid token";
|
|
8
10
|
/**
|
|
@@ -82,6 +84,57 @@ var JwtAuthenticator = class {
|
|
|
82
84
|
async authenticate(token) {
|
|
83
85
|
const raw = token.getCredentials();
|
|
84
86
|
if (typeof raw !== "string" || raw.length === 0) throw new AuthenticationError(INVALID_TOKEN);
|
|
87
|
+
const { user, payload } = await this.#verify(raw, this.#runtime.audiences);
|
|
88
|
+
return this.#promote(token, user, payload);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Vérifie un jeton émis par CETTE application pour une ressource protégée
|
|
92
|
+
* nommée par l'appelant — la porte d'une ressource (RFC 8707), pas une zone.
|
|
93
|
+
*
|
|
94
|
+
* Les MÊMES règles que {@link authenticate}, appliquées par la même fonction :
|
|
95
|
+
* signature par le keyset local, `iss`, `typ: at+jwt`, révocation ciblée
|
|
96
|
+
* (`jti`) et en masse (`invalidBefore`), compte encore actif. Seule
|
|
97
|
+
* l'audience diffère : c'est celle de la ressource qui appelle, et elle est
|
|
98
|
+
* exigée. Un jeton révoqué, ou celui d'un compte désactivé, n'ouvre donc pas
|
|
99
|
+
* plus une porte MCP qu'une route — ce que la vérification par le seul jeu de
|
|
100
|
+
* clés publiques laissait passer jusqu'à l'expiration.
|
|
101
|
+
*
|
|
102
|
+
* @param raw - le jeton présenté, brut
|
|
103
|
+
* @param audience - l'URI canonique de la ressource qui le reçoit
|
|
104
|
+
* @returns le principal établi, ou `null` pour tout refus (contrat
|
|
105
|
+
* `IAccessTokenVerifier` : un refus n'est jamais une exception)
|
|
106
|
+
* @throws Error si le câblage manque (keystore, store, annuaire) — la porte
|
|
107
|
+
* le traduit en `503`, jamais en jeton accepté
|
|
108
|
+
*/
|
|
109
|
+
async verifyForResource(raw, audience) {
|
|
110
|
+
let verified;
|
|
111
|
+
try {
|
|
112
|
+
verified = await this.#verify(raw, audience);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof AuthenticationError) return null;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const { payload } = verified;
|
|
118
|
+
const scope = payload.scope;
|
|
119
|
+
return {
|
|
120
|
+
issuer: this.#resourceIssuer(),
|
|
121
|
+
subject: payload.sub,
|
|
122
|
+
scopes: typeof scope === "string" && scope.length > 0 ? scope.split(" ") : [],
|
|
123
|
+
expiresAt: payload.exp,
|
|
124
|
+
issuedAt: payload.iat,
|
|
125
|
+
tokenId: payload.jti,
|
|
126
|
+
claims: payload
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* La vérification entière d'un jeton maison — UNE implémentation, pour la
|
|
131
|
+
* zone ({@link authenticate}) comme pour la ressource
|
|
132
|
+
* ({@link verifyForResource}).
|
|
133
|
+
*
|
|
134
|
+
* @throws AuthenticationError pour tout refus (message uniforme)
|
|
135
|
+
* @throws Error si le câblage manque
|
|
136
|
+
*/
|
|
137
|
+
async #verify(raw, audience) {
|
|
85
138
|
const jose = this.#jose ??= await import("jose");
|
|
86
139
|
const getKey = await this.#ensureGetKey(jose);
|
|
87
140
|
let payload;
|
|
@@ -89,7 +142,7 @@ var JwtAuthenticator = class {
|
|
|
89
142
|
payload = (await jose.jwtVerify(raw, getKey, {
|
|
90
143
|
algorithms: ["EdDSA"],
|
|
91
144
|
issuer: this.#runtime.issuer,
|
|
92
|
-
audience
|
|
145
|
+
audience,
|
|
93
146
|
typ: "at+jwt"
|
|
94
147
|
})).payload;
|
|
95
148
|
} catch {
|
|
@@ -104,8 +157,31 @@ var JwtAuthenticator = class {
|
|
|
104
157
|
if (invalidBefore !== null) {
|
|
105
158
|
if ((typeof payload.iat === "number" ? payload.iat * 1e3 : 0) < invalidBefore) throw new AuthenticationError(INVALID_TOKEN);
|
|
106
159
|
}
|
|
107
|
-
const user = await this.#
|
|
108
|
-
|
|
160
|
+
const user = await loadBoundAccount(this.#resolveUserProvider(), sub, payload["uid"], payload["cv"]);
|
|
161
|
+
if (user === null) throw new AuthenticationError(INVALID_TOKEN);
|
|
162
|
+
return {
|
|
163
|
+
user,
|
|
164
|
+
payload
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Recharge le compte que désigne un principal établi par
|
|
169
|
+
* {@link verifyForResource} — pour une ressource protégée qui fait agir un
|
|
170
|
+
* outil au nom du propriétaire du jeton.
|
|
171
|
+
*
|
|
172
|
+
* La MÊME règle que la zone ({@link loadBoundAccount}), rejouée sur les
|
|
173
|
+
* revendications du principal : le vérificateur ne garde pas le compte qu'il
|
|
174
|
+
* a lu, et une seconde lecture de l'annuaire est le coût accepté d'un outil
|
|
175
|
+
* qui la demande — les autres ne la paient pas.
|
|
176
|
+
*
|
|
177
|
+
* @param principal - principal d'un jeton émis par CETTE application
|
|
178
|
+
* @returns le compte, ou `null` s'il n'est pas (ou plus) celui du jeton
|
|
179
|
+
* @throws Error si l'annuaire des comptes n'est pas câblé
|
|
180
|
+
*/
|
|
181
|
+
resolveAccount(principal) {
|
|
182
|
+
const subject = principal.subject;
|
|
183
|
+
if (principal.issuer !== this.#resourceIssuer() || typeof subject !== "string" || subject.length === 0) return Promise.resolve(null);
|
|
184
|
+
return loadBoundAccount(this.#resolveUserProvider(), subject, principal.claims?.["uid"], principal.claims?.["cv"]);
|
|
109
185
|
}
|
|
110
186
|
/** Slot audit (J4b). */
|
|
111
187
|
onSuccess(_context, _token) {
|
|
@@ -119,6 +195,17 @@ var JwtAuthenticator = class {
|
|
|
119
195
|
challenge() {
|
|
120
196
|
return "Bearer";
|
|
121
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* L'émetteur que portent les principaux de {@link verifyForResource} — et
|
|
200
|
+
* donc celui qu'un principal doit porter pour être un jeton maison.
|
|
201
|
+
*/
|
|
202
|
+
#resourceIssuer() {
|
|
203
|
+
try {
|
|
204
|
+
return canonicalIssuer(this.#runtime.issuer);
|
|
205
|
+
} catch {
|
|
206
|
+
return this.#runtime.issuer;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
122
209
|
/** Construit (une fois) le résolveur de clé par `kid` depuis le JWKS public. */
|
|
123
210
|
async #ensureGetKey(jose) {
|
|
124
211
|
if (this.#getKey !== null) return this.#getKey;
|
|
@@ -134,19 +221,9 @@ var JwtAuthenticator = class {
|
|
|
134
221
|
ut.setAttribute("scopes", scopes);
|
|
135
222
|
ut.setAttribute("jti", payload.jti);
|
|
136
223
|
ut.setAttribute("claims", payload);
|
|
224
|
+
ut.setAttribute("issuer", payload.iss);
|
|
137
225
|
return ut;
|
|
138
226
|
}
|
|
139
|
-
async #resolveUserOrReject(sub) {
|
|
140
|
-
const provider = this.#resolveUserProvider();
|
|
141
|
-
let user;
|
|
142
|
-
try {
|
|
143
|
-
user = await provider.loadUserByIdentifier(sub);
|
|
144
|
-
} catch {
|
|
145
|
-
throw new AuthenticationError(INVALID_TOKEN);
|
|
146
|
-
}
|
|
147
|
-
if (!user.isActive() || user.isLocked()) throw new AuthenticationError(INVALID_TOKEN);
|
|
148
|
-
return user;
|
|
149
|
-
}
|
|
150
227
|
#resolveKeystore() {
|
|
151
228
|
if (this.#keystore === null) {
|
|
152
229
|
const ks = this.#container.get("jwtKeystore");
|
|
@@ -172,5 +249,4 @@ var JwtAuthenticator = class {
|
|
|
172
249
|
return this.#userProvider;
|
|
173
250
|
}
|
|
174
251
|
};
|
|
175
|
-
//#endregion
|
|
176
252
|
export { JwtAuthenticator, JwtAuthenticator as default };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AuthenticationError } from "../../errors/AuthenticationError.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY } from "../accountBinding.js";
|
|
3
|
+
import { resolveBoundSessionIdentity } from "../sessionIdentity.js";
|
|
3
4
|
import { UserToken } from "../token/UserToken.js";
|
|
4
|
-
|
|
5
|
+
// nodefony/src/authenticator/SessionAuthenticator.ts
|
|
5
6
|
/**
|
|
6
7
|
* Authentification par **session serveur** (cookie opaque, modèle BFF) — la
|
|
7
8
|
* preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
|
|
@@ -59,7 +60,11 @@ var SessionAuthenticator = class {
|
|
|
59
60
|
}
|
|
60
61
|
/** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
|
|
61
62
|
createToken(context) {
|
|
62
|
-
const credentials = {
|
|
63
|
+
const credentials = {
|
|
64
|
+
identifier: context.session?.user ?? "",
|
|
65
|
+
accountId: context.session?.getMetaBag(ACCOUNT_ID_KEY),
|
|
66
|
+
credentialsVersion: context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)
|
|
67
|
+
};
|
|
63
68
|
return Promise.resolve(new UserToken(this.name, credentials));
|
|
64
69
|
}
|
|
65
70
|
/**
|
|
@@ -74,7 +79,7 @@ var SessionAuthenticator = class {
|
|
|
74
79
|
const credentials = token.getCredentials();
|
|
75
80
|
if (!credentials?.identifier) throw new AuthenticationError("Invalid session");
|
|
76
81
|
const provider = this.#provider ??= this.#resolveProvider();
|
|
77
|
-
const user = await
|
|
82
|
+
const user = await resolveBoundSessionIdentity(provider, credentials.identifier, credentials.accountId, credentials.credentialsVersion);
|
|
78
83
|
return token.promote(user);
|
|
79
84
|
}
|
|
80
85
|
/**
|
|
@@ -90,5 +95,4 @@ var SessionAuthenticator = class {
|
|
|
90
95
|
return Promise.resolve();
|
|
91
96
|
}
|
|
92
97
|
};
|
|
93
|
-
//#endregion
|
|
94
98
|
export { SessionAuthenticator, SessionAuthenticator as default };
|