@nodefony/security 10.0.0-beta.5 → 10.0.0-beta.7
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/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +63 -61
- package/dist/nodefony/command/security-mcp-token.js +43 -5
- package/dist/nodefony/config/config.js +30 -11
- package/dist/nodefony/config/defineModuleConfig.js +52 -1
- package/dist/nodefony/contracts/IWebhookPayload.js +1 -0
- package/dist/nodefony/errors/FetchPublicError.js +18 -0
- package/dist/nodefony/errors/SsrfError.js +9 -1
- package/dist/nodefony/errors/StaleSessionError.js +20 -0
- package/dist/nodefony/errors/index.js +7 -5
- package/dist/nodefony/keycloak/keycloakAdmin.js +3 -3
- package/dist/nodefony/keycloak/scaffold.js +31 -14
- package/dist/nodefony/keycloak/security-keycloak-realm.js +1 -1
- package/dist/nodefony/service/accessTokenVerifier.js +6 -6
- package/dist/nodefony/service/apiKeys.js +8 -4
- package/dist/nodefony/service/auditService.js +48 -3
- package/dist/nodefony/service/authFlow.js +9 -6
- package/dist/nodefony/service/authorization.js +1 -1
- package/dist/nodefony/service/firewall.js +281 -44
- package/dist/nodefony/service/keycloak.js +1 -1
- package/dist/nodefony/service/oauth2.js +8 -4
- package/dist/nodefony/service/securityHeaders.js +340 -47
- package/dist/nodefony/service/tokenService.js +16 -11
- package/dist/nodefony/service/totp.js +10 -5
- package/dist/nodefony/service/webAuthn.js +46 -7
- package/dist/nodefony/service/webhooks.js +11 -4
- package/dist/nodefony/src/admin/SecurityAdminApi.js +19 -0
- package/dist/nodefony/src/audit/AuditRefusalAggregator.js +139 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +1 -1
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +31 -7
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +62 -20
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +29 -6
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +22 -9
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +2 -2
- package/dist/nodefony/src/csp.js +265 -10
- package/dist/nodefony/src/cspReports.js +307 -0
- package/dist/nodefony/src/net/fetchPublic.js +244 -0
- package/dist/nodefony/src/net/ssrfGuard.js +2 -2
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -1
- package/dist/nodefony/src/oauth/metadata.js +1 -1
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +17 -4
- package/dist/nodefony/src/oauth/providers/github.js +1 -1
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +59 -2
- package/dist/nodefony/src/realtime/frameAuthorizer.js +136 -44
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +8 -2
- package/dist/nodefony/src/token/jwtRuntime.js +22 -2
- package/dist/nodefony/src/token/tokenTimeClaims.js +34 -0
- package/dist/nodefony/src/totp/totpOperations.js +1 -1
- package/dist/nodefony/src/voter/RoleVoter.js +1 -1
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +71 -17
- package/dist/nodefony/src/webhook/webhookDelivery.js +37 -86
- package/dist/nodefony/testing/index.js +26 -36
- package/dist/types/index.d.ts +5 -1
- package/dist/types/nodefony/command/security-mcp-token.d.ts +12 -0
- package/dist/types/nodefony/config/config.d.ts +8 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +8 -2
- package/dist/types/nodefony/contracts/IFirewall.d.ts +15 -2
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +7 -3
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +13 -3
- package/dist/types/nodefony/contracts/IWebhookPayload.d.ts +24 -0
- package/dist/types/nodefony/errors/FetchPublicError.d.ts +15 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +8 -1
- package/dist/types/nodefony/errors/StaleSessionError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +3 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +12 -6
- package/dist/types/nodefony/service/apiKeys.d.ts +3 -0
- package/dist/types/nodefony/service/authFlow.d.ts +2 -2
- package/dist/types/nodefony/service/firewall.d.ts +28 -7
- package/dist/types/nodefony/service/securityHeaders.d.ts +82 -11
- package/dist/types/nodefony/service/webAuthn.d.ts +13 -0
- package/dist/types/nodefony/service/webhooks.d.ts +7 -1
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +3 -0
- package/dist/types/nodefony/src/audit/AuditRefusalAggregator.d.ts +55 -0
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +11 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +17 -3
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +11 -4
- package/dist/types/nodefony/src/csp.d.ts +142 -0
- package/dist/types/nodefony/src/cspReports.d.ts +73 -0
- package/dist/types/nodefony/src/net/fetchPublic.d.ts +73 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +15 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +35 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +10 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +42 -18
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +21 -2
- package/dist/types/nodefony/src/token/tokenTimeClaims.d.ts +32 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +36 -1
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +5 -2
- package/dist/types/nodefony/testing/index.d.ts +6 -0
- package/docs/api-keys.md +3 -1
- package/docs/audit.md +112 -28
- package/docs/authenticators.md +6 -4
- package/docs/csrf.md +3 -2
- package/docs/external-jwt.md +5 -1
- package/docs/firewall.md +3 -2
- package/docs/headers.md +427 -71
- package/docs/keycloak.md +4 -4
- package/docs/login-page.md +4 -3
- package/docs/obtenir-un-jeton.md +17 -10
- package/docs/tokens.md +1 -1
- package/docs/totp.md +2 -2
- package/docs/webauthn.md +2 -2
- package/docs/webhooks.md +63 -18
- package/package.json +17 -17
|
@@ -25,15 +25,19 @@ const APP_KEYCLOAK_REALM_FILE = "docker/keycloak/import/realm.json";
|
|
|
25
25
|
const APP_KEYCLOAK_THEMES_DIR = "docker/keycloak/themes";
|
|
26
26
|
const APP_KEYCLOAK_THEME_DIR = `${APP_KEYCLOAK_THEMES_DIR}/${KEYCLOAK_THEME_NAME}`;
|
|
27
27
|
/** Fichier de configuration de sécurité d'une application générée. */
|
|
28
|
-
const APP_SECURITY_CONFIG_FILE = "nodefony/config/security.ts";
|
|
29
28
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* ni `NF_PORT_HTTPS` — le port HTTPS par défaut, comme les URL de retour du
|
|
33
|
-
* realm. Deux paquets, une valeur : le test de contribution compare le
|
|
34
|
-
* gabarit rendu à cette constante.
|
|
29
|
+
* Le fichier où la démo de `create app --demo` (module privé `modules/demo`)
|
|
30
|
+
* déclare sa zone à jeton — la surcharge `module-security` qu'elle apporte.
|
|
35
31
|
*/
|
|
36
|
-
const
|
|
32
|
+
const DEMO_SECURITY_CONFIG_FILE = "modules/demo/nodefony/config/security.ts";
|
|
33
|
+
/**
|
|
34
|
+
* Ressource de l'API partenaire de la démo en développement : le chemin de sa
|
|
35
|
+
* zone (`/demo/api/partner`) rendu absolu contre l'émetteur de développement
|
|
36
|
+
* — le port HTTPS par défaut, comme les URL de retour du realm. Deux paquets,
|
|
37
|
+
* une valeur : le test de contribution la confronte à une application
|
|
38
|
+
* `--demo` réellement rendue.
|
|
39
|
+
*/
|
|
40
|
+
const APP_PARTNER_DEV_RESOURCE = "https://localhost:5152/demo/api/partner";
|
|
37
41
|
/** Les types de thème que le realm sait déclarer, et la clé qui les porte. */
|
|
38
42
|
const THEME_TYPES = [
|
|
39
43
|
"login",
|
|
@@ -124,6 +128,12 @@ function devMachineSecret(appName) {
|
|
|
124
128
|
* rôle client `admin` (`roleMapping: { admin: "ROLE_ADMIN" }`), et les
|
|
125
129
|
* audiences des zones `external-jwt` que la configuration rendue déclare.
|
|
126
130
|
*
|
|
131
|
+
* Le client « machine » (`<app>-machine`, `client_credentials`) n'existe que
|
|
132
|
+
* s'il a une audience à recevoir : une application neuve n'ouvre aucune zone à
|
|
133
|
+
* jeton tiers (#746), et un client que rien n'accepte serait un secret de plus
|
|
134
|
+
* à garder pour rien. La démo, qui apporte l'API entre programmes, le fait
|
|
135
|
+
* naître en déclarant sa ressource.
|
|
136
|
+
*
|
|
127
137
|
* @param appName - nom de l'application (= realm = identifiant de client)
|
|
128
138
|
* @param themes - types de thème à déclarer
|
|
129
139
|
* @param ids - `sub` des comptes de démonstration, FIXÉS dans le fichier :
|
|
@@ -150,7 +160,7 @@ function appDevRealmInput(appName, themes, ids, audiences = []) {
|
|
|
150
160
|
description: "Administrateur de l'application — traduit en ROLE_ADMIN par la table roleMapping du fournisseur keycloak."
|
|
151
161
|
}],
|
|
152
162
|
realmRoles: [],
|
|
153
|
-
machine: {
|
|
163
|
+
machine: audiences.length === 0 ? null : {
|
|
154
164
|
clientId: `${appName}-machine`,
|
|
155
165
|
secret: devMachineSecret(appName),
|
|
156
166
|
name: `${appName} — appelant machine (compte de service)`,
|
|
@@ -191,17 +201,24 @@ function mountsKeycloak(context) {
|
|
|
191
201
|
].some((file) => context.read(file)?.includes("./docker/keycloak/") === true);
|
|
192
202
|
}
|
|
193
203
|
/**
|
|
194
|
-
* Les audiences que la configuration RENDUE de l'application déclare.
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
204
|
+
* Les audiences que la configuration RENDUE de l'application déclare. Une
|
|
205
|
+
* application neuve n'en déclare aucune ; la démo ouvre la zone
|
|
206
|
+
* `demo-partner` (`jwt` + `external-jwt`) dans son propre module : sans son
|
|
207
|
+
* audience dans le realm, le jeton du client machine y serait refusé dès le
|
|
208
|
+
* premier essai, et `security:keycloak:realm --check` trouverait le realm né
|
|
209
|
+
* en désaccord avec la configuration.
|
|
210
|
+
*
|
|
211
|
+
* Le fichier de zone de la démo doit écrire le CHEMIN de la ressource : un
|
|
212
|
+
* module qui l'aurait changé n'a pas l'audience de cette constante, et
|
|
213
|
+
* mieux vaut ne rien inscrire qu'une audience que la zone refuserait.
|
|
199
214
|
*
|
|
200
215
|
* @param context - contexte de contribution
|
|
201
216
|
* @returns les ressources à inscrire dans les jetons du realm
|
|
202
217
|
*/
|
|
203
218
|
function renderedAudiences(context) {
|
|
204
|
-
|
|
219
|
+
const demo = context.read(DEMO_SECURITY_CONFIG_FILE);
|
|
220
|
+
const resourcePath = new URL(APP_PARTNER_DEV_RESOURCE).pathname;
|
|
221
|
+
return demo?.includes(`"${resourcePath}"`) === true ? [APP_PARTNER_DEV_RESOURCE] : [];
|
|
205
222
|
}
|
|
206
223
|
/**
|
|
207
224
|
* Recopie les thèmes livrés dans l'application — `nodefony` et ses thèmes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { KeycloakRealmError } from "./keycloakRealmInput.js";
|
|
2
1
|
import { KeycloakAdminError } from "./keycloakAdmin.js";
|
|
2
|
+
import { KeycloakRealmError } from "./keycloakRealmInput.js";
|
|
3
3
|
import { Command } from "nodefony";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
// nodefony/keycloak/security-keycloak-realm.ts
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
2
1
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
3
|
-
import {
|
|
4
|
-
import peekIssuer from "../src/authenticator/peekIssuer.js";
|
|
2
|
+
import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
|
|
5
3
|
import { JwtAuthenticator } from "../src/authenticator/JwtAuthenticator.js";
|
|
6
|
-
import
|
|
4
|
+
import peekIssuer from "../src/authenticator/peekIssuer.js";
|
|
7
5
|
import { resolveJwtRuntime } from "../src/token/jwtRuntime.js";
|
|
8
|
-
import {
|
|
6
|
+
import { UserToken } from "../src/token/UserToken.js";
|
|
7
|
+
import { ExternalSubjectResolver, externalSubjectOptions } from "../src/authenticator/ExternalSubjectResolver.js";
|
|
8
|
+
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
9
9
|
import { ACCESS_ACCOUNT_RESOLVER, ACCESS_TOKEN_VERIFIER, Service } from "nodefony";
|
|
10
10
|
// nodefony/service/accessTokenVerifier.ts
|
|
11
11
|
const serviceName = ACCESS_TOKEN_VERIFIER;
|
|
@@ -67,7 +67,7 @@ var AccessTokenVerifierService = class extends Service {
|
|
|
67
67
|
} catch {
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
const runtime = config.jwt.enabled ? resolveJwtRuntime(config.jwt) : null;
|
|
70
|
+
const runtime = config.jwt.enabled ? resolveJwtRuntime(config.jwt, config.areas) : null;
|
|
71
71
|
const local = runtime === null ? null : new JwtAuthenticator(this.container, runtime);
|
|
72
72
|
const { issuers, ...tuning } = config.resourceServer;
|
|
73
73
|
let remote = null;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TOKEN_FACETS } from "../src/token/tokenFilters.js";
|
|
2
2
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ApiKeyError } from "../errors/ApiKeyError.js";
|
|
4
4
|
import { generateApiKey } from "../src/apikey/apiKeyFormat.js";
|
|
5
|
+
import { ScopeGrantPolicy, refusedScopes } from "../src/token/scopeGrants.js";
|
|
5
6
|
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
6
|
-
import {
|
|
7
|
-
import { ApiKeyError } from "../errors/ApiKeyError.js";
|
|
7
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
|
|
8
8
|
import { Service, countFacets } from "nodefony";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
// nodefony/service/apiKeys.ts
|
|
@@ -97,6 +97,9 @@ var ApiKeyService = class extends Service {
|
|
|
97
97
|
* @param roles - rôles du porteur tels que son compte les porte ; omis =
|
|
98
98
|
* aucun rôle, donc tout scope réservé est refusé (fail-closed).
|
|
99
99
|
* @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
|
|
100
|
+
* @throws ApiKeyError 400 — `tenantId` rempli : le périmètre d'un locataire n'est
|
|
101
|
+
* pas encore appliqué (aucun lecteur ne cloisonne sur lui) — l'accepter rendrait
|
|
102
|
+
* la clé trompeuse, et l'appliquer un jour lui retirerait des accès (rupture).
|
|
100
103
|
* @throws ApiKeyError 403 — scope réservé à des rôles que le porteur n'a pas.
|
|
101
104
|
* @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
|
|
102
105
|
* @throws ApiKeyError 403 — aucun compte sous cet identifiant (la clé est liée
|
|
@@ -104,6 +107,7 @@ var ApiKeyService = class extends Service {
|
|
|
104
107
|
* @throws ApiKeyError 503 — store ou annuaire `users` indisponible.
|
|
105
108
|
*/
|
|
106
109
|
async createForSubject(subjectId, subjectType, opts, roles = []) {
|
|
110
|
+
if (opts.tenantId != null) throw new ApiKeyError("apiKeys: tenantId n'est pas encore pris en charge — le multi-tenant n'existe pas, et une clé rattachée à un locataire verrait tout. Laisser tenantId absent ou null.", 400);
|
|
107
111
|
const store = this.#resolveStore();
|
|
108
112
|
const name = this.#normalizeName(opts.name);
|
|
109
113
|
const scopes = this.#normalizeScopes(opts.scopes);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSecurityConfig } from "../config/defineModuleConfig.js";
|
|
2
2
|
import { getAuditStoreFactory, listAuditStores } from "../src/audit/auditStoreRegistry.js";
|
|
3
|
+
import { AuditRefusalAggregator } from "../src/audit/AuditRefusalAggregator.js";
|
|
3
4
|
import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Service, durableStoreRemedy, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
|
|
4
5
|
import { randomBytes } from "node:crypto";
|
|
5
6
|
// nodefony/service/auditService.ts
|
|
@@ -30,11 +31,21 @@ var AuditService = class extends Service {
|
|
|
30
31
|
#idPrefix = "";
|
|
31
32
|
#seq = 0;
|
|
32
33
|
#gc = null;
|
|
34
|
+
/** Agrégation des refus — `null` si désactivée par la config. */
|
|
35
|
+
#aggregator = null;
|
|
36
|
+
/** Balayage des fenêtres échues — armé au premier refus suivi, coupé à vide. */
|
|
37
|
+
#sweep = null;
|
|
38
|
+
#windowMs = 0;
|
|
33
39
|
constructor(module) {
|
|
34
40
|
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
35
41
|
this.module = module;
|
|
36
|
-
this.kernel?.once("onBoot", () =>
|
|
37
|
-
|
|
42
|
+
this.kernel?.once("onBoot", () => {
|
|
43
|
+
try {
|
|
44
|
+
this.#build();
|
|
45
|
+
} finally {
|
|
46
|
+
this.kernel?.once("onTerminate", () => this.#shutdown());
|
|
47
|
+
}
|
|
48
|
+
});
|
|
38
49
|
}
|
|
39
50
|
#build() {
|
|
40
51
|
let config;
|
|
@@ -91,9 +102,17 @@ var AuditService = class extends Service {
|
|
|
91
102
|
onError: (e) => this.log(e, "WARNING")
|
|
92
103
|
});
|
|
93
104
|
this.#gc.start();
|
|
105
|
+
const aggregate = config.audit.aggregate;
|
|
106
|
+
if (aggregate.enabled) {
|
|
107
|
+
this.#windowMs = aggregate.windowMs;
|
|
108
|
+
this.#aggregator = new AuditRefusalAggregator(aggregate, (draft) => this.#write(draft, Date.now()));
|
|
109
|
+
}
|
|
94
110
|
this.log(`audit service ready — store "${config.audit.store}", rétention ${config.audit.retentionDays}j`, "DEBUG");
|
|
95
111
|
}
|
|
96
112
|
#shutdown() {
|
|
113
|
+
this.#stopSweep();
|
|
114
|
+
this.#aggregator?.flush();
|
|
115
|
+
this.#aggregator = null;
|
|
97
116
|
this.#gc?.stop();
|
|
98
117
|
this.#gc = null;
|
|
99
118
|
this.#listeners = null;
|
|
@@ -102,10 +121,21 @@ var AuditService = class extends Service {
|
|
|
102
121
|
}
|
|
103
122
|
record(draft) {
|
|
104
123
|
if (!this.#enabled || this.#store === null) return;
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (draft.outcome !== "success" && this.#aggregator !== null) {
|
|
126
|
+
const written = this.#aggregator.admit(draft, now);
|
|
127
|
+
if (this.#sweep === null) this.#startSweep();
|
|
128
|
+
if (!written) return;
|
|
129
|
+
}
|
|
130
|
+
this.#write(draft, now);
|
|
131
|
+
}
|
|
132
|
+
/** Persiste (fire-and-forget) et diffuse un événement horodaté `ts`. */
|
|
133
|
+
#write(draft, ts) {
|
|
134
|
+
if (this.#store === null) return;
|
|
105
135
|
const event = {
|
|
106
136
|
...draft,
|
|
107
137
|
id: `${this.#idPrefix}-${(this.#seq++).toString(36)}`,
|
|
108
|
-
ts
|
|
138
|
+
ts
|
|
109
139
|
};
|
|
110
140
|
this.#store.append(event).catch((error) => this.log(error, "ERROR"));
|
|
111
141
|
if (this.#listeners !== null) for (const listener of this.#listeners) try {
|
|
@@ -114,6 +144,21 @@ var AuditService = class extends Service {
|
|
|
114
144
|
this.log(error, "ERROR");
|
|
115
145
|
}
|
|
116
146
|
}
|
|
147
|
+
/** Arme le balayage des fenêtres échues (une fois par fenêtre, `unref`). */
|
|
148
|
+
#startSweep() {
|
|
149
|
+
this.#sweep = setInterval(() => {
|
|
150
|
+
const aggregator = this.#aggregator;
|
|
151
|
+
if (aggregator === null) return this.#stopSweep();
|
|
152
|
+
aggregator.sweep(Date.now());
|
|
153
|
+
if (aggregator.size === 0) this.#stopSweep();
|
|
154
|
+
}, this.#windowMs);
|
|
155
|
+
this.#sweep.unref();
|
|
156
|
+
}
|
|
157
|
+
#stopSweep() {
|
|
158
|
+
if (this.#sweep === null) return;
|
|
159
|
+
clearInterval(this.#sweep);
|
|
160
|
+
this.#sweep = null;
|
|
161
|
+
}
|
|
117
162
|
subscribe(listener) {
|
|
118
163
|
this.#listeners ??= [];
|
|
119
164
|
this.#listeners.push(listener);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { securityConfigSchema } from "../config/config.js";
|
|
2
|
-
import { InsecureTransportError } from "../errors/InsecureTransportError.js";
|
|
3
1
|
import { AuthenticationError } from "../errors/AuthenticationError.js";
|
|
4
2
|
import { ThrottledError } from "../errors/ThrottledError.js";
|
|
5
|
-
import {
|
|
3
|
+
import { InsecureTransportError } from "../errors/InsecureTransportError.js";
|
|
6
4
|
import { resolveBoundSessionIdentity, resolveSessionIdentity } from "../src/sessionIdentity.js";
|
|
5
|
+
import { ACCOUNT_ID_KEY, CREDENTIALS_VERSION_KEY, credentialsVersionOf } from "../src/accountBinding.js";
|
|
7
6
|
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
8
7
|
import { readAuditContext } from "../src/audit/readAuditContext.js";
|
|
9
8
|
import { amrForFactor } from "../src/sessionAuthentication.js";
|
|
9
|
+
import { securityConfigSchema } from "../config/config.js";
|
|
10
10
|
import { LOGIN_PAGE_SKIN_SPECS, MFA_METHOD_TOTP, SESSION_AMR_KEY, SESSION_AUTH_AT_KEY, Service } from "nodefony";
|
|
11
11
|
import { requestUser } from "@nodefony/user";
|
|
12
12
|
// nodefony/service/authFlow.ts
|
|
@@ -284,8 +284,8 @@ var AuthFlow = class extends Service {
|
|
|
284
284
|
* (mêmes contrôles que le `SessionAuthenticator` — source unique).
|
|
285
285
|
*
|
|
286
286
|
* @returns la projection publique, ou `null` (pas de session, session
|
|
287
|
-
* orpheline, compte verrouillé/désactivé
|
|
288
|
-
* `{ user: null }`.
|
|
287
|
+
* orpheline, compte verrouillé/désactivé — la session périmée est alors
|
|
288
|
+
* détruite, cookie compris) — le handler répond `200` `{ user: null }`.
|
|
289
289
|
*/
|
|
290
290
|
async me(context) {
|
|
291
291
|
const identifier = context.session?.user;
|
|
@@ -295,7 +295,10 @@ var AuthFlow = class extends Service {
|
|
|
295
295
|
try {
|
|
296
296
|
return toSafeUser(await resolveBoundSessionIdentity(this.#resolveUsers(), identifier, context.session?.getMetaBag(ACCOUNT_ID_KEY), context.session?.getMetaBag(CREDENTIALS_VERSION_KEY)));
|
|
297
297
|
} catch (error) {
|
|
298
|
-
if (error instanceof AuthenticationError)
|
|
298
|
+
if (error instanceof AuthenticationError) {
|
|
299
|
+
await context.session?.destroy(true);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
299
302
|
throw error;
|
|
300
303
|
}
|
|
301
304
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
2
1
|
import "../contracts/IAccessVoter.js";
|
|
3
2
|
import { listVoterFactories } from "../src/voter/voterRegistry.js";
|
|
3
|
+
import { recordAudit } from "../src/audit/recordAudit.js";
|
|
4
4
|
import { Service, logColor } from "nodefony";
|
|
5
5
|
// nodefony/service/authorization.ts
|
|
6
6
|
const serviceName = "authorization";
|