@astram/guard 0.6.0 → 0.8.0
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/auth/account.controller.d.ts +3 -1
- package/dist/auth/account.controller.d.ts.map +1 -1
- package/dist/auth/account.controller.js +12 -5
- package/dist/auth/auth.controller.d.ts +130 -21
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +537 -143
- package/dist/auth/auth.module.d.ts +15 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +241 -30
- package/dist/auth/connections.d.ts +1 -1
- package/dist/auth/connections.d.ts.map +1 -1
- package/dist/auth/connections.js +14 -3
- package/dist/auth/default-session-facts-projector.d.ts +26 -0
- package/dist/auth/default-session-facts-projector.d.ts.map +1 -0
- package/dist/auth/default-session-facts-projector.js +21 -0
- package/dist/auth/mfa-step-up.d.ts +32 -0
- package/dist/auth/mfa-step-up.d.ts.map +1 -0
- package/dist/auth/mfa-step-up.js +109 -0
- package/dist/auth/pending-mfa-store.d.ts +50 -52
- package/dist/auth/pending-mfa-store.d.ts.map +1 -1
- package/dist/auth/pending-mfa-store.js +103 -61
- package/dist/auth/ports/session-facts-projector.d.ts +5 -0
- package/dist/auth/ports/session-facts-projector.d.ts.map +1 -1
- package/dist/auth/rate-limit/canonical-hint.d.ts +24 -0
- package/dist/auth/rate-limit/canonical-hint.d.ts.map +1 -0
- package/dist/auth/rate-limit/canonical-hint.js +63 -0
- package/dist/auth/rate-limit/trust-proxy-check.d.ts +46 -0
- package/dist/auth/rate-limit/trust-proxy-check.d.ts.map +1 -0
- package/dist/auth/rate-limit/trust-proxy-check.js +94 -0
- package/dist/auth/tokens.d.ts +70 -11
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +71 -12
- package/dist/authentication/log-email-sender.d.ts +9 -4
- package/dist/authentication/log-email-sender.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.d.ts +46 -0
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-api-key-store.js +111 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +79 -65
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +116 -73
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-or-totp-verifier.d.ts +21 -3
- package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -1
- package/dist/authentication/mariadb-or-totp-verifier.js +37 -6
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts +14 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.js +29 -11
- package/dist/authentication/noop-email-sender.d.ts +12 -0
- package/dist/authentication/noop-email-sender.d.ts.map +1 -0
- package/dist/authentication/noop-email-sender.js +9 -0
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
- package/dist/authz/manifest-operation-authority-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +3 -1
- package/dist/authz/mariadb-policy-store.js +1 -1
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +1 -4
- package/dist/authz/permission-version-cache.d.ts +38 -0
- package/dist/authz/permission-version-cache.d.ts.map +1 -0
- package/dist/authz/permission-version-cache.js +81 -0
- package/dist/authz/wire-record-authorization.d.ts +25 -1
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +55 -2
- package/dist/completeness/catalog-reconciler.d.ts.map +1 -1
- package/dist/config/app-config.d.ts +52 -0
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +66 -4
- package/dist/encryption/local-kek-crypto.d.ts +15 -0
- package/dist/encryption/local-kek-crypto.d.ts.map +1 -0
- package/dist/encryption/local-kek-crypto.js +79 -0
- package/dist/enforcement/enforcement.module.d.ts.map +1 -1
- package/dist/enforcement/enforcement.module.js +9 -0
- package/dist/enforcement/engine-error.filter.d.ts +57 -0
- package/dist/enforcement/engine-error.filter.d.ts.map +1 -0
- package/dist/enforcement/engine-error.filter.js +118 -0
- package/dist/enforcement/global-enforcement.guard.d.ts +31 -2
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +148 -6
- package/dist/identity/auth-facts.controller.d.ts.map +1 -1
- package/dist/identity/identity-actions.d.ts +9 -0
- package/dist/identity/identity-actions.d.ts.map +1 -1
- package/dist/identity/identity-actions.js +16 -1
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +2 -3
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -25
- package/dist/module/shield.module.d.ts +9 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +47 -38
- package/dist/request-auth/request-principal.d.ts +11 -3
- package/dist/request-auth/request-principal.d.ts.map +1 -1
- package/dist/request-auth/request-principal.js +58 -2
- package/dist/request-auth/session-facts-als.d.ts +16 -1
- package/dist/request-auth/session-facts-als.d.ts.map +1 -1
- package/dist/request-auth/session-facts-als.js +23 -0
- package/dist/request-auth/session-token-binder.d.ts +166 -10
- package/dist/request-auth/session-token-binder.d.ts.map +1 -1
- package/dist/request-auth/session-token-binder.js +408 -28
- package/dist/request-auth/tokens.d.ts +10 -0
- package/dist/request-auth/tokens.d.ts.map +1 -1
- package/dist/request-auth/tokens.js +11 -1
- package/dist/secrets/encrypted-file-secrets.d.ts +16 -0
- package/dist/secrets/encrypted-file-secrets.d.ts.map +1 -0
- package/dist/secrets/encrypted-file-secrets.js +98 -0
- package/dist/session/hot-revoking-session-record-store.d.ts +46 -0
- package/dist/session/hot-revoking-session-record-store.d.ts.map +1 -0
- package/dist/session/hot-revoking-session-record-store.js +58 -0
- package/dist/session/index.d.ts +3 -1
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +8 -1
- package/dist/session/mariadb-session-record-store.d.ts +6 -0
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
- package/dist/session/mariadb-session-record-store.js +16 -1
- package/dist/session/session-lifetimes.d.ts +30 -0
- package/dist/session/session-lifetimes.d.ts.map +1 -0
- package/dist/session/session-lifetimes.js +21 -0
- package/dist/session/session-restriction-store.d.ts +43 -0
- package/dist/session/session-restriction-store.d.ts.map +1 -0
- package/dist/session/session-restriction-store.js +35 -0
- package/migrations/1710000000017-CreateServiceAccountKey.cjs +41 -0
- package/package.json +3 -3
|
@@ -136,7 +136,16 @@ export interface AuthModuleOptions {
|
|
|
136
136
|
*/
|
|
137
137
|
readonly credentialWriter?: CredentialWriter;
|
|
138
138
|
readonly resetTokenStore?: ResetTokenStore;
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* The password-reset email delivery seam (security hardening #6):
|
|
141
|
+
* - an {@link EmailSender} → used in every environment (production needs one);
|
|
142
|
+
* - `'disabled'` → a no-op sender: forgot-password still answers 204, nothing is
|
|
143
|
+
* delivered or printed, one startup warning;
|
|
144
|
+
* - omitted → the dev `LogEmailSender` (prints reset links to stdout) ONLY when the raw
|
|
145
|
+
* `NODE_ENV` is `development` or `test`; otherwise (incl. unset `NODE_ENV`) auth
|
|
146
|
+
* composition throws "auth:true requires `emailSender` (or `emailSender: 'disabled'`)".
|
|
147
|
+
*/
|
|
148
|
+
readonly emailSender?: EmailSender | 'disabled';
|
|
140
149
|
readonly recoveryCodeStore?: RecoveryCodeStore;
|
|
141
150
|
readonly profileWriter?: ProfileWriter;
|
|
142
151
|
readonly sessionSweeper?: SessionSweeper;
|
|
@@ -210,6 +219,11 @@ export interface AuthModuleOptions {
|
|
|
210
219
|
*/
|
|
211
220
|
readonly membershipChecker?: MembershipChecker;
|
|
212
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The no-override default sender decision (pure apart from the warning). Exported for
|
|
224
|
+
* the unit test only — not part of the package's public surface.
|
|
225
|
+
*/
|
|
226
|
+
export declare function resolveDefaultEmailSender(rawNodeEnv: string | undefined): EmailSender;
|
|
213
227
|
/**
|
|
214
228
|
* The auth composition module. Imports {@link PersistenceModule} (the live
|
|
215
229
|
* connections) and provides the adapters + composed engines. Exports the two
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/auth.module.js
CHANGED
|
@@ -107,6 +107,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
107
107
|
exports.AuthModule = exports.hmacSha1 = void 0;
|
|
108
108
|
exports.resolveTotpVerifier = resolveTotpVerifier;
|
|
109
109
|
exports.makeDefaultTotpVerifier = makeDefaultTotpVerifier;
|
|
110
|
+
exports.resolveDefaultEmailSender = resolveDefaultEmailSender;
|
|
110
111
|
const common_1 = require("@nestjs/common");
|
|
111
112
|
const node_crypto_1 = require("node:crypto");
|
|
112
113
|
const app_config_js_1 = require("../config/app-config.js");
|
|
@@ -121,6 +122,7 @@ const mariadb_recovery_code_store_js_1 = require("../authentication/mariadb-reco
|
|
|
121
122
|
const mariadb_mfa_device_store_js_1 = require("../authentication/mariadb-mfa-device-store.js");
|
|
122
123
|
const mariadb_session_sweeper_js_1 = require("../authentication/mariadb-session-sweeper.js");
|
|
123
124
|
const log_email_sender_js_1 = require("../authentication/log-email-sender.js");
|
|
125
|
+
const noop_email_sender_js_1 = require("../authentication/noop-email-sender.js");
|
|
124
126
|
const mariadb_or_totp_verifier_js_1 = require("../authentication/mariadb-or-totp-verifier.js");
|
|
125
127
|
const permission_facts_adapter_js_1 = require("../identity/permission-facts.adapter.js");
|
|
126
128
|
const auth_facts_controller_js_1 = require("../identity/auth-facts.controller.js");
|
|
@@ -129,9 +131,15 @@ const persistence_module_js_1 = require("./persistence.module.js");
|
|
|
129
131
|
const auth_controller_js_1 = require("./auth.controller.js");
|
|
130
132
|
const account_controller_js_1 = require("./account.controller.js");
|
|
131
133
|
const pending_mfa_store_js_1 = require("./pending-mfa-store.js");
|
|
134
|
+
const mfa_step_up_js_1 = require("./mfa-step-up.js");
|
|
132
135
|
const tokens_js_1 = require("./tokens.js");
|
|
136
|
+
const permission_version_cache_js_1 = require("../authz/permission-version-cache.js");
|
|
137
|
+
const session_restriction_store_js_1 = require("../session/session-restriction-store.js");
|
|
133
138
|
const attempt_limiter_js_1 = require("./rate-limit/attempt-limiter.js");
|
|
139
|
+
const trust_proxy_check_js_1 = require("./rate-limit/trust-proxy-check.js");
|
|
134
140
|
const tokens_js_2 = require("../request-auth/tokens.js");
|
|
141
|
+
const mariadb_api_key_store_js_1 = require("../authentication/mariadb-api-key-store.js");
|
|
142
|
+
const core_1 = require("@astram/core");
|
|
135
143
|
const session_token_binder_js_1 = require("../request-auth/session-token-binder.js");
|
|
136
144
|
/**
|
|
137
145
|
* The injected HMAC-SHA1 primitive the core TOTP verifier needs (Core is zero-dep
|
|
@@ -236,14 +244,53 @@ function makeCredentialResolverProvider(override) {
|
|
|
236
244
|
? { provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, useValue: override }
|
|
237
245
|
: {
|
|
238
246
|
provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
// #7: the default resolver folds the principal's CONFIRMED MFA devices into a
|
|
248
|
+
// `totp` required factor, so enrolled MFA is enforced at every password login.
|
|
249
|
+
useFactory: (pool, devices) => new mariadb_credential_resolver_js_1.MariaDbCredentialResolver(pool, devices),
|
|
250
|
+
inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_1.SHIELD_MFA_DEVICE_STORE],
|
|
241
251
|
};
|
|
242
252
|
}
|
|
243
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* The resolved, validated session lifetimes (ADR-0015) — the ONE source the session
|
|
255
|
+
* authority, the token binder and the auth controller read (security hardening #8).
|
|
256
|
+
*/
|
|
257
|
+
const sessionLifetimesProvider = {
|
|
258
|
+
provide: index_js_1.SHIELD_SESSION_LIFETIMES,
|
|
259
|
+
useFactory: (config) => (0, core_1.resolveLifetimes)(config.lifetimes),
|
|
260
|
+
inject: [config_module_js_1.APP_CONFIG],
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Durable session record store (MariaDB adapter, injected with the live pool), decorated
|
|
264
|
+
* by {@link HotRevokingSessionRecordStore} (M-A): a durable lineage / session revoke is
|
|
265
|
+
* followed by a Redis unbind, so a reuse-killed session's live access token dies on its
|
|
266
|
+
* next request (the request path stays Redis-only).
|
|
267
|
+
*/
|
|
244
268
|
const sessionRecordStoreProvider = {
|
|
245
269
|
provide: index_js_1.SHIELD_SESSION_RECORD_STORE,
|
|
246
|
-
useFactory: (pool) =>
|
|
270
|
+
useFactory: (pool, binder) => {
|
|
271
|
+
const durable = new index_js_1.MariaDbSessionRecordStore(pool);
|
|
272
|
+
return new index_js_1.HotRevokingSessionRecordStore(durable, binder, durable);
|
|
273
|
+
},
|
|
274
|
+
inject: [tokens_js_1.SHIELD_DB_POOL, tokens_js_2.SHIELD_SESSION_TOKEN_BINDER],
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* The Redis permission-version cache (ADR-0035, GAPS #6): gate 5 reads the tenant's version from
|
|
278
|
+
* Redis (one GET) instead of MariaDB, and identity's bumper publishes to it after commit. Exported
|
|
279
|
+
* so the framework-built decision authority (ShieldModule) and the identity bumper inject it.
|
|
280
|
+
*/
|
|
281
|
+
const permissionVersionCacheProvider = {
|
|
282
|
+
provide: tokens_js_1.SHIELD_PERMISSION_VERSION_CACHE,
|
|
283
|
+
useFactory: (redis) => new permission_version_cache_js_1.RedisPermissionVersionCache(redis),
|
|
284
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* The durable session-restriction store (Phase 3B review P1): the recovery-login re-enroll marker
|
|
288
|
+
* on `shield_identity.session` + the account status the refresh rebuild checks. Exported so the
|
|
289
|
+
* account controller (device confirm) clears the marker.
|
|
290
|
+
*/
|
|
291
|
+
const sessionRestrictionStoreProvider = {
|
|
292
|
+
provide: tokens_js_1.SHIELD_SESSION_RESTRICTIONS,
|
|
293
|
+
useFactory: (pool) => new session_restriction_store_js_1.MariaDbSessionRestrictionStore(pool),
|
|
247
294
|
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
248
295
|
};
|
|
249
296
|
/** Hot session store (Redis adapter, injected with the live client). */
|
|
@@ -260,12 +307,64 @@ const hotSessionStoreProvider = {
|
|
|
260
307
|
*/
|
|
261
308
|
const sessionTokenBinderProvider = {
|
|
262
309
|
provide: tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
|
|
263
|
-
|
|
310
|
+
// #8: the session-claims (liveness) entry lives for the refresh idle window, and the
|
|
311
|
+
// previous access token keeps a 30 s grace after a rotation (plan D7).
|
|
312
|
+
useFactory: (redis, lifetimes) => new session_token_binder_js_1.SessionTokenBinder(redis, {
|
|
313
|
+
sessionClaimsTtlMs: lifetimes.refreshIdleMs,
|
|
314
|
+
rotateGraceMs: index_js_1.ROTATE_GRACE_MS,
|
|
315
|
+
}),
|
|
316
|
+
inject: [tokens_js_1.SHIELD_REDIS, index_js_1.SHIELD_SESSION_LIFETIMES],
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* The pending-MFA challenge store (M6): Redis-backed, so `/auth/mfa` completes on any
|
|
320
|
+
* instance, and it holds the password step's RESULT (principal + hint + credential
|
|
321
|
+
* fingerprint) — never the password. Bound under the {@link PendingMfaStore} class token
|
|
322
|
+
* the controller injects.
|
|
323
|
+
*/
|
|
324
|
+
const pendingMfaStoreProvider = {
|
|
325
|
+
provide: pending_mfa_store_js_1.PendingMfaStore,
|
|
326
|
+
useFactory: (redis) => new pending_mfa_store_js_1.RedisPendingMfaStore(redis),
|
|
264
327
|
inject: [tokens_js_1.SHIELD_REDIS],
|
|
265
328
|
};
|
|
329
|
+
/**
|
|
330
|
+
* The TOTP-step limiter (#9) — 5 failures / 15-min window, keyed `mfa:pid:<principalId>`.
|
|
331
|
+
* Checked before the factor is verified; fails open on a Redis outage like every limiter
|
|
332
|
+
* (the code itself still gates — and its replay guard fails closed).
|
|
333
|
+
*/
|
|
334
|
+
const mfaLimiterProvider = {
|
|
335
|
+
provide: tokens_js_1.SHIELD_MFA_LIMITER,
|
|
336
|
+
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 5, windowMs: 15 * 60 * 1000 }),
|
|
337
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* The service-account API-key authenticator (ADR-0033) — resolves an `Authorization: Bearer ask_…`
|
|
341
|
+
* into a `service_account` Principal. Built HERE (in the guard's own AuthModule) over the live
|
|
342
|
+
* {@link SHIELD_DB_POOL} + the framework Argon2id hasher, and EXPORTED so the `APP_GUARD` (in this
|
|
343
|
+
* same module tree) injects it — the SAME reason the {@link sessionTokenBinderProvider} lives here.
|
|
344
|
+
*
|
|
345
|
+
* WHY HERE, not in the consumer's identity module: the enforcement guard is provided by ShieldModule;
|
|
346
|
+
* a token provided in a later, sibling feature module is NOT visible to it (that DI boundary made an
|
|
347
|
+
* identity-module-provided authenticator resolve to `undefined` → an `ask_…` bearer 401'd). Providing
|
|
348
|
+
* it alongside the guard (like the binder) is what makes the machine auth path work out-of-the-box
|
|
349
|
+
* under `auth: true`. The store reads `shield_authz.service_account_key` (created by the framework
|
|
350
|
+
* migration); the account admin surface lives in @astram/identity, but AUTHENTICATION is the guard's.
|
|
351
|
+
*/
|
|
352
|
+
const serviceAuthenticatorProvider = {
|
|
353
|
+
provide: tokens_js_2.SHIELD_SERVICE_AUTHENTICATOR,
|
|
354
|
+
useFactory: (pool) => (0, core_1.createServiceAuthenticator)({
|
|
355
|
+
store: new mariadb_api_key_store_js_1.MariaDbApiKeyStore(pool),
|
|
356
|
+
hasher: new argon2id_password_hasher_js_1.Argon2idPasswordHasher(),
|
|
357
|
+
}),
|
|
358
|
+
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
359
|
+
};
|
|
360
|
+
/** Boot-time warnings from the auth composition (dev email logger, disabled email, …). */
|
|
361
|
+
const authBootLogger = new common_1.Logger('ShieldAuth');
|
|
266
362
|
/**
|
|
267
363
|
* The password-login rate limiter (spec §5 / H1) — a Redis-injected
|
|
268
|
-
* {@link AttemptLimiter}, 10 failures / 15-min window, keyed
|
|
364
|
+
* {@link AttemptLimiter}, 10 failures / 15-min window, keyed
|
|
365
|
+
* per ACCOUNT, `login:pid:<principalId>` (security hardening #5 / review F1: every spelling
|
|
366
|
+
* the DB collation maps to one account shares one budget; an unknown hint falls back to
|
|
367
|
+
* `login:<sha256(canonical hint)>`; no PII in the Redis key). Same
|
|
269
368
|
* live-Redis provider pattern as {@link sessionTokenBinderProvider} (a `useFactory`
|
|
270
369
|
* injecting {@link SHIELD_REDIS}); provided only under `auth: true`. Fail-open on a
|
|
271
370
|
* Redis outage is the limiter's own behavior (never locks out on a blip).
|
|
@@ -275,6 +374,22 @@ const loginLimiterProvider = {
|
|
|
275
374
|
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 10, windowMs: 15 * 60 * 1000 }),
|
|
276
375
|
inject: [tokens_js_1.SHIELD_REDIS],
|
|
277
376
|
};
|
|
377
|
+
/**
|
|
378
|
+
* The per-client-IP password-login FAILURE limiter (security hardening #5, plan D3) — a
|
|
379
|
+
* Redis-injected {@link AttemptLimiter} keyed `login:ip:<req.ip>`, default 50 failures /
|
|
380
|
+
* 15-min window, configurable through `AppConfig.loginIpLimit` (env
|
|
381
|
+
* `LOGIN_IP_MAX_FAILURES` / `LOGIN_IP_WINDOW_MS`). Stops password spraying across many
|
|
382
|
+
* distinct hints from one client. Failures only; never reset on success. Fails open on
|
|
383
|
+
* a Redis outage like every limiter. ENFORCED ONLY when the app set Express `trust proxy`
|
|
384
|
+
* explicitly (0 = no proxy, else the hop count); otherwise {@link TrustProxyBootCheck}
|
|
385
|
+
* switches it OFF at boot with a warning, because every client behind a proxy would share
|
|
386
|
+
* one IP and 50 failures would lock everyone out.
|
|
387
|
+
*/
|
|
388
|
+
const loginIpLimiterProvider = {
|
|
389
|
+
provide: tokens_js_1.SHIELD_LOGIN_IP_LIMITER,
|
|
390
|
+
useFactory: (redis, config) => new attempt_limiter_js_1.AttemptLimiter(redis, config.loginIpLimit ?? app_config_js_1.DEFAULT_LOGIN_IP_LIMIT),
|
|
391
|
+
inject: [tokens_js_1.SHIELD_REDIS, config_module_js_1.APP_CONFIG],
|
|
392
|
+
};
|
|
278
393
|
/**
|
|
279
394
|
* The recovery-code rate limiter (spec §5) — a Redis-injected {@link AttemptLimiter},
|
|
280
395
|
* 5 failures / 15-min window, keyed `recovery:<principalId>` (a lower ceiling than
|
|
@@ -289,7 +404,8 @@ const recoveryLimiterProvider = {
|
|
|
289
404
|
};
|
|
290
405
|
/**
|
|
291
406
|
* The forgot-password rate limiter (feature #141) — a Redis-injected
|
|
292
|
-
* {@link AttemptLimiter}, keyed BY CLIENT IP (`forgot:ip:<ip>`, 5 per 15-min window
|
|
407
|
+
* {@link AttemptLimiter}, keyed BY CLIENT IP (`forgot:ip:<ip>`, 5 per 15-min window;
|
|
408
|
+
* enforced only with an explicit Express `trust proxy`, see {@link TrustProxyBootCheck})
|
|
293
409
|
* AND, separately, per-hint (`forgot:<hint>`, provided as {@link forgotHintLimiterProvider}).
|
|
294
410
|
* NOTE the "attempt" semantics here differ from login: forgot-password is ALWAYS 204
|
|
295
411
|
* (returning 429 would itself be an enumeration signal), so this limiter counts every
|
|
@@ -307,7 +423,7 @@ const forgotLimiterProvider = {
|
|
|
307
423
|
};
|
|
308
424
|
/**
|
|
309
425
|
* The forgot-password PER-HINT rate limiter (feature #141) — a Redis-injected
|
|
310
|
-
* {@link AttemptLimiter}, keyed
|
|
426
|
+
* {@link AttemptLimiter}, keyed per account (`forgot:pid:<principalId>`, unknown → `forgot:<sha256(canonical)>`), 3 per 60-min
|
|
311
427
|
* window). This caps how often ANY caller (across IPs) can trigger a reset email for
|
|
312
428
|
* ONE account, so a single victim's inbox can't be email-bombed even from a rotating
|
|
313
429
|
* IP pool. A GENEROUS per-hint ceiling (a real user rarely needs >3 reset emails/hour)
|
|
@@ -321,7 +437,8 @@ const forgotHintLimiterProvider = {
|
|
|
321
437
|
};
|
|
322
438
|
/**
|
|
323
439
|
* The reset-password rate limiter (feature #141) — a Redis-injected
|
|
324
|
-
* {@link AttemptLimiter}, keyed by CLIENT IP (`reset:ip:<ip>`, 10 per 15-min window
|
|
440
|
+
* {@link AttemptLimiter}, keyed by CLIENT IP (`reset:ip:<ip>`, 10 per 15-min window;
|
|
441
|
+
* enforced only with an explicit Express `trust proxy`, see {@link TrustProxyBootCheck}).
|
|
325
442
|
* Reset is token-gated (a 256-bit single-use token), so this is DoS/abuse throttling,
|
|
326
443
|
* NOT anti-brute-force — and unlike forgot it MAY return a generic 429 on lockout (a
|
|
327
444
|
* token-gated endpoint is not enumeration-sensitive). `recordFailure` on a bad/invalid
|
|
@@ -359,7 +476,7 @@ const clockProvider = {
|
|
|
359
476
|
const sessionAuthorityProvider = {
|
|
360
477
|
provide: tokens_js_1.SHIELD_SESSION_AUTHORITY,
|
|
361
478
|
// async: loads the core factory lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
362
|
-
useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock,
|
|
479
|
+
useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes) => {
|
|
363
480
|
const { createSessionAuthority } = await loadCore();
|
|
364
481
|
return createSessionAuthority({
|
|
365
482
|
recordStore,
|
|
@@ -367,7 +484,7 @@ const sessionAuthorityProvider = {
|
|
|
367
484
|
tokenGenerator,
|
|
368
485
|
refreshHasher,
|
|
369
486
|
clock,
|
|
370
|
-
lifetimes
|
|
487
|
+
lifetimes,
|
|
371
488
|
});
|
|
372
489
|
},
|
|
373
490
|
inject: [
|
|
@@ -376,7 +493,7 @@ const sessionAuthorityProvider = {
|
|
|
376
493
|
index_js_1.SHIELD_TOKEN_GENERATOR,
|
|
377
494
|
index_js_1.SHIELD_REFRESH_HASHER,
|
|
378
495
|
index_js_1.SHIELD_CLOCK,
|
|
379
|
-
|
|
496
|
+
index_js_1.SHIELD_SESSION_LIFETIMES,
|
|
380
497
|
],
|
|
381
498
|
};
|
|
382
499
|
/**
|
|
@@ -388,18 +505,28 @@ const sessionAuthorityProvider = {
|
|
|
388
505
|
* The override reaches this async factory via `inject` (not a captured closure) so
|
|
389
506
|
* the default binding stays exactly as before when no override is plugged.
|
|
390
507
|
*/
|
|
508
|
+
/**
|
|
509
|
+
* The LOGIN `totp` verifier ({@link SHIELD_LOGIN_TOTP_VERIFIER}) — ALWAYS the
|
|
510
|
+
* OR-across-devices wrapper: it delegates to the resolved base (the consumer
|
|
511
|
+
* `totpOverride` when supplied, else the framework's single-secret RFC-6238 verifier) per
|
|
512
|
+
* device secret, so a principal's multiple confirmed devices OR-match, and it claims each
|
|
513
|
+
* accepted code in Redis (M-B replay guard, fail closed). ONE instance, shared by the
|
|
514
|
+
* authenticator and the MFA step-up.
|
|
515
|
+
*/
|
|
516
|
+
const loginTotpVerifierProvider = {
|
|
517
|
+
provide: tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER,
|
|
518
|
+
useFactory: async (totpOverride, mfaDeviceStore, redis) => {
|
|
519
|
+
const { createTotpFactorVerifier } = await loadCore();
|
|
520
|
+
const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
|
|
521
|
+
return new mariadb_or_totp_verifier_js_1.OrTotpVerifier(base, mfaDeviceStore, redis);
|
|
522
|
+
},
|
|
523
|
+
inject: [tokens_js_1.SHIELD_TOTP_VERIFIER_OVERRIDE, tokens_js_1.SHIELD_MFA_DEVICE_STORE, tokens_js_1.SHIELD_REDIS],
|
|
524
|
+
};
|
|
391
525
|
const authenticatorProvider = {
|
|
392
526
|
provide: tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
393
527
|
// async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
394
|
-
useFactory: async (hasher, resolver,
|
|
395
|
-
const { createAuthenticator, createIdentityBroker
|
|
396
|
-
// The login totp verifier is ALWAYS the OR-across-devices wrapper: it delegates
|
|
397
|
-
// to the resolved base (the consumer `totpOverride` when supplied, else the
|
|
398
|
-
// framework's single-secret RFC-6238 verifier) per device secret, so a
|
|
399
|
-
// principal's multiple confirmed devices OR-match. A consumer override still wins
|
|
400
|
-
// (it's the base inside the OR-wrap); multi-device works by default.
|
|
401
|
-
const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
|
|
402
|
-
const totp = new mariadb_or_totp_verifier_js_1.OrTotpVerifier(base, mfaDeviceStore);
|
|
528
|
+
useFactory: async (hasher, resolver, totp, rootIds) => {
|
|
529
|
+
const { createAuthenticator, createIdentityBroker } = await loadCore();
|
|
403
530
|
return createAuthenticator({
|
|
404
531
|
hasher,
|
|
405
532
|
resolver,
|
|
@@ -414,11 +541,25 @@ const authenticatorProvider = {
|
|
|
414
541
|
inject: [
|
|
415
542
|
argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
416
543
|
tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
417
|
-
tokens_js_1.
|
|
544
|
+
tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER,
|
|
418
545
|
tokens_js_1.SHIELD_ROOT_IDS,
|
|
419
|
-
tokens_js_1.SHIELD_MFA_DEVICE_STORE,
|
|
420
546
|
],
|
|
421
547
|
};
|
|
548
|
+
/**
|
|
549
|
+
* The MFA step-up (M6) behind `/auth/mfa`'s TOTP branch: verifies ONLY the second factor
|
|
550
|
+
* for the principal the password step recorded, through the SAME resolver, the SAME login
|
|
551
|
+
* TOTP verifier (replay guard included) and an identity broker with the SAME root
|
|
552
|
+
* allowlist as {@link authenticatorProvider} — so the minted principal is identical to the
|
|
553
|
+
* one a single-step login would produce. No password is held between the two requests.
|
|
554
|
+
*/
|
|
555
|
+
const mfaStepUpProvider = {
|
|
556
|
+
provide: tokens_js_1.SHIELD_MFA_STEP_UP,
|
|
557
|
+
useFactory: async (resolver, totp, rootIds) => {
|
|
558
|
+
const { createIdentityBroker } = await loadCore();
|
|
559
|
+
return new mfa_step_up_js_1.CredentialMfaStepUp(resolver, [totp], createIdentityBroker({ internalRootIds: rootIds ?? [] }));
|
|
560
|
+
},
|
|
561
|
+
inject: [tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER, tokens_js_1.SHIELD_ROOT_IDS],
|
|
562
|
+
};
|
|
422
563
|
/**
|
|
423
564
|
* Bind the OPTIONAL login `totp` verifier override (the Phase-2 multi-device seam).
|
|
424
565
|
* Same value-provider pattern as the other seams: the supplied verifier when present,
|
|
@@ -503,10 +644,48 @@ function makeResetTokenStoreProvider(override) {
|
|
|
503
644
|
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
504
645
|
};
|
|
505
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Bind the {@link EmailSender} (security hardening #6):
|
|
649
|
+
* - a supplied sender → used verbatim (any environment);
|
|
650
|
+
* - `'disabled'` → {@link NoopEmailSender} + one startup warning (forgot-password stays 204;
|
|
651
|
+
* nothing is delivered or printed);
|
|
652
|
+
* - omitted → the dev {@link LogEmailSender} ONLY when the RAW `NODE_ENV` is explicitly
|
|
653
|
+
* `development` or `test` (+ one startup warning, since it prints live reset links);
|
|
654
|
+
* anything else, INCLUDING an unset `NODE_ENV`, THROWS at composition (boot fails closed).
|
|
655
|
+
* `NODE_ENV` is read when the factory runs (module compile), not at `forRoot()` time.
|
|
656
|
+
*/
|
|
506
657
|
function makeEmailSenderProvider(override) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
658
|
+
if (override === 'disabled') {
|
|
659
|
+
return {
|
|
660
|
+
provide: tokens_js_1.SHIELD_EMAIL_SENDER,
|
|
661
|
+
useFactory: () => {
|
|
662
|
+
authBootLogger.warn("emailSender: 'disabled' — password-reset emails are NOT delivered " +
|
|
663
|
+
'(forgot-password still answers 204). Supply a real EmailSender to enable resets.');
|
|
664
|
+
return new noop_email_sender_js_1.NoopEmailSender();
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
if (override)
|
|
669
|
+
return { provide: tokens_js_1.SHIELD_EMAIL_SENDER, useValue: override };
|
|
670
|
+
return {
|
|
671
|
+
provide: tokens_js_1.SHIELD_EMAIL_SENDER,
|
|
672
|
+
useFactory: () => resolveDefaultEmailSender(process.env.NODE_ENV),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* The no-override default sender decision (pure apart from the warning). Exported for
|
|
677
|
+
* the unit test only — not part of the package's public surface.
|
|
678
|
+
*/
|
|
679
|
+
function resolveDefaultEmailSender(rawNodeEnv) {
|
|
680
|
+
if (!(0, app_config_js_1.isDevOrTestNodeEnv)(rawNodeEnv)) {
|
|
681
|
+
throw new Error("Astram auth composition: auth:true requires `emailSender` (or `emailSender: 'disabled'`) " +
|
|
682
|
+
`when NODE_ENV is not 'development' or 'test' (got: ${rawNodeEnv === undefined ? 'unset' : `'${rawNodeEnv}'`}). ` +
|
|
683
|
+
'The dev LogEmailSender prints live password-reset links to stdout, so it is only ' +
|
|
684
|
+
'used when NODE_ENV is explicitly development/test.');
|
|
685
|
+
}
|
|
686
|
+
authBootLogger.warn(`Using the dev LogEmailSender (NODE_ENV=${rawNodeEnv}): password-reset emails are printed ` +
|
|
687
|
+
'to stdout, reset links included. Never run like this in production — supply an emailSender.');
|
|
688
|
+
return new log_email_sender_js_1.LogEmailSender();
|
|
510
689
|
}
|
|
511
690
|
function makeRecoveryCodeStoreProvider(override) {
|
|
512
691
|
return override
|
|
@@ -667,25 +846,47 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
667
846
|
// The reset-link base URL value the forgot-password handler composes into the
|
|
668
847
|
// emailed link (framework-owned URL composition; EmailSender stays dumb).
|
|
669
848
|
resetLinkBaseUrlProvider,
|
|
849
|
+
// The resolved session lifetimes (#8) — one source for authority, binder, controller.
|
|
850
|
+
sessionLifetimesProvider,
|
|
670
851
|
sessionRecordStoreProvider,
|
|
852
|
+
// ADR-0035: the Redis permission-version cache (gate 5 + the identity bumper).
|
|
853
|
+
permissionVersionCacheProvider,
|
|
854
|
+
// Phase 3B review P1: the durable re-enroll marker + account status for the rebuild.
|
|
855
|
+
sessionRestrictionStoreProvider,
|
|
671
856
|
hotSessionStoreProvider,
|
|
672
857
|
tokenGeneratorProvider,
|
|
673
858
|
refreshHasherProvider,
|
|
674
859
|
clockProvider,
|
|
675
860
|
sessionAuthorityProvider,
|
|
861
|
+
// The login TOTP verifier (OR across devices + Redis replay guard, M-B), shared by
|
|
862
|
+
// the authenticator and the MFA step-up.
|
|
863
|
+
loginTotpVerifierProvider,
|
|
676
864
|
authenticatorProvider,
|
|
677
|
-
// The
|
|
865
|
+
// The MFA step-up (M6): `/auth/mfa` verifies only the second factor.
|
|
866
|
+
mfaStepUpProvider,
|
|
867
|
+
// The single-secret TOTP verifier for account MFA enroll/disable.
|
|
678
868
|
totpVerifierProvider,
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
|
|
869
|
+
// The server-side, single-use, TTL'd challenge store backing the login→mfa
|
|
870
|
+
// handoff — Redis-backed, never holds the password (M6).
|
|
871
|
+
pendingMfaStoreProvider,
|
|
682
872
|
// The request-auth token binder (access-token → principal claims).
|
|
683
873
|
sessionTokenBinderProvider,
|
|
874
|
+
// The service-account API-key authenticator (ADR-0033) — `ask_…` bearer → service_account
|
|
875
|
+
// Principal. Provided here (with the guard) so the APP_GUARD sees it; exported below.
|
|
876
|
+
serviceAuthenticatorProvider,
|
|
684
877
|
// The anti-brute-force rate limiters (spec §5): login (H1) + recovery, each
|
|
685
878
|
// a Redis-injected AttemptLimiter with its own threshold. Injected into
|
|
686
879
|
// AuthController (@Optional()).
|
|
687
880
|
loginLimiterProvider,
|
|
881
|
+
// #5: the per-client-IP login failure limiter (D3: 50 / 15 min, configurable) +
|
|
882
|
+
// the boot-time `trust proxy` check that enables ALL per-IP limiters (login,
|
|
883
|
+
// forgot, reset) on an explicit trust proxy, or switches them OFF with a warning
|
|
884
|
+
// (unset → req.ip would be the shared proxy IP).
|
|
885
|
+
loginIpLimiterProvider,
|
|
886
|
+
trust_proxy_check_js_1.TrustProxyBootCheck,
|
|
688
887
|
recoveryLimiterProvider,
|
|
888
|
+
// #9: the TOTP-step limiter (5 / 15 min per account, `mfa:pid:<principalId>`).
|
|
889
|
+
mfaLimiterProvider,
|
|
689
890
|
// The forgot/reset-password limiters (feature #141): forgot keyed by client IP
|
|
690
891
|
// + per-hint (request-rate caps → silently skip the send when exceeded, stays
|
|
691
892
|
// 204); reset keyed by client IP (may 429 on lockout — token-gated, not enum-
|
|
@@ -701,6 +902,9 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
701
902
|
argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
702
903
|
// Exported so the (app-wide) enforcement guard can inject it.
|
|
703
904
|
tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
|
|
905
|
+
// Exported so the (app-wide) enforcement guard injects it → the `ask_…` machine auth path
|
|
906
|
+
// (ADR-0033) works out-of-the-box under `auth: true`.
|
|
907
|
+
tokens_js_2.SHIELD_SERVICE_AUTHENTICATOR,
|
|
704
908
|
// The account/credential-surface seams — exported so the consumer's impls
|
|
705
909
|
// and the account/auth controllers see them app-wide through @Global Shield.
|
|
706
910
|
tokens_js_1.SHIELD_CREDENTIAL_WRITER,
|
|
@@ -719,6 +923,13 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
719
923
|
tokens_js_1.SHIELD_TOTP_VERIFIER,
|
|
720
924
|
// The eager session-facts projector (consumed by AuthController).
|
|
721
925
|
tokens_js_1.SHIELD_SESSION_FACTS_PROJECTOR,
|
|
926
|
+
// The scope-subtree resolver — exported so the (app-wide) enforcement guard can re-derive
|
|
927
|
+
// a session's subtree when gate 5 reloads at a newer permission version (Phase 1C).
|
|
928
|
+
tokens_js_1.SHIELD_SCOPE_SUBTREE_RESOLVER,
|
|
929
|
+
// ADR-0035: the Redis permission-version cache — the framework-built decision authority
|
|
930
|
+
// reads it (gate 5) and `@astram/identity`'s bumper publishes to it after commit.
|
|
931
|
+
tokens_js_1.SHIELD_PERMISSION_VERSION_CACHE,
|
|
932
|
+
tokens_js_1.SHIELD_SESSION_RESTRICTIONS,
|
|
722
933
|
],
|
|
723
934
|
};
|
|
724
935
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { type OnModuleDestroy } from '@nestjs/common';
|
|
25
25
|
import { type Pool } from 'mariadb';
|
|
26
26
|
import Redis from 'ioredis';
|
|
27
|
-
import type
|
|
27
|
+
import { type AppConfig } from '../config/app-config.js';
|
|
28
28
|
/**
|
|
29
29
|
* Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
|
|
30
30
|
* {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"connections.d.ts","sourceRoot":"","sources":["../../src/auth/connections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAcpD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAc1D;AAED;;;;;;GAMG;AACH,qBACa,mBAAoB,YAAW,eAAe;IAEvD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK;IAGzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAavC"}
|
package/dist/auth/connections.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.createRedisClient = createRedisClient;
|
|
|
41
41
|
const common_1 = require("@nestjs/common");
|
|
42
42
|
const mariadb_1 = __importDefault(require("mariadb"));
|
|
43
43
|
const ioredis_1 = __importDefault(require("ioredis"));
|
|
44
|
+
const app_config_js_1 = require("../config/app-config.js");
|
|
44
45
|
/**
|
|
45
46
|
* Create the ONE live `mariadb` {@link Pool} for the `shield_core` account from
|
|
46
47
|
* {@link AppConfig}. The `connectionLimit` comes from the ADR-0018 budget
|
|
@@ -75,9 +76,19 @@ function createDbPool(config) {
|
|
|
75
76
|
* `unavailable` hot-store outcome (fail-closed), never a silent `absent`.
|
|
76
77
|
*/
|
|
77
78
|
function createRedisClient(config) {
|
|
78
|
-
//
|
|
79
|
-
// command
|
|
80
|
-
|
|
79
|
+
// BOUNDED (GAPS #7, security hardening Phase 3). Previously `maxRetriesPerRequest: null`
|
|
80
|
+
// queued every command while Redis was down, so a classified request HUNG until Redis came
|
|
81
|
+
// back instead of failing closed (ADR-0013: "if Redis cannot answer, the request is denied").
|
|
82
|
+
// Now each command fails after `commandTimeout` (default 2 s, `REDIS_COMMAND_TIMEOUT_MS`) or
|
|
83
|
+
// after one reconnect attempt; every caller treats a Redis error as its fail-closed outcome
|
|
84
|
+
// (401 / 503), except the rate limiters, which fail OPEN by design (never a lockout blip).
|
|
85
|
+
// Reconnects keep ioredis's default strategy. Nothing in the framework relies on queued
|
|
86
|
+
// commands (no pub/sub, no blocking reads, no MULTI/pipeline).
|
|
87
|
+
return new ioredis_1.default(config.redis.url, {
|
|
88
|
+
maxRetriesPerRequest: 1,
|
|
89
|
+
commandTimeout: config.redis.commandTimeoutMs ?? app_config_js_1.DEFAULT_REDIS_COMMAND_TIMEOUT_MS,
|
|
90
|
+
lazyConnect: false,
|
|
91
|
+
});
|
|
81
92
|
}
|
|
82
93
|
/**
|
|
83
94
|
* Lifecycle-aware holder that owns the pool + redis handles and closes them on
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework-DEFAULT session-facts projector (ADR-0035, GAPS #6). Without a projector, a session
|
|
3
|
+
* carries no `cachedFacts` and gate 5 re-reads the grants from the DB on every request. When the app
|
|
4
|
+
* hands ShieldModule its `recordAuthorization` (and no explicit `sessionFactsProjector`), the facts
|
|
5
|
+
* the decision authority uses are the right ones to cache: this projector delegates to that SAME
|
|
6
|
+
* `PermissionFactsPort.loadCoarseGrants` (one definition of a principal's grants and live roles).
|
|
7
|
+
*
|
|
8
|
+
* DEFERRED: `AuthModule` is composed statically, but `recordAuthorization` may be resolved later
|
|
9
|
+
* (`forRootAsync`), and the port is only known when the decision authority is built. ShieldModule
|
|
10
|
+
* creates one instance per registration, hands it to `AuthModule`, and attaches the port when it
|
|
11
|
+
* builds the authority. Until attached it is INACTIVE: the auth controller then behaves exactly as
|
|
12
|
+
* with no projector (nothing cached at login, the existing cache carried forward at refresh).
|
|
13
|
+
*
|
|
14
|
+
* Free of runtime `@astram/core` imports (type-only).
|
|
15
|
+
*/
|
|
16
|
+
import type { CoarseGrants, PermissionFactsPort } from '@astram/core';
|
|
17
|
+
import type { SessionFactsProjector } from './ports/session-facts-projector.js';
|
|
18
|
+
export declare class DeferredSessionFactsProjector implements SessionFactsProjector {
|
|
19
|
+
private source;
|
|
20
|
+
/** Attach the decision authority's facts port (once; a second attach is ignored). */
|
|
21
|
+
attach(source: Pick<PermissionFactsPort, 'loadCoarseGrants'>): void;
|
|
22
|
+
/** Whether a port was attached (inactive ⇒ the controller treats it as "no projector"). */
|
|
23
|
+
isActive(): boolean;
|
|
24
|
+
project(principalId: string, tenantId: string): Promise<CoarseGrants | null>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=default-session-facts-projector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-session-facts-projector.d.ts","sourceRoot":"","sources":["../../src/auth/default-session-facts-projector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAEhF,qBAAa,6BAA8B,YAAW,qBAAqB;IACzE,OAAO,CAAC,MAAM,CAA4D;IAE1E,qFAAqF;IACrF,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,GAAG,IAAI;IAInE,2FAA2F;IAC3F,QAAQ,IAAI,OAAO;IAIb,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;CAInF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeferredSessionFactsProjector = void 0;
|
|
4
|
+
class DeferredSessionFactsProjector {
|
|
5
|
+
source;
|
|
6
|
+
/** Attach the decision authority's facts port (once; a second attach is ignored). */
|
|
7
|
+
attach(source) {
|
|
8
|
+
if (this.source === undefined)
|
|
9
|
+
this.source = source;
|
|
10
|
+
}
|
|
11
|
+
/** Whether a port was attached (inactive ⇒ the controller treats it as "no projector"). */
|
|
12
|
+
isActive() {
|
|
13
|
+
return this.source !== undefined;
|
|
14
|
+
}
|
|
15
|
+
async project(principalId, tenantId) {
|
|
16
|
+
if (this.source === undefined)
|
|
17
|
+
return null;
|
|
18
|
+
return this.source.loadCoarseGrants(principalId, tenantId);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.DeferredSessionFactsProjector = DeferredSessionFactsProjector;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Credential, CredentialResolver, FactorVerifier, IdentityBroker, Principal, ResolvedCredential } from '@astram/core';
|
|
2
|
+
import type { PendingMfaChallenge } from './pending-mfa-store.js';
|
|
3
|
+
/** The step-up outcome the controller maps to 200 / 401 / 503. */
|
|
4
|
+
export type MfaStepUpResult = {
|
|
5
|
+
readonly outcome: 'authenticated';
|
|
6
|
+
readonly principal: Principal;
|
|
7
|
+
} | {
|
|
8
|
+
readonly outcome: 'rejected';
|
|
9
|
+
} | {
|
|
10
|
+
readonly outcome: 'unavailable';
|
|
11
|
+
};
|
|
12
|
+
/** The seam the controller injects under `SHIELD_MFA_STEP_UP`. */
|
|
13
|
+
export interface MfaStepUp {
|
|
14
|
+
verify(challenge: PendingMfaChallenge, credential: Credential): Promise<MfaStepUpResult>;
|
|
15
|
+
}
|
|
16
|
+
/** SHA-256 of the stored credential hash — binds a challenge to the credential state. */
|
|
17
|
+
export declare function credentialFingerprint(resolved: Pick<ResolvedCredential, 'passwordHash'>): string;
|
|
18
|
+
/**
|
|
19
|
+
* `true` iff `resolved` is still the credential the challenge was issued for: same
|
|
20
|
+
* principal, same stored credential hash. Shared by the TOTP step-up and the
|
|
21
|
+
* recovery-code branch.
|
|
22
|
+
*/
|
|
23
|
+
export declare function challengeMatches(resolved: ResolvedCredential, challenge: PendingMfaChallenge): boolean;
|
|
24
|
+
/** The framework-default {@link MfaStepUp} over the injected core ports. */
|
|
25
|
+
export declare class CredentialMfaStepUp implements MfaStepUp {
|
|
26
|
+
private readonly resolver;
|
|
27
|
+
private readonly broker;
|
|
28
|
+
private readonly verifiers;
|
|
29
|
+
constructor(resolver: Pick<CredentialResolver, 'resolve'>, factorVerifiers: readonly FactorVerifier[], broker: IdentityBroker);
|
|
30
|
+
verify(challenge: PendingMfaChallenge, credential: Credential): Promise<MfaStepUpResult>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=mfa-step-up.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mfa-step-up.d.ts","sourceRoot":"","sources":["../../src/auth/mfa-step-up.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,SAAS,EACT,kBAAkB,EACnB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,kEAAkE;AAClE,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAExC,kEAAkE;AAClE,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,SAAS,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC1F;AAED,yFAAyF;AACzF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC,GAAG,MAAM,CAIhG;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,kBAAkB,EAC5B,SAAS,EAAE,mBAAmB,GAC7B,OAAO,CAKT;AAKD,4EAA4E;AAC5E,qBAAa,mBAAoB,YAAW,SAAS;IAIjD,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;gBAG5C,QAAQ,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,EAC9D,eAAe,EAAE,SAAS,cAAc,EAAE,EACzB,MAAM,EAAE,cAAc;IAUnC,MAAM,CAAC,SAAS,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC;CAqC/F"}
|