@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,8 +1,8 @@
|
|
|
1
1
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
2
2
|
import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
|
|
3
|
-
import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore,
|
|
3
|
+
import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
|
|
5
|
+
// nodefony/service/auditService.ts
|
|
6
6
|
const serviceName = "auditService";
|
|
7
7
|
const GC_INTERVAL_MS = 36e5;
|
|
8
8
|
/**
|
|
@@ -50,7 +50,7 @@ var AuditService = class extends Service {
|
|
|
50
50
|
let storeName = config.audit.store;
|
|
51
51
|
let reason = `store explicitement configuré ("${storeName}")`;
|
|
52
52
|
if (storeName === AUTO_STORE) {
|
|
53
|
-
const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory",
|
|
53
|
+
const auto = resolveAutoStore("durable", this.kernel?.infra ?? EMPTY_INFRA, listAuditStores(), "memory", storeConnectivity(this.kernel));
|
|
54
54
|
storeName = auto.store;
|
|
55
55
|
reason = auto.reason;
|
|
56
56
|
this.log(`audit.store "auto" → "${storeName}" (${auto.reason})`, "INFO");
|
|
@@ -62,7 +62,7 @@ var AuditService = class extends Service {
|
|
|
62
62
|
this.log(`${msg} — audit désactivé (journal de sécurité non collecté)`, "CRITIC");
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
|
-
if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. " + durableStoreRemedy(
|
|
65
|
+
if (storeName === "memory" && this.kernel?.environment === "production") this.log("audit.store \"memory\" en PRODUCTION — journal de sécurité volatil et per-pod : perdu au redémarrage, invisible des autres pods, rétention de conformité impossible. " + durableStoreRemedy(storeConnectivity(this.kernel)), "WARNING");
|
|
66
66
|
this.#enabled = true;
|
|
67
67
|
this.#idPrefix = randomBytes(4).toString("hex");
|
|
68
68
|
this.#store = factory({
|
|
@@ -142,5 +142,4 @@ var AuditService = class extends Service {
|
|
|
142
142
|
return this.#enabled;
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
|
-
//#endregion
|
|
146
145
|
export { AuditService as default };
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { securityConfigSchema } from "../config/config.js";
|
|
2
|
+
import { InsecureTransportError } from "../errors/InsecureTransportError.js";
|
|
1
3
|
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
2
4
|
import { ThrottledError } from "../errors/ThrottledError.js";
|
|
3
|
-
import {
|
|
5
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
|
|
6
|
+
import { resolveBoundSessionIdentity, resolveSessionIdentity } from "../src/sessionIdentity.js";
|
|
4
7
|
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
5
8
|
import { readAuditContext } from "../src/audit/readAuditContext.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
9
|
+
import { amrForFactor } from "../src/sessionAuthentication.js";
|
|
10
|
+
import { LOGIN_PAGE_SKIN_SPECS, MFA_METHOD_TOTP, SESSION_AMR_KEY, SESSION_AUTH_AT_KEY, Service } from "nodefony";
|
|
11
|
+
import { requestUser } from "@nodefony/user";
|
|
12
|
+
// nodefony/service/authFlow.ts
|
|
13
|
+
const MFA_METHODS = Object.freeze([MFA_METHOD_TOTP]);
|
|
8
14
|
const serviceName = "authFlow";
|
|
9
15
|
const INVALID_CREDENTIALS = "Invalid credentials";
|
|
10
16
|
const PENDING_MFA_KEY = "mfa:pending";
|
|
@@ -34,8 +40,11 @@ var AuthFlow = class extends Service {
|
|
|
34
40
|
#sessions = null;
|
|
35
41
|
#throttler = null;
|
|
36
42
|
#throttlerResolved = false;
|
|
43
|
+
#transport = null;
|
|
37
44
|
#totp = null;
|
|
38
45
|
#totpResolved = false;
|
|
46
|
+
#loginPage = null;
|
|
47
|
+
#loginPageResolved = false;
|
|
39
48
|
constructor(module) {
|
|
40
49
|
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
41
50
|
this.module = module;
|
|
@@ -51,6 +60,8 @@ var AuthFlow = class extends Service {
|
|
|
51
60
|
* @param identifier - identifiant saisi (body JSON, non typé à la frontière).
|
|
52
61
|
* @param password - mot de passe saisi.
|
|
53
62
|
* @returns `authenticated` (identité établie) ou `mfa_required` (2ᵉ facteur requis).
|
|
63
|
+
* @throws InsecureTransportError (403) — mot de passe reçu en clair en
|
|
64
|
+
* production (refusé AVANT le throttle et le verifier).
|
|
54
65
|
* @throws ThrottledError (429 + `Retry-After`) — backoff actif.
|
|
55
66
|
* @throws AuthenticationError (401, message uniforme) — credential absent ou
|
|
56
67
|
* invalide.
|
|
@@ -58,6 +69,7 @@ var AuthFlow = class extends Service {
|
|
|
58
69
|
async login(context, identifier, password) {
|
|
59
70
|
const info = readAuditContext(context);
|
|
60
71
|
const who = typeof identifier === "string" ? identifier : null;
|
|
72
|
+
this.#assertTransport(context, "login", who, info);
|
|
61
73
|
if (typeof identifier !== "string" || identifier.length === 0 || typeof password !== "string" || password.length === 0) {
|
|
62
74
|
recordAudit(this.container, {
|
|
63
75
|
category: "auth",
|
|
@@ -113,12 +125,10 @@ var AuthFlow = class extends Service {
|
|
|
113
125
|
});
|
|
114
126
|
return {
|
|
115
127
|
status: "mfa_required",
|
|
116
|
-
methods:
|
|
128
|
+
methods: MFA_METHODS
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
|
-
await this.#openSession(context, user
|
|
120
|
-
context.user = user.identifier;
|
|
121
|
-
RequestContext.set("user", user);
|
|
131
|
+
await this.#openSession(context, user, "password");
|
|
122
132
|
recordAudit(this.container, {
|
|
123
133
|
category: "auth",
|
|
124
134
|
action: "login.success",
|
|
@@ -148,9 +158,7 @@ var AuthFlow = class extends Service {
|
|
|
148
158
|
async establishSessionFor(context, identifier, reason = "federated") {
|
|
149
159
|
if (typeof identifier !== "string" || identifier.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
|
|
150
160
|
const user = await resolveSessionIdentity(this.#resolveUsers(), identifier);
|
|
151
|
-
await this.#openSession(context, user
|
|
152
|
-
context.user = user.identifier;
|
|
153
|
-
RequestContext.set("user", user);
|
|
161
|
+
await this.#openSession(context, user, reason);
|
|
154
162
|
recordAudit(this.container, {
|
|
155
163
|
category: "auth",
|
|
156
164
|
action: "login.success",
|
|
@@ -171,6 +179,7 @@ var AuthFlow = class extends Service {
|
|
|
171
179
|
* @param context - contexte HTTP (porte la session PENDING).
|
|
172
180
|
* @param code - code présenté (TOTP ou code de récupération).
|
|
173
181
|
* @returns la projection publique de l'utilisateur authentifié.
|
|
182
|
+
* @throws InsecureTransportError (403) — code reçu en clair en production.
|
|
174
183
|
* @throws ThrottledError (429) — trop de tentatives.
|
|
175
184
|
* @throws AuthenticationError (401, uniforme) — aucun défi en cours, code absent
|
|
176
185
|
* ou invalide (la session N'est PAS ouverte ; le défi reste pour un retry).
|
|
@@ -178,6 +187,7 @@ var AuthFlow = class extends Service {
|
|
|
178
187
|
async completeMfaLogin(context, code) {
|
|
179
188
|
const info = readAuditContext(context);
|
|
180
189
|
const pending = context.session?.get(PENDING_MFA_KEY);
|
|
190
|
+
this.#assertTransport(context, "login/totp", typeof pending === "string" && pending.length > 0 ? pending : null, info);
|
|
181
191
|
if (typeof pending !== "string" || pending.length === 0) throw new AuthenticationError(INVALID_CREDENTIALS);
|
|
182
192
|
const throttler = this.#resolveThrottler();
|
|
183
193
|
if (throttler !== null) {
|
|
@@ -253,23 +263,95 @@ var AuthFlow = class extends Service {
|
|
|
253
263
|
return true;
|
|
254
264
|
}
|
|
255
265
|
/**
|
|
266
|
+
* Déconnexion COMPLÈTE : détruit la session locale et rend, si la session
|
|
267
|
+
* vient d'un fournisseur qui publie un point de déconnexion (Keycloak…),
|
|
268
|
+
* l'adresse où envoyer le navigateur pour fermer AUSSI sa session à lui —
|
|
269
|
+
* sans quoi le clic suivant sur « Continuer avec … » reconnecte sans mot de
|
|
270
|
+
* passe (OpenID Connect RP-Initiated Logout 1.0).
|
|
271
|
+
*
|
|
272
|
+
* @returns `destroyed` comme {@link logout} ; `logoutUrl` à suivre, ou `null`.
|
|
273
|
+
*/
|
|
274
|
+
async logoutWithProvider(context) {
|
|
275
|
+
const session = context.session;
|
|
276
|
+
const logoutUrl = session?.status === "active" ? await this.get("oauth2")?.logoutUrlFor(session) ?? null : null;
|
|
277
|
+
return {
|
|
278
|
+
destroyed: await this.logout(context),
|
|
279
|
+
logoutUrl
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
256
283
|
* Identité portée par la session courante, re-résolue auprès du provider
|
|
257
284
|
* (mêmes contrôles que le `SessionAuthenticator` — source unique).
|
|
258
285
|
*
|
|
259
286
|
* @returns la projection publique, ou `null` (pas de session, session
|
|
260
|
-
* orpheline, compte verrouillé/désactivé) — le handler répond
|
|
287
|
+
* orpheline, compte verrouillé/désactivé) — le handler répond `200`
|
|
288
|
+
* `{ user: null }`.
|
|
261
289
|
*/
|
|
262
290
|
async me(context) {
|
|
263
291
|
const identifier = context.session?.user;
|
|
264
292
|
if (typeof identifier !== "string" || identifier.length === 0) return null;
|
|
293
|
+
const current = requestUser(identifier);
|
|
294
|
+
if (current !== null) return toSafeUser(current);
|
|
265
295
|
try {
|
|
266
|
-
return toSafeUser(await
|
|
296
|
+
return toSafeUser(await resolveBoundSessionIdentity(this.#resolveUsers(), identifier, context.session?.getMetaBag(ACCOUNT_ID_KEY), context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)));
|
|
267
297
|
} catch (error) {
|
|
268
298
|
if (error instanceof AuthenticationError) return null;
|
|
269
299
|
throw error;
|
|
270
300
|
}
|
|
271
301
|
}
|
|
272
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Décrit la page de connexion par défaut : ce qu'elle propose et comment
|
|
304
|
+
* elle s'habille, depuis `security.loginPage` et les fournisseurs
|
|
305
|
+
* opérationnels.
|
|
306
|
+
*
|
|
307
|
+
* Lue par le contrôleur de `@nodefony/framework`, qui n'importe pas ce
|
|
308
|
+
* paquet. Les fournisseurs sont relus à chaque appel : l'un d'eux peut
|
|
309
|
+
* tomber ou revenir pendant que le serveur tourne.
|
|
310
|
+
*
|
|
311
|
+
* @returns la description, ou `null` quand la page par défaut est
|
|
312
|
+
* désactivée (`loginPage.enabled: false`) ou la configuration invalide —
|
|
313
|
+
* le contrôleur répond alors 404.
|
|
314
|
+
*/
|
|
315
|
+
describeLoginPage() {
|
|
316
|
+
const page = this.#resolveLoginPage();
|
|
317
|
+
if (page === null || !page.enabled) return null;
|
|
318
|
+
const offered = this.get("oauth2")?.listDisplayProviders() ?? [];
|
|
319
|
+
return {
|
|
320
|
+
path: page.path,
|
|
321
|
+
title: page.title ?? null,
|
|
322
|
+
logo: page.logo ?? null,
|
|
323
|
+
template: page.template ?? null,
|
|
324
|
+
heading: page.heading ?? null,
|
|
325
|
+
subtitle: page.subtitle ?? null,
|
|
326
|
+
stylesheet: page.stylesheet ?? null,
|
|
327
|
+
providersFirst: page.providersFirst,
|
|
328
|
+
hero: page.hero === void 0 ? null : page.hero === false ? false : {
|
|
329
|
+
heading: page.hero.heading,
|
|
330
|
+
text: page.hero.text ?? null
|
|
331
|
+
},
|
|
332
|
+
footer: page.footer,
|
|
333
|
+
skin: page.skin,
|
|
334
|
+
layout: page.layout ?? LOGIN_PAGE_SKIN_SPECS[page.skin].layout,
|
|
335
|
+
previewPicker: page.previewPicker,
|
|
336
|
+
previewHook: page.previewHook ?? null,
|
|
337
|
+
password: page.password,
|
|
338
|
+
passkey: this.get("webauthn")?.isEnabled() ?? false,
|
|
339
|
+
providers: offered.map(({ name, label, icon }) => ({
|
|
340
|
+
name,
|
|
341
|
+
label,
|
|
342
|
+
icon: icon ?? null
|
|
343
|
+
}))
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
#resolveLoginPage() {
|
|
347
|
+
if (!this.#loginPageResolved) {
|
|
348
|
+
this.#loginPageResolved = true;
|
|
349
|
+
const parsed = securityConfigSchema.safeParse(this.options);
|
|
350
|
+
this.#loginPage = parsed.success ? parsed.data.loginPage : null;
|
|
351
|
+
}
|
|
352
|
+
return this.#loginPage;
|
|
353
|
+
}
|
|
354
|
+
async #openSession(context, user, factor) {
|
|
273
355
|
const session = context.session ?? await this.#resolveSessions().start(context);
|
|
274
356
|
if (session === null) {
|
|
275
357
|
this.log("login: session service returned no session", "ERROR");
|
|
@@ -287,7 +369,11 @@ var AuthFlow = class extends Service {
|
|
|
287
369
|
const ua = provenance.getUserAgent?.();
|
|
288
370
|
if (ua) session.setMetaBag("ua", ua);
|
|
289
371
|
} catch {}
|
|
290
|
-
|
|
372
|
+
session.setMetaBag(SESSION_AUTH_AT_KEY, Date.now());
|
|
373
|
+
session.setMetaBag(SESSION_AMR_KEY, amrForFactor(factor));
|
|
374
|
+
session.setMetaBag(ACCOUNT_ID_KEY, user.id);
|
|
375
|
+
session.setMetaBag(CREDENTIALS_VERSION_KEY, credentialsVersionOf(user));
|
|
376
|
+
await session.save(user.identifier);
|
|
291
377
|
}
|
|
292
378
|
#resolveSessions() {
|
|
293
379
|
if (this.#sessions === null) {
|
|
@@ -305,6 +391,23 @@ var AuthFlow = class extends Service {
|
|
|
305
391
|
}
|
|
306
392
|
return this.#users;
|
|
307
393
|
}
|
|
394
|
+
#assertTransport(context, door, actor, info) {
|
|
395
|
+
this.#transport ??= this.get("credentialTransport") ?? null;
|
|
396
|
+
if (this.#transport === null) return;
|
|
397
|
+
try {
|
|
398
|
+
this.#transport.assert(context, door);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof InsecureTransportError) recordAudit(this.container, {
|
|
401
|
+
category: "auth",
|
|
402
|
+
action: "login.failure",
|
|
403
|
+
outcome: "failure",
|
|
404
|
+
actor,
|
|
405
|
+
reason: "insecure_transport",
|
|
406
|
+
...info
|
|
407
|
+
});
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
308
411
|
#resolveThrottler() {
|
|
309
412
|
if (!this.#throttlerResolved) {
|
|
310
413
|
this.#throttler = this.get("loginThrottler") ?? null;
|
|
@@ -324,9 +427,8 @@ var AuthFlow = class extends Service {
|
|
|
324
427
|
function toSafeUser(user) {
|
|
325
428
|
return {
|
|
326
429
|
id: user.id,
|
|
327
|
-
|
|
430
|
+
identifier: user.identifier,
|
|
328
431
|
roles: [...user.roles]
|
|
329
432
|
};
|
|
330
433
|
}
|
|
331
|
-
//#endregion
|
|
332
434
|
export { AuthFlow, AuthFlow as default };
|
|
@@ -2,7 +2,7 @@ import { recordAudit } from "../src/audit/recordAudit.js";
|
|
|
2
2
|
import "../contracts/IAccessVoter.js";
|
|
3
3
|
import { listVoterFactories } from "../src/voter/voterRegistry.js";
|
|
4
4
|
import { Service, logColor } from "nodefony";
|
|
5
|
-
|
|
5
|
+
// nodefony/service/authorization.ts
|
|
6
6
|
const serviceName = "authorization";
|
|
7
7
|
/**
|
|
8
8
|
* Service d'autorisation Nodefony (niveau C, P6 J6) — décide d'un accès via un
|
|
@@ -90,5 +90,4 @@ function describeSubject(subject) {
|
|
|
90
90
|
if (subject === null || subject === void 0) return String(subject);
|
|
91
91
|
return subject.constructor?.name ?? typeof subject;
|
|
92
92
|
}
|
|
93
|
-
//#endregion
|
|
94
93
|
export { Authorization, Authorization as default };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/service/cors.ts
|
|
2
2
|
/**
|
|
3
3
|
* Politique CORS de Nodefony — Same-Origin Policy assouplie côté serveur
|
|
4
4
|
* (Fetch Standard / W3C CORS protocol). Logique PURE et synchrone : décide les
|
|
@@ -77,5 +77,4 @@ var Cors = class {
|
|
|
77
77
|
return h;
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
-
//#endregion
|
|
81
80
|
export { Cors, Cors as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CsrfError } from "../errors/CsrfError.js";
|
|
2
|
-
|
|
2
|
+
// nodefony/service/csrf.ts
|
|
3
3
|
/**
|
|
4
4
|
* Méthodes HTTP « sûres » (RFC 9110 §9.2.1) — sans effet de bord par contrat,
|
|
5
5
|
* donc hors du vecteur CSRF. Tout le reste (POST/PUT/PATCH/DELETE…) est traité
|
|
@@ -93,5 +93,4 @@ var Csrf = class {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
//#endregion
|
|
97
96
|
export { Csrf, Csrf as default };
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { compileProviderRoleMapping, isPlatformRole } from "../src/oauth/providerRoles.js";
|
|
1
2
|
import { SecuredArea } from "../src/SecuredArea.js";
|
|
3
|
+
import { entryPointLocation } from "../src/entryPoint.js";
|
|
2
4
|
import { LoginThrottler } from "../src/throttle/LoginThrottler.js";
|
|
5
|
+
import { InsecureTransportError } from "../errors/InsecureTransportError.js";
|
|
6
|
+
import { CredentialTransportPolicy, shouldEnforceCredentialTransport } from "../src/transport/CredentialTransportPolicy.js";
|
|
3
7
|
import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
|
|
8
|
+
import { rolesWithin } from "../src/rolesWithin.js";
|
|
4
9
|
import { mergeCspFragments } from "../src/csp.js";
|
|
5
10
|
import { CsrfTokenManager } from "../src/csrfToken.js";
|
|
6
11
|
import { CsrfError } from "../errors/CsrfError.js";
|
|
@@ -17,10 +22,11 @@ import { DEFAULT_SYSTEM_RULES, buildFrameAuthorizer, buildSystemRules } from "..
|
|
|
17
22
|
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
18
23
|
import { readAuditContext } from "../src/audit/readAuditContext.js";
|
|
19
24
|
import { SECURITY_AUDIT_CHANNEL, createAuditBridge } from "../src/audit/auditBridge.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
25
|
+
import { findZonesOpenToSignup } from "../src/openToSignup.js";
|
|
26
|
+
import { ROLES_WITHIN, RequestContext, Service, logColor } from "nodefony";
|
|
22
27
|
import { randomBytes } from "node:crypto";
|
|
23
|
-
|
|
28
|
+
import { encoderFromConfig } from "@nodefony/user";
|
|
29
|
+
// nodefony/service/firewall.ts
|
|
24
30
|
const serviceName = "firewall";
|
|
25
31
|
/**
|
|
26
32
|
* Réponse partagée de {@link Firewall.publishedProtectedResources} quand il n'y
|
|
@@ -87,6 +93,7 @@ var Firewall = class extends Service {
|
|
|
87
93
|
#cspFragments = null;
|
|
88
94
|
#config = null;
|
|
89
95
|
#configError = null;
|
|
96
|
+
#longLivedRevalidation = null;
|
|
90
97
|
constructor(module) {
|
|
91
98
|
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
92
99
|
this.module = module;
|
|
@@ -102,6 +109,7 @@ var Firewall = class extends Service {
|
|
|
102
109
|
}
|
|
103
110
|
this.#roleHierarchy = new RoleHierarchyWalker(this.#config.roleHierarchy);
|
|
104
111
|
this.container?.set("roleHierarchy", this.#roleHierarchy);
|
|
112
|
+
this.container?.set(ROLES_WITHIN, rolesWithin(() => this.container?.get("users") ?? null, this.#roleHierarchy));
|
|
105
113
|
this.#provisionSharedServices(this.#config);
|
|
106
114
|
if (this.#config.csrf.enabled) {
|
|
107
115
|
this.#csrf = new Csrf(this.#config.csrf, [...this.#config.csrf.trustedOrigins, ...this.#config.cors.origins]);
|
|
@@ -122,6 +130,7 @@ var Firewall = class extends Service {
|
|
|
122
130
|
this.#areas = list;
|
|
123
131
|
this.#instantiateAuthenticators(list);
|
|
124
132
|
}
|
|
133
|
+
for (const z of findZonesOpenToSignup(this.#config)) this.log(`${z.message} → ${z.action}`, "WARNING");
|
|
125
134
|
if (!this.#configError) {
|
|
126
135
|
this.#wireRealtime();
|
|
127
136
|
this.log(`Firewall ready — ${this.#areas?.length ?? 0} area(s), ${this.#authenticators?.size ?? 0} authenticator(s)`, "DEBUG");
|
|
@@ -137,7 +146,7 @@ var Firewall = class extends Service {
|
|
|
137
146
|
pattern: area.pattern,
|
|
138
147
|
host: area.host
|
|
139
148
|
} : { pattern: area.pattern };
|
|
140
|
-
realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null));
|
|
149
|
+
realtime.useAuthenticator(matcher, new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null, () => this.container?.get("users") ?? null));
|
|
141
150
|
wired = true;
|
|
142
151
|
}
|
|
143
152
|
const configRules = (this.#config?.realtimeChannels ?? []).map((r) => ({
|
|
@@ -172,6 +181,10 @@ var Firewall = class extends Service {
|
|
|
172
181
|
this.log(wired ? "Realtime data plane locked — WS handshake authenticators (RBAC) + frame authorizer + audit channel wired" : "Realtime data plane locked — frame authorizer (system floor) wired without qualifying zone; system channels closed to anonymous", "DEBUG");
|
|
173
182
|
}
|
|
174
183
|
#provisionSharedServices(config) {
|
|
184
|
+
this.container?.set("credentialTransport", new CredentialTransportPolicy({
|
|
185
|
+
enforce: shouldEnforceCredentialTransport(this.kernel?.environment, config.allowInsecureCredentials),
|
|
186
|
+
log: (message) => this.log(message, "WARNING")
|
|
187
|
+
}));
|
|
175
188
|
const specs = Object.values(config.encoders);
|
|
176
189
|
if (specs.length > 0) this.container?.set("passwordEncoder", encoderFromConfig(specs));
|
|
177
190
|
const rl = config.rateLimit;
|
|
@@ -263,17 +276,27 @@ var Firewall = class extends Service {
|
|
|
263
276
|
describe() {
|
|
264
277
|
const config = this.#config;
|
|
265
278
|
const mounted = this.#authenticators;
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
const open = new Map((config ? findZonesOpenToSignup(config) : []).map((z) => [z.zone, z]));
|
|
280
|
+
const zones = (this.#areas ?? []).map((a) => {
|
|
281
|
+
const notice = open.get(a.name);
|
|
282
|
+
return {
|
|
283
|
+
name: a.name,
|
|
284
|
+
pattern: a.pattern.source,
|
|
285
|
+
security: a.security,
|
|
286
|
+
stateless: a.stateless,
|
|
287
|
+
mode: a.mode,
|
|
288
|
+
authenticators: [...a.authenticators],
|
|
289
|
+
allowsAnonymous: a.authenticators.includes("anonymous"),
|
|
290
|
+
host: a.host ?? null,
|
|
291
|
+
realtime: a.realtime,
|
|
292
|
+
roles: a.roles ? [...a.roles] : [],
|
|
293
|
+
openToSignup: notice !== void 0,
|
|
294
|
+
openToSignupNotice: notice ? {
|
|
295
|
+
message: notice.message,
|
|
296
|
+
action: notice.action
|
|
297
|
+
} : null
|
|
298
|
+
};
|
|
299
|
+
});
|
|
277
300
|
const available = new Set(listAuthenticatorFactories());
|
|
278
301
|
const names = new Set(available);
|
|
279
302
|
if (mounted) for (const n of mounted.keys()) names.add(n);
|
|
@@ -340,12 +363,32 @@ var Firewall = class extends Service {
|
|
|
340
363
|
const hierarchy = {};
|
|
341
364
|
for (const [role, inherited] of Object.entries(raw)) hierarchy[role] = [...inherited];
|
|
342
365
|
const walker = this.roleHierarchy;
|
|
366
|
+
const roles = Object.keys(hierarchy).map((role) => ({
|
|
367
|
+
role,
|
|
368
|
+
inherits: [...walker.reachableRoles([role])].filter((r) => r !== role).sort()
|
|
369
|
+
}));
|
|
370
|
+
const providerMappings = [];
|
|
371
|
+
for (const [provider, p] of Object.entries(this.#config?.oauth2.providers ?? {})) {
|
|
372
|
+
const mapping = compileProviderRoleMapping(p);
|
|
373
|
+
if (mapping === null) continue;
|
|
374
|
+
providerMappings.push({
|
|
375
|
+
provider,
|
|
376
|
+
sources: [...mapping.sources],
|
|
377
|
+
mappings: Object.keys(mapping.table).sort().map((from) => {
|
|
378
|
+
const to = mapping.table[from] ?? "";
|
|
379
|
+
return {
|
|
380
|
+
from,
|
|
381
|
+
to,
|
|
382
|
+
platform: isPlatformRole(to)
|
|
383
|
+
};
|
|
384
|
+
}),
|
|
385
|
+
allowPlatformRoles: p.allowPlatformRoles
|
|
386
|
+
});
|
|
387
|
+
}
|
|
343
388
|
return {
|
|
344
389
|
hierarchy,
|
|
345
|
-
roles
|
|
346
|
-
|
|
347
|
-
inherits: [...walker.reachableRoles([role])].filter((r) => r !== role).sort()
|
|
348
|
-
}))
|
|
390
|
+
roles,
|
|
391
|
+
providerMappings
|
|
349
392
|
};
|
|
350
393
|
}
|
|
351
394
|
/**
|
|
@@ -424,7 +467,7 @@ var Firewall = class extends Service {
|
|
|
424
467
|
}
|
|
425
468
|
/**
|
|
426
469
|
* Pipeline complet de la zone : chaîne d'authenticators (selon `mode`) → ALS
|
|
427
|
-
* → Zero Trust. Rejette (401, challenge RFC
|
|
470
|
+
* → Zero Trust. Rejette (401, challenge RFC 9110 §11 posé) ou résout.
|
|
428
471
|
*/
|
|
429
472
|
async handleSecurity(context) {
|
|
430
473
|
if (this.#configError) throw new AuthenticationError("Security configuration invalid");
|
|
@@ -449,17 +492,22 @@ var Firewall = class extends Service {
|
|
|
449
492
|
this.#recordAuth(context, area, "auth.unverifiable", "failure", "verifier_unavailable", null);
|
|
450
493
|
throw error;
|
|
451
494
|
}
|
|
495
|
+
if (error instanceof InsecureTransportError) {
|
|
496
|
+
this.#recordAuth(context, area, "auth.failure", "failure", "insecure_transport", null);
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
452
499
|
this.#setChallenge(context, area);
|
|
453
500
|
this.#recordAuth(context, area, "auth.failure", "failure", "invalid_credentials", null);
|
|
454
501
|
throw error;
|
|
455
502
|
}
|
|
456
503
|
if (token === null) {
|
|
457
|
-
this.#setChallenge(context, area);
|
|
504
|
+
if (!this.#redirectToEntryPoint(context, area)) this.#setChallenge(context, area);
|
|
458
505
|
this.#recordAuth(context, area, "auth.denied", "denied", "no_credentials", null);
|
|
459
506
|
throw new AuthenticationError(`Authentication required for area "${area.name}"`);
|
|
460
507
|
}
|
|
461
508
|
RequestContext.set("user", token.getUser());
|
|
462
509
|
RequestContext.set("token", token);
|
|
510
|
+
if (token.isAuthenticated()) context.userIdentifier = token.getUserIdentifier();
|
|
463
511
|
if (!token.isAuthenticated() && token.type !== "anonymous") {
|
|
464
512
|
this.#setChallenge(context, area);
|
|
465
513
|
this.#recordAuth(context, area, "auth.denied", "denied", "unauthenticated", token.getUserIdentifier());
|
|
@@ -520,8 +568,13 @@ var Firewall = class extends Service {
|
|
|
520
568
|
* synchronizer token (`context.csrfToken`) — HttpContext pose ensuite le cookie
|
|
521
569
|
* lisible `csrf-token`. Sinon, hot-path GET = retour immédiat (aucun en-tête lu).
|
|
522
570
|
* - **Étape 1 (globale)** : sur une mutation, défense Fetch Metadata / Origin
|
|
523
|
-
* (rejet cross-site même sur route publique). Skippée
|
|
524
|
-
* auth par signature/clé)
|
|
571
|
+
* (rejet cross-site même sur route publique). Skippée SEULEMENT par `@CsrfExempt`
|
|
572
|
+
* (webhook, auth par signature/clé). `bypassFirewall` ne l'en dispense PAS : il
|
|
573
|
+
* retire l'authentification, pas la provenance — les routes de connexion sont
|
|
574
|
+
* en bypass, et c'est précisément là qu'un formulaire tiers ouvrirait chez la
|
|
575
|
+
* victime une session sur le compte de l'attaquant (login CSRF, CWE-352). Un
|
|
576
|
+
* appel de serveur à serveur (canal arrière OIDC) ne porte ni `Sec-Fetch-Site`
|
|
577
|
+
* ni `Origin` : il passe toujours.
|
|
525
578
|
* - **Étape 2 (opt-in)** : sur une mutation `@CsrfProtect`, exige EN PLUS le
|
|
526
579
|
* synchronizer token (en-tête `x-csrf-token` ≡ cookie + HMAC valide).
|
|
527
580
|
*
|
|
@@ -529,7 +582,6 @@ var Firewall = class extends Service {
|
|
|
529
582
|
*/
|
|
530
583
|
enforceCsrf(context) {
|
|
531
584
|
if (!this.#csrf) return;
|
|
532
|
-
if (context.resolver?.bypassFirewall) return;
|
|
533
585
|
const ctx = context;
|
|
534
586
|
if (!Csrf.isStateChanging(context.method)) {
|
|
535
587
|
if (ctx.csrfProtect && this.#csrfTokens && ctx.csrfToken == null) ctx.csrfToken = this.#csrfTokens.issue();
|
|
@@ -577,6 +629,18 @@ var Firewall = class extends Service {
|
|
|
577
629
|
return isPreflight ? 204 : void 0;
|
|
578
630
|
}
|
|
579
631
|
/**
|
|
632
|
+
* Le revalidateur de l'identité de la requête courante, pour une connexion
|
|
633
|
+
* longue qui la gardera (flux SSE). Même règle, même store de jetons que les
|
|
634
|
+
* sockets WebSocket — un transport qui en aurait une autre serait celui
|
|
635
|
+
* qu'un attaquant prendrait pour survivre à sa révocation.
|
|
636
|
+
*
|
|
637
|
+
* @returns `null` pour un anonyme ; sinon la fonction qui re-valide.
|
|
638
|
+
*/
|
|
639
|
+
currentRevalidator() {
|
|
640
|
+
this.#longLivedRevalidation ??= new FirewallRealtimeAuthenticator(() => this.container?.get("tokenStore") ?? null);
|
|
641
|
+
return this.#longLivedRevalidation.currentRevalidator();
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
580
644
|
* En-têtes de sécurité APPLICATIFS (P6 J5) — CSP, Referrer-Policy, isolation
|
|
581
645
|
* cross-origin (COOP/COEP/CORP), Origin-Agent-Cluster, Permissions-Policy.
|
|
582
646
|
* Posés sur toute réponse du pipeline (branché dans `handleHttp`). Complète le
|
|
@@ -690,6 +754,34 @@ var Firewall = class extends Service {
|
|
|
690
754
|
}
|
|
691
755
|
return token;
|
|
692
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* Pose la redirection vers la page de connexion de la zone, quand la requête
|
|
759
|
+
* refusée est la navigation d'un navigateur. Cold path (refus seulement).
|
|
760
|
+
*
|
|
761
|
+
* La réponse est seulement MARQUÉE (`302` + `Location`) : le refus continue
|
|
762
|
+
* de remonter, et http rend une réponse déjà redirigée telle quelle au lieu
|
|
763
|
+
* d'un 401. Capability check : un contexte sans `redirect` (WebSocket) garde
|
|
764
|
+
* son refus.
|
|
765
|
+
*
|
|
766
|
+
* @returns `true` si la redirection est posée
|
|
767
|
+
*/
|
|
768
|
+
#redirectToEntryPoint(context, area) {
|
|
769
|
+
if (area.entryPoint === void 0 || !("redirect" in context)) return false;
|
|
770
|
+
const request = context.request;
|
|
771
|
+
const header = (name) => {
|
|
772
|
+
const value = request.headers[name];
|
|
773
|
+
return typeof value === "string" ? value : void 0;
|
|
774
|
+
};
|
|
775
|
+
const location = entryPointLocation(area.entryPoint, {
|
|
776
|
+
method: request.method,
|
|
777
|
+
accept: header("accept"),
|
|
778
|
+
fetchMode: header("sec-fetch-mode"),
|
|
779
|
+
path: request.url.pathname + request.url.search
|
|
780
|
+
});
|
|
781
|
+
if (location === null) return false;
|
|
782
|
+
context.redirect(location, 302);
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
693
785
|
#setChallenge(context, area) {
|
|
694
786
|
const response = context.response;
|
|
695
787
|
if (typeof response?.setHeader !== "function") return;
|
|
@@ -707,5 +799,4 @@ var Firewall = class extends Service {
|
|
|
707
799
|
return super.log(pci, severity, msgid, msg);
|
|
708
800
|
}
|
|
709
801
|
};
|
|
710
|
-
//#endregion
|
|
711
802
|
export { Firewall, Firewall as default };
|