@astram/guard 0.7.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 +212 -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 +4 -1
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.js +31 -7
- 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 +36 -0
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +44 -4
- package/dist/encryption/local-kek-crypto.d.ts.map +1 -1
- 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 +29 -2
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +143 -5
- package/dist/identity/auth-facts.controller.d.ts.map +1 -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 +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -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/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/secrets/encrypted-file-secrets.d.ts.map +1 -1
- package/dist/secrets/encrypted-file-secrets.js +9 -3
- 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/package.json +3 -3
- package/dist/auth/account.controller.js.map +0 -1
- package/dist/auth/auth.controller.js.map +0 -1
- package/dist/auth/auth.module.js.map +0 -1
- package/dist/auth/connections.js.map +0 -1
- package/dist/auth/dto.js.map +0 -1
- package/dist/auth/pending-mfa-store.js.map +0 -1
- package/dist/auth/persistence.module.js.map +0 -1
- package/dist/auth/ports/audit-sink.js.map +0 -1
- package/dist/auth/ports/credential-writer.js.map +0 -1
- package/dist/auth/ports/email-sender.js.map +0 -1
- package/dist/auth/ports/membership-checker.js.map +0 -1
- package/dist/auth/ports/mfa-device-store.js.map +0 -1
- package/dist/auth/ports/profile-writer.js.map +0 -1
- package/dist/auth/ports/recovery-code-store.js.map +0 -1
- package/dist/auth/ports/reset-token-store.js.map +0 -1
- package/dist/auth/ports/scope-subtree-resolver.js.map +0 -1
- package/dist/auth/ports/session-facts-projector.js.map +0 -1
- package/dist/auth/ports/session-sweeper.js.map +0 -1
- package/dist/auth/profile-resolver.js.map +0 -1
- package/dist/auth/rate-limit/attempt-limiter.js.map +0 -1
- package/dist/auth/tokens.js.map +0 -1
- package/dist/auth/totp-enrollment.js.map +0 -1
- package/dist/authentication/argon2id-password-hasher.js.map +0 -1
- package/dist/authentication/log-email-sender.js.map +0 -1
- package/dist/authentication/mariadb-credential-resolver.js.map +0 -1
- package/dist/authentication/mariadb-credential-writer.js.map +0 -1
- package/dist/authentication/mariadb-mfa-device-store.js.map +0 -1
- package/dist/authentication/mariadb-or-totp-verifier.js.map +0 -1
- package/dist/authentication/mariadb-profile-resolver.js.map +0 -1
- package/dist/authentication/mariadb-profile-writer.js.map +0 -1
- package/dist/authentication/mariadb-recovery-code-store.js.map +0 -1
- package/dist/authentication/mariadb-reset-token-store.js.map +0 -1
- package/dist/authentication/mariadb-session-sweeper.js.map +0 -1
- package/dist/authz/cross-tenant-grant-writer.js.map +0 -1
- package/dist/authz/list-scope-sql.js.map +0 -1
- package/dist/authz/manifest-operation-authority-adapter.js.map +0 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js.map +0 -1
- package/dist/authz/mariadb-list-scope-adapter.js.map +0 -1
- package/dist/authz/mariadb-policy-store.js.map +0 -1
- package/dist/authz/mariadb-record-rule-adapter.js.map +0 -1
- package/dist/authz/mariadb-record-share-adapter.js.map +0 -1
- package/dist/authz/record-share-cascade.js.map +0 -1
- package/dist/authz/record-share-writer.js.map +0 -1
- package/dist/authz/wire-record-authorization.js.map +0 -1
- package/dist/boot/boot-sequence.js.map +0 -1
- package/dist/boot/boot.module.js.map +0 -1
- package/dist/completeness/catalog-reconciler.js.map +0 -1
- package/dist/completeness/operation-inventory-scanner.js.map +0 -1
- package/dist/completeness/ports/catalog-store.js.map +0 -1
- package/dist/config/app-config.js.map +0 -1
- package/dist/config/config.module.js.map +0 -1
- package/dist/enforcement/classification.decorator.js.map +0 -1
- package/dist/enforcement/core-seam.js.map +0 -1
- package/dist/enforcement/enforcement.module.js.map +0 -1
- package/dist/enforcement/global-enforcement.guard.js.map +0 -1
- package/dist/enforcement/reenroll-allowlist.js.map +0 -1
- package/dist/identity/auth-facts.controller.js.map +0 -1
- package/dist/identity/identity-actions.js.map +0 -1
- package/dist/identity/module-registry.bridge.js.map +0 -1
- package/dist/identity/permission-facts.adapter.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/migrations-dir.js.map +0 -1
- package/dist/module/shield-tokens.js.map +0 -1
- package/dist/module/shield.module.js.map +0 -1
- package/dist/request-auth/request-principal.js.map +0 -1
- package/dist/request-auth/session-facts-als.js.map +0 -1
- package/dist/request-auth/session-token-binder.js.map +0 -1
- package/dist/request-auth/tokens.js.map +0 -1
- package/dist/scope/closure-builder.js.map +0 -1
- package/dist/session/crypto-adapters.js.map +0 -1
- package/dist/session/index.js.map +0 -1
- package/dist/session/mariadb-session-record-store.js.map +0 -1
- package/dist/session/redis-hot-session-store.js.map +0 -1
|
@@ -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,8 +131,12 @@ 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");
|
|
135
141
|
const mariadb_api_key_store_js_1 = require("../authentication/mariadb-api-key-store.js");
|
|
136
142
|
const core_1 = require("@astram/core");
|
|
@@ -238,14 +244,53 @@ function makeCredentialResolverProvider(override) {
|
|
|
238
244
|
? { provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER, useValue: override }
|
|
239
245
|
: {
|
|
240
246
|
provide: tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
241
|
-
|
|
242
|
-
|
|
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],
|
|
243
251
|
};
|
|
244
252
|
}
|
|
245
|
-
/**
|
|
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
|
+
*/
|
|
246
268
|
const sessionRecordStoreProvider = {
|
|
247
269
|
provide: index_js_1.SHIELD_SESSION_RECORD_STORE,
|
|
248
|
-
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),
|
|
249
294
|
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
250
295
|
};
|
|
251
296
|
/** Hot session store (Redis adapter, injected with the live client). */
|
|
@@ -262,7 +307,33 @@ const hotSessionStoreProvider = {
|
|
|
262
307
|
*/
|
|
263
308
|
const sessionTokenBinderProvider = {
|
|
264
309
|
provide: tokens_js_2.SHIELD_SESSION_TOKEN_BINDER,
|
|
265
|
-
|
|
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),
|
|
327
|
+
inject: [tokens_js_1.SHIELD_REDIS],
|
|
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 }),
|
|
266
337
|
inject: [tokens_js_1.SHIELD_REDIS],
|
|
267
338
|
};
|
|
268
339
|
/**
|
|
@@ -286,9 +357,14 @@ const serviceAuthenticatorProvider = {
|
|
|
286
357
|
}),
|
|
287
358
|
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
288
359
|
};
|
|
360
|
+
/** Boot-time warnings from the auth composition (dev email logger, disabled email, …). */
|
|
361
|
+
const authBootLogger = new common_1.Logger('ShieldAuth');
|
|
289
362
|
/**
|
|
290
363
|
* The password-login rate limiter (spec §5 / H1) — a Redis-injected
|
|
291
|
-
* {@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
|
|
292
368
|
* live-Redis provider pattern as {@link sessionTokenBinderProvider} (a `useFactory`
|
|
293
369
|
* injecting {@link SHIELD_REDIS}); provided only under `auth: true`. Fail-open on a
|
|
294
370
|
* Redis outage is the limiter's own behavior (never locks out on a blip).
|
|
@@ -298,6 +374,22 @@ const loginLimiterProvider = {
|
|
|
298
374
|
useFactory: (redis) => new attempt_limiter_js_1.AttemptLimiter(redis, { maxAttempts: 10, windowMs: 15 * 60 * 1000 }),
|
|
299
375
|
inject: [tokens_js_1.SHIELD_REDIS],
|
|
300
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
|
+
};
|
|
301
393
|
/**
|
|
302
394
|
* The recovery-code rate limiter (spec §5) — a Redis-injected {@link AttemptLimiter},
|
|
303
395
|
* 5 failures / 15-min window, keyed `recovery:<principalId>` (a lower ceiling than
|
|
@@ -312,7 +404,8 @@ const recoveryLimiterProvider = {
|
|
|
312
404
|
};
|
|
313
405
|
/**
|
|
314
406
|
* The forgot-password rate limiter (feature #141) — a Redis-injected
|
|
315
|
-
* {@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})
|
|
316
409
|
* AND, separately, per-hint (`forgot:<hint>`, provided as {@link forgotHintLimiterProvider}).
|
|
317
410
|
* NOTE the "attempt" semantics here differ from login: forgot-password is ALWAYS 204
|
|
318
411
|
* (returning 429 would itself be an enumeration signal), so this limiter counts every
|
|
@@ -330,7 +423,7 @@ const forgotLimiterProvider = {
|
|
|
330
423
|
};
|
|
331
424
|
/**
|
|
332
425
|
* The forgot-password PER-HINT rate limiter (feature #141) — a Redis-injected
|
|
333
|
-
* {@link AttemptLimiter}, keyed
|
|
426
|
+
* {@link AttemptLimiter}, keyed per account (`forgot:pid:<principalId>`, unknown → `forgot:<sha256(canonical)>`), 3 per 60-min
|
|
334
427
|
* window). This caps how often ANY caller (across IPs) can trigger a reset email for
|
|
335
428
|
* ONE account, so a single victim's inbox can't be email-bombed even from a rotating
|
|
336
429
|
* IP pool. A GENEROUS per-hint ceiling (a real user rarely needs >3 reset emails/hour)
|
|
@@ -344,7 +437,8 @@ const forgotHintLimiterProvider = {
|
|
|
344
437
|
};
|
|
345
438
|
/**
|
|
346
439
|
* The reset-password rate limiter (feature #141) — a Redis-injected
|
|
347
|
-
* {@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}).
|
|
348
442
|
* Reset is token-gated (a 256-bit single-use token), so this is DoS/abuse throttling,
|
|
349
443
|
* NOT anti-brute-force — and unlike forgot it MAY return a generic 429 on lockout (a
|
|
350
444
|
* token-gated endpoint is not enumeration-sensitive). `recordFailure` on a bad/invalid
|
|
@@ -382,7 +476,7 @@ const clockProvider = {
|
|
|
382
476
|
const sessionAuthorityProvider = {
|
|
383
477
|
provide: tokens_js_1.SHIELD_SESSION_AUTHORITY,
|
|
384
478
|
// async: loads the core factory lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
385
|
-
useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock,
|
|
479
|
+
useFactory: async (recordStore, hotStore, tokenGenerator, refreshHasher, clock, lifetimes) => {
|
|
386
480
|
const { createSessionAuthority } = await loadCore();
|
|
387
481
|
return createSessionAuthority({
|
|
388
482
|
recordStore,
|
|
@@ -390,7 +484,7 @@ const sessionAuthorityProvider = {
|
|
|
390
484
|
tokenGenerator,
|
|
391
485
|
refreshHasher,
|
|
392
486
|
clock,
|
|
393
|
-
lifetimes
|
|
487
|
+
lifetimes,
|
|
394
488
|
});
|
|
395
489
|
},
|
|
396
490
|
inject: [
|
|
@@ -399,7 +493,7 @@ const sessionAuthorityProvider = {
|
|
|
399
493
|
index_js_1.SHIELD_TOKEN_GENERATOR,
|
|
400
494
|
index_js_1.SHIELD_REFRESH_HASHER,
|
|
401
495
|
index_js_1.SHIELD_CLOCK,
|
|
402
|
-
|
|
496
|
+
index_js_1.SHIELD_SESSION_LIFETIMES,
|
|
403
497
|
],
|
|
404
498
|
};
|
|
405
499
|
/**
|
|
@@ -411,18 +505,28 @@ const sessionAuthorityProvider = {
|
|
|
411
505
|
* The override reaches this async factory via `inject` (not a captured closure) so
|
|
412
506
|
* the default binding stays exactly as before when no override is plugged.
|
|
413
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
|
+
};
|
|
414
525
|
const authenticatorProvider = {
|
|
415
526
|
provide: tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
416
527
|
// async: loads the core factories lazily (CJS→ESM, see loadCore()); Nest awaits it.
|
|
417
|
-
useFactory: async (hasher, resolver,
|
|
418
|
-
const { createAuthenticator, createIdentityBroker
|
|
419
|
-
// The login totp verifier is ALWAYS the OR-across-devices wrapper: it delegates
|
|
420
|
-
// to the resolved base (the consumer `totpOverride` when supplied, else the
|
|
421
|
-
// framework's single-secret RFC-6238 verifier) per device secret, so a
|
|
422
|
-
// principal's multiple confirmed devices OR-match. A consumer override still wins
|
|
423
|
-
// (it's the base inside the OR-wrap); multi-device works by default.
|
|
424
|
-
const base = resolveTotpVerifier(totpOverride, createTotpFactorVerifier({ hmacSha1: exports.hmacSha1 }));
|
|
425
|
-
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();
|
|
426
530
|
return createAuthenticator({
|
|
427
531
|
hasher,
|
|
428
532
|
resolver,
|
|
@@ -437,11 +541,25 @@ const authenticatorProvider = {
|
|
|
437
541
|
inject: [
|
|
438
542
|
argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER,
|
|
439
543
|
tokens_js_1.SHIELD_CREDENTIAL_RESOLVER,
|
|
440
|
-
tokens_js_1.
|
|
544
|
+
tokens_js_1.SHIELD_LOGIN_TOTP_VERIFIER,
|
|
441
545
|
tokens_js_1.SHIELD_ROOT_IDS,
|
|
442
|
-
tokens_js_1.SHIELD_MFA_DEVICE_STORE,
|
|
443
546
|
],
|
|
444
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
|
+
};
|
|
445
563
|
/**
|
|
446
564
|
* Bind the OPTIONAL login `totp` verifier override (the Phase-2 multi-device seam).
|
|
447
565
|
* Same value-provider pattern as the other seams: the supplied verifier when present,
|
|
@@ -526,10 +644,48 @@ function makeResetTokenStoreProvider(override) {
|
|
|
526
644
|
inject: [tokens_js_1.SHIELD_DB_POOL],
|
|
527
645
|
};
|
|
528
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
|
+
*/
|
|
529
657
|
function makeEmailSenderProvider(override) {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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();
|
|
533
689
|
}
|
|
534
690
|
function makeRecoveryCodeStoreProvider(override) {
|
|
535
691
|
return override
|
|
@@ -690,18 +846,29 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
690
846
|
// The reset-link base URL value the forgot-password handler composes into the
|
|
691
847
|
// emailed link (framework-owned URL composition; EmailSender stays dumb).
|
|
692
848
|
resetLinkBaseUrlProvider,
|
|
849
|
+
// The resolved session lifetimes (#8) — one source for authority, binder, controller.
|
|
850
|
+
sessionLifetimesProvider,
|
|
693
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,
|
|
694
856
|
hotSessionStoreProvider,
|
|
695
857
|
tokenGeneratorProvider,
|
|
696
858
|
refreshHasherProvider,
|
|
697
859
|
clockProvider,
|
|
698
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,
|
|
699
864
|
authenticatorProvider,
|
|
700
|
-
// 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.
|
|
701
868
|
totpVerifierProvider,
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
|
|
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,
|
|
705
872
|
// The request-auth token binder (access-token → principal claims).
|
|
706
873
|
sessionTokenBinderProvider,
|
|
707
874
|
// The service-account API-key authenticator (ADR-0033) — `ask_…` bearer → service_account
|
|
@@ -711,7 +878,15 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
711
878
|
// a Redis-injected AttemptLimiter with its own threshold. Injected into
|
|
712
879
|
// AuthController (@Optional()).
|
|
713
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,
|
|
714
887
|
recoveryLimiterProvider,
|
|
888
|
+
// #9: the TOTP-step limiter (5 / 15 min per account, `mfa:pid:<principalId>`).
|
|
889
|
+
mfaLimiterProvider,
|
|
715
890
|
// The forgot/reset-password limiters (feature #141): forgot keyed by client IP
|
|
716
891
|
// + per-hint (request-rate caps → silently skip the send when exceeded, stays
|
|
717
892
|
// 204); reset keyed by client IP (may 429 on lockout — token-gated, not enum-
|
|
@@ -748,6 +923,13 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
748
923
|
tokens_js_1.SHIELD_TOTP_VERIFIER,
|
|
749
924
|
// The eager session-facts projector (consumed by AuthController).
|
|
750
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,
|
|
751
933
|
],
|
|
752
934
|
};
|
|
753
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"}
|