@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var TrustProxyBootCheck_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.TrustProxyBootCheck = exports.TRUST_PROXY_WARNING = void 0;
|
|
17
|
+
exports.isTrustProxyConfigured = isTrustProxyConfigured;
|
|
18
|
+
/**
|
|
19
|
+
* Boot-time `trust proxy` decision for ALL per-IP limiters (security hardening #5,
|
|
20
|
+
* review F2/F3): the login failure limiter (`login:ip:`), the forgot-password request cap
|
|
21
|
+
* (`forgot:ip:`) and the reset-password abuse cap (`reset:ip:`).
|
|
22
|
+
*
|
|
23
|
+
* Each keys on `req.ip`. Behind a reverse proxy (Traefik in the Astram stack) `req.ip` is
|
|
24
|
+
* the PROXY's address unless the app sets `app.set('trust proxy', <hop count>)`. With it
|
|
25
|
+
* unset every client shares one bucket, so a per-IP limit becomes an unauthenticated
|
|
26
|
+
* global DoS: 50 failed logins lock ALL logins, 10 bad reset tokens 429 EVERY reset, 5
|
|
27
|
+
* forgot requests silently drop EVERY reset email. Setting it too high lets a client spoof
|
|
28
|
+
* `X-Forwarded-For`, so the framework cannot pick a value.
|
|
29
|
+
*
|
|
30
|
+
* Decision (made ONCE at bootstrap, never per request):
|
|
31
|
+
* - Express `trust proxy` explicitly configured (`app.get('trust proxy') !== false`, so an
|
|
32
|
+
* explicit `0` = "no proxy" counts) → {@link TrustProxyBootCheck.perIpLimitingEnabled}
|
|
33
|
+
* is `true` and AuthController enforces the per-IP limiters;
|
|
34
|
+
* - otherwise (unset, a non-Express adapter, or before bootstrap) → `false`: every per-IP
|
|
35
|
+
* limiter is OFF and one boot warning says so. The per-account limiters (login,
|
|
36
|
+
* forgot-password) and the recovery-code limiter are unaffected.
|
|
37
|
+
*
|
|
38
|
+
* Runs on `onApplicationBootstrap` (inside `app.init()` / `app.listen()`), i.e. after a
|
|
39
|
+
* typical `main.ts` has called `app.set('trust proxy', …)` on the created app.
|
|
40
|
+
*/
|
|
41
|
+
const common_1 = require("@nestjs/common");
|
|
42
|
+
const core_1 = require("@nestjs/core");
|
|
43
|
+
/**
|
|
44
|
+
* `true` when the Express app has an explicit `trust proxy` value. Express's default is
|
|
45
|
+
* `false`; an explicit `0`/`'loopback'`/hop count/subnet list counts as configured.
|
|
46
|
+
* Returns `null` when the instance is not Express-like (cannot tell).
|
|
47
|
+
*/
|
|
48
|
+
function isTrustProxyConfigured(instance) {
|
|
49
|
+
if (instance === null || (typeof instance !== 'function' && typeof instance !== 'object'))
|
|
50
|
+
return null;
|
|
51
|
+
const get = instance.get;
|
|
52
|
+
if (typeof get !== 'function')
|
|
53
|
+
return null;
|
|
54
|
+
const value = get.call(instance, 'trust proxy');
|
|
55
|
+
return value !== undefined && value !== false;
|
|
56
|
+
}
|
|
57
|
+
exports.TRUST_PROXY_WARNING = 'per-IP limiting (login, forgot-password, reset-password) is OFF: set trust proxy ' +
|
|
58
|
+
'(0 if no proxy, else the hop count). Without an explicit Express ' +
|
|
59
|
+
"'trust proxy' every client behind a reverse proxy shares the proxy's IP, so a per-IP " +
|
|
60
|
+
"limit would block everyone. In main.ts call app.set('trust proxy', <number of proxy hops " +
|
|
61
|
+
'in front of this app>) — never more than the real count. The per-account login and ' +
|
|
62
|
+
'forgot-password limits stay on.';
|
|
63
|
+
let TrustProxyBootCheck = class TrustProxyBootCheck {
|
|
64
|
+
static { TrustProxyBootCheck_1 = this; }
|
|
65
|
+
adapterHost;
|
|
66
|
+
static logger = new common_1.Logger('ShieldAuth');
|
|
67
|
+
enabled = false;
|
|
68
|
+
constructor(adapterHost) {
|
|
69
|
+
this.adapterHost = adapterHost;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether AuthController enforces the per-IP limiters (login failures, forgot-password,
|
|
73
|
+
* reset-password). `false` until bootstrap has confirmed an explicit Express
|
|
74
|
+
* `trust proxy` (fail-safe: never a global lockout from a shared proxy IP).
|
|
75
|
+
*/
|
|
76
|
+
get perIpLimitingEnabled() {
|
|
77
|
+
return this.enabled;
|
|
78
|
+
}
|
|
79
|
+
onApplicationBootstrap() {
|
|
80
|
+
const adapter = this.adapterHost?.httpAdapter;
|
|
81
|
+
if (!adapter)
|
|
82
|
+
return; // no HTTP server (e.g. a bare module) → no auth routes; stays off
|
|
83
|
+
const configured = adapter.getType() === 'express' ? isTrustProxyConfigured(adapter.getInstance()) : null;
|
|
84
|
+
this.enabled = configured === true;
|
|
85
|
+
if (!this.enabled)
|
|
86
|
+
TrustProxyBootCheck_1.logger.warn(exports.TRUST_PROXY_WARNING);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.TrustProxyBootCheck = TrustProxyBootCheck;
|
|
90
|
+
exports.TrustProxyBootCheck = TrustProxyBootCheck = TrustProxyBootCheck_1 = __decorate([
|
|
91
|
+
(0, common_1.Injectable)(),
|
|
92
|
+
__param(0, (0, common_1.Optional)()),
|
|
93
|
+
__metadata("design:paramtypes", [core_1.HttpAdapterHost])
|
|
94
|
+
], TrustProxyBootCheck);
|
package/dist/auth/tokens.d.ts
CHANGED
|
@@ -211,13 +211,29 @@ export declare const SHIELD_SCOPE_SUBTREE_RESOLVER: unique symbol;
|
|
|
211
211
|
*/
|
|
212
212
|
export declare const SHIELD_MEMBERSHIP_CHECKER: unique symbol;
|
|
213
213
|
/**
|
|
214
|
-
* The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed
|
|
215
|
-
* `login:<
|
|
216
|
-
*
|
|
217
|
-
* (
|
|
218
|
-
* `
|
|
214
|
+
* The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed per
|
|
215
|
+
* ACCOUNT: `login:pid:<principalId>` for the account the credential store resolves for the
|
|
216
|
+
* typed hint (the DB collation decides which spellings are one account), else
|
|
217
|
+
* `login:<sha256(canonical hint)>` for an unknown hint — no PII in the key; see
|
|
218
|
+
* `rate-limit/canonical-hint.ts`. Default 10 failures / 15-min window.
|
|
219
|
+
* Consumed at the TOP of `POST /auth/login` (`check` → generic 401 when locked) and on
|
|
220
|
+
* the outcome (`recordFailure` on a rejected wrong-password; `reset` on `authenticated`
|
|
221
|
+
* OR `mfa_required` — a correct password that merely needs MFA is NOT a failure).
|
|
219
222
|
*/
|
|
220
223
|
export declare const SHIELD_LOGIN_LIMITER: unique symbol;
|
|
224
|
+
/**
|
|
225
|
+
* The password-login PER-CLIENT-IP {@link AttemptLimiter} (security hardening #5) —
|
|
226
|
+
* keyed `login:ip:<req.ip>`, default 50 failures / 15-min window (configurable via
|
|
227
|
+
* `LOGIN_IP_MAX_FAILURES` / `LOGIN_IP_WINDOW_MS`, see `AppConfig.loginIpLimit`). Caps
|
|
228
|
+
* password spraying across MANY distinct hints from one client. Counts wrong-password
|
|
229
|
+
* FAILURES only and is NEVER reset by a success (an attacker's own valid login must not
|
|
230
|
+
* wipe the budget). Checked before the account lookup and the authenticator. Fails open
|
|
231
|
+
* on a Redis blip (the limiter's posture). ENFORCED ONLY when the app set Express
|
|
232
|
+
* `trust proxy` explicitly (0 = no proxy, else the hop count): otherwise every client
|
|
233
|
+
* behind a proxy shares one `req.ip`, so the limiter is switched OFF at boot with a
|
|
234
|
+
* warning (`TrustProxyBootCheck`) rather than becoming a global login DoS.
|
|
235
|
+
*/
|
|
236
|
+
export declare const SHIELD_LOGIN_IP_LIMITER: unique symbol;
|
|
221
237
|
/**
|
|
222
238
|
* The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
|
|
223
239
|
* default 5 failures / 15-min window (recovery codes are higher-value than a rotating
|
|
@@ -226,6 +242,29 @@ export declare const SHIELD_LOGIN_LIMITER: unique symbol;
|
|
|
226
242
|
* code, `reset` on success.
|
|
227
243
|
*/
|
|
228
244
|
export declare const SHIELD_RECOVERY_LIMITER: unique symbol;
|
|
245
|
+
/**
|
|
246
|
+
* The TOTP-step {@link AttemptLimiter} (security hardening #9) — keyed per account,
|
|
247
|
+
* `mfa:pid:<principalId>`, default 5 failures / 15-min window. Consumed in the `/auth/mfa`
|
|
248
|
+
* TOTP branch: `check` BEFORE the factor is verified (locked → generic 401 without
|
|
249
|
+
* consulting the step-up), `recordFailure` on a rejected factor (the threshold edge audits
|
|
250
|
+
* `auth.mfa.locked` once), `reset` on success.
|
|
251
|
+
*/
|
|
252
|
+
export declare const SHIELD_MFA_LIMITER: unique symbol;
|
|
253
|
+
/**
|
|
254
|
+
* The MFA step-up (security hardening M6) — `MfaStepUp.verify(challenge, credential)`
|
|
255
|
+
* verifies ONLY the second factor for the principal recorded by the password step (no
|
|
256
|
+
* password is held between the two requests). Default: `CredentialMfaStepUp` over the
|
|
257
|
+
* credential resolver, the login TOTP verifier and the identity broker. Absent → the
|
|
258
|
+
* `/auth/mfa` TOTP branch fails closed (generic 401).
|
|
259
|
+
*/
|
|
260
|
+
export declare const SHIELD_MFA_STEP_UP: unique symbol;
|
|
261
|
+
/**
|
|
262
|
+
* The LOGIN `totp` {@link FactorVerifier}: the OR-across-devices `OrTotpVerifier` (with the
|
|
263
|
+
* Redis replay guard, M-B) wrapping the base verifier (the consumer `totpVerifier` override
|
|
264
|
+
* or the framework RFC-6238 one). ONE instance shared by the composed authenticator and the
|
|
265
|
+
* MFA step-up, so both paths verify — and replay-guard — identically.
|
|
266
|
+
*/
|
|
267
|
+
export declare const SHIELD_LOGIN_TOTP_VERIFIER: unique symbol;
|
|
229
268
|
/**
|
|
230
269
|
* The forgot-password IP {@link AttemptLimiter} (feature #141) — keyed
|
|
231
270
|
* `forgot:ip:<clientIp>`, default 5 per 15-min window. Consumed at the TOP of
|
|
@@ -233,14 +272,16 @@ export declare const SHIELD_RECOVERY_LIMITER: unique symbol;
|
|
|
233
272
|
* would itself be an enumeration/probing signal), so this is a request-rate cap: the
|
|
234
273
|
* handler counts EVERY forgot request against the IP budget and, once the budget is
|
|
235
274
|
* exceeded, SILENTLY skips the email-send work while STILL returning 204 (the existing
|
|
236
|
-
* always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip.
|
|
237
|
-
*
|
|
238
|
-
*
|
|
275
|
+
* always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip. Keyed on
|
|
276
|
+
* `req.ip`, so ENFORCED ONLY when the app set Express `trust proxy` explicitly; otherwise
|
|
277
|
+
* it is OFF (TrustProxyBootCheck), since a shared proxy IP would drop every reset email.
|
|
239
278
|
*/
|
|
240
279
|
export declare const SHIELD_FORGOT_LIMITER: unique symbol;
|
|
241
280
|
/**
|
|
242
281
|
* The forgot-password PER-HINT {@link AttemptLimiter} (feature #141) — keyed
|
|
243
|
-
* `forgot:<
|
|
282
|
+
* per ACCOUNT, `forgot:pid:<principalId>` (unknown hint → `forgot:<sha256(canonical)>`),
|
|
283
|
+
* same PII-free scheme as the login key,
|
|
284
|
+
* default 3 per 60-min window (generous). Caps how often ANY
|
|
244
285
|
* caller (across IPs) can trigger a reset email for ONE account, so a single victim
|
|
245
286
|
* can't be email-bombed from a rotating IP pool. Same request-rate-cap + silent-skip-
|
|
246
287
|
* still-204 semantics as {@link SHIELD_FORGOT_LIMITER}; consumed alongside it in
|
|
@@ -254,8 +295,9 @@ export declare const SHIELD_FORGOT_HINT_LIMITER: unique symbol;
|
|
|
254
295
|
* 256-bit single-use token), so this is DoS/abuse throttling, NOT anti-brute-force —
|
|
255
296
|
* and it MAY return a GENERIC 429 on lockout (a token-gated endpoint is not
|
|
256
297
|
* enumeration-sensitive). `recordFailure` on a bad/invalid token (the abuse signal);
|
|
257
|
-
* `reset` on a successful reset (204). Fail-open on a Redis blip.
|
|
258
|
-
* `
|
|
298
|
+
* `reset` on a successful reset (204). Fail-open on a Redis blip. Keyed on `req.ip`, so
|
|
299
|
+
* ENFORCED ONLY when the app set Express `trust proxy` explicitly; otherwise it is OFF
|
|
300
|
+
* (TrustProxyBootCheck), since a shared proxy IP would 429 every reset.
|
|
259
301
|
*/
|
|
260
302
|
export declare const SHIELD_RESET_LIMITER: unique symbol;
|
|
261
303
|
/**
|
|
@@ -267,4 +309,21 @@ export declare const SHIELD_RESET_LIMITER: unique symbol;
|
|
|
267
309
|
* or version, which would fail-OPEN).
|
|
268
310
|
*/
|
|
269
311
|
export declare const SHIELD_PERMISSION_FACTS: unique symbol;
|
|
312
|
+
/**
|
|
313
|
+
* The Redis permission-version cache (ADR-0035, GAPS #6) — a
|
|
314
|
+
* {@link import('../authz/permission-version-cache.js').RedisPermissionVersionCache} over
|
|
315
|
+
* {@link SHIELD_REDIS}. Provided and exported by `AuthModule` (`auth: true`). Consumed by the
|
|
316
|
+
* framework-built decision authority (gate 5 reads the version from Redis) and, through the
|
|
317
|
+
* `@Global` ShieldModule, by `@astram/identity`'s permission-version bumper (publish after commit).
|
|
318
|
+
* Absent (`auth: false`) → gate 5 keeps the app port's own version read.
|
|
319
|
+
*/
|
|
320
|
+
export declare const SHIELD_PERMISSION_VERSION_CACHE: unique symbol;
|
|
321
|
+
/**
|
|
322
|
+
* The durable session-restriction store (Phase 3B review P1, ADR-0035 §3) — a
|
|
323
|
+
* {@link import('../session/session-restriction-store.js').SessionRestrictionStore}. Persists the
|
|
324
|
+
* recovery-login re-enroll restriction on the durable session and answers the refresh rebuild
|
|
325
|
+
* (restriction + account status). Provided by `AuthModule` over {@link SHIELD_DB_POOL}. Absent →
|
|
326
|
+
* the rebuild is refused (401) and recovery logins cannot be marked (they fail closed).
|
|
327
|
+
*/
|
|
328
|
+
export declare const SHIELD_SESSION_RESTRICTIONS: unique symbol;
|
|
270
329
|
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAEvF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,eAAsC,CAAC;AAU7E
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAEvF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,eAAsC,CAAC;AAU7E;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC;AAEzF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,eAAwC,CAAC"}
|
package/dist/auth/tokens.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ADR-0003: no framework/DB/crypto imports here — just Symbols (+ one re-export).
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.SHIELD_PERMISSION_FACTS = exports.SHIELD_RESET_LIMITER = exports.SHIELD_FORGOT_HINT_LIMITER = exports.SHIELD_FORGOT_LIMITER = exports.SHIELD_RECOVERY_LIMITER = exports.SHIELD_LOGIN_LIMITER = exports.SHIELD_MEMBERSHIP_CHECKER = exports.SHIELD_SCOPE_SUBTREE_RESOLVER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.SHIELD_ROOT_IDS = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_TOTP_VERIFIER = exports.SHIELD_RESET_LINK_BASE_URL = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
|
|
26
|
+
exports.SHIELD_SESSION_RESTRICTIONS = exports.SHIELD_PERMISSION_VERSION_CACHE = exports.SHIELD_PERMISSION_FACTS = exports.SHIELD_RESET_LIMITER = exports.SHIELD_FORGOT_HINT_LIMITER = exports.SHIELD_FORGOT_LIMITER = exports.SHIELD_LOGIN_TOTP_VERIFIER = exports.SHIELD_MFA_STEP_UP = exports.SHIELD_MFA_LIMITER = exports.SHIELD_RECOVERY_LIMITER = exports.SHIELD_LOGIN_IP_LIMITER = exports.SHIELD_LOGIN_LIMITER = exports.SHIELD_MEMBERSHIP_CHECKER = exports.SHIELD_SCOPE_SUBTREE_RESOLVER = exports.SHIELD_SESSION_FACTS_PROJECTOR = exports.SHIELD_ROOT_IDS = exports.SHIELD_TOTP_VERIFIER_OVERRIDE = exports.SHIELD_TOTP_VERIFIER = exports.SHIELD_RESET_LINK_BASE_URL = exports.SHIELD_SESSION_SWEEPER = exports.SHIELD_MFA_DEVICE_STORE = exports.SHIELD_AUDIT_SINK = exports.SHIELD_PROFILE_WRITER = exports.SHIELD_RECOVERY_CODE_STORE = exports.SHIELD_EMAIL_SENDER = exports.SHIELD_RESET_TOKEN_STORE = exports.SHIELD_CREDENTIAL_WRITER = exports.SHIELD_SESSION_AUTHORITY = exports.SHIELD_AUTHENTICATOR = exports.SHIELD_PROFILE_RESOLVER = exports.SHIELD_CREDENTIAL_RESOLVER = exports.SHIELD_REDIS = exports.SHIELD_DB_POOL = exports.SHIELD_PASSWORD_HASHER = void 0;
|
|
27
27
|
var argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
|
|
28
28
|
Object.defineProperty(exports, "SHIELD_PASSWORD_HASHER", { enumerable: true, get: function () { return argon2id_password_hasher_js_1.SHIELD_PASSWORD_HASHER; } });
|
|
29
29
|
/**
|
|
@@ -230,13 +230,29 @@ exports.SHIELD_MEMBERSHIP_CHECKER = Symbol('SHIELD_MEMBERSHIP_CHECKER');
|
|
|
230
230
|
// limiter degrades to "allow" (never a lock-out from missing wiring).
|
|
231
231
|
// ---------------------------------------------------------------------------
|
|
232
232
|
/**
|
|
233
|
-
* The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed
|
|
234
|
-
* `login:<
|
|
235
|
-
*
|
|
236
|
-
* (
|
|
237
|
-
* `
|
|
233
|
+
* The password-login {@link AttemptLimiter} (spec §5 / IdP-hardening H1) — keyed per
|
|
234
|
+
* ACCOUNT: `login:pid:<principalId>` for the account the credential store resolves for the
|
|
235
|
+
* typed hint (the DB collation decides which spellings are one account), else
|
|
236
|
+
* `login:<sha256(canonical hint)>` for an unknown hint — no PII in the key; see
|
|
237
|
+
* `rate-limit/canonical-hint.ts`. Default 10 failures / 15-min window.
|
|
238
|
+
* Consumed at the TOP of `POST /auth/login` (`check` → generic 401 when locked) and on
|
|
239
|
+
* the outcome (`recordFailure` on a rejected wrong-password; `reset` on `authenticated`
|
|
240
|
+
* OR `mfa_required` — a correct password that merely needs MFA is NOT a failure).
|
|
238
241
|
*/
|
|
239
242
|
exports.SHIELD_LOGIN_LIMITER = Symbol('SHIELD_LOGIN_LIMITER');
|
|
243
|
+
/**
|
|
244
|
+
* The password-login PER-CLIENT-IP {@link AttemptLimiter} (security hardening #5) —
|
|
245
|
+
* keyed `login:ip:<req.ip>`, default 50 failures / 15-min window (configurable via
|
|
246
|
+
* `LOGIN_IP_MAX_FAILURES` / `LOGIN_IP_WINDOW_MS`, see `AppConfig.loginIpLimit`). Caps
|
|
247
|
+
* password spraying across MANY distinct hints from one client. Counts wrong-password
|
|
248
|
+
* FAILURES only and is NEVER reset by a success (an attacker's own valid login must not
|
|
249
|
+
* wipe the budget). Checked before the account lookup and the authenticator. Fails open
|
|
250
|
+
* on a Redis blip (the limiter's posture). ENFORCED ONLY when the app set Express
|
|
251
|
+
* `trust proxy` explicitly (0 = no proxy, else the hop count): otherwise every client
|
|
252
|
+
* behind a proxy shares one `req.ip`, so the limiter is switched OFF at boot with a
|
|
253
|
+
* warning (`TrustProxyBootCheck`) rather than becoming a global login DoS.
|
|
254
|
+
*/
|
|
255
|
+
exports.SHIELD_LOGIN_IP_LIMITER = Symbol('SHIELD_LOGIN_IP_LIMITER');
|
|
240
256
|
/**
|
|
241
257
|
* The recovery-code {@link AttemptLimiter} (spec §5) — keyed `recovery:<principalId>`,
|
|
242
258
|
* default 5 failures / 15-min window (recovery codes are higher-value than a rotating
|
|
@@ -245,6 +261,29 @@ exports.SHIELD_LOGIN_LIMITER = Symbol('SHIELD_LOGIN_LIMITER');
|
|
|
245
261
|
* code, `reset` on success.
|
|
246
262
|
*/
|
|
247
263
|
exports.SHIELD_RECOVERY_LIMITER = Symbol('SHIELD_RECOVERY_LIMITER');
|
|
264
|
+
/**
|
|
265
|
+
* The TOTP-step {@link AttemptLimiter} (security hardening #9) — keyed per account,
|
|
266
|
+
* `mfa:pid:<principalId>`, default 5 failures / 15-min window. Consumed in the `/auth/mfa`
|
|
267
|
+
* TOTP branch: `check` BEFORE the factor is verified (locked → generic 401 without
|
|
268
|
+
* consulting the step-up), `recordFailure` on a rejected factor (the threshold edge audits
|
|
269
|
+
* `auth.mfa.locked` once), `reset` on success.
|
|
270
|
+
*/
|
|
271
|
+
exports.SHIELD_MFA_LIMITER = Symbol('SHIELD_MFA_LIMITER');
|
|
272
|
+
/**
|
|
273
|
+
* The MFA step-up (security hardening M6) — `MfaStepUp.verify(challenge, credential)`
|
|
274
|
+
* verifies ONLY the second factor for the principal recorded by the password step (no
|
|
275
|
+
* password is held between the two requests). Default: `CredentialMfaStepUp` over the
|
|
276
|
+
* credential resolver, the login TOTP verifier and the identity broker. Absent → the
|
|
277
|
+
* `/auth/mfa` TOTP branch fails closed (generic 401).
|
|
278
|
+
*/
|
|
279
|
+
exports.SHIELD_MFA_STEP_UP = Symbol('SHIELD_MFA_STEP_UP');
|
|
280
|
+
/**
|
|
281
|
+
* The LOGIN `totp` {@link FactorVerifier}: the OR-across-devices `OrTotpVerifier` (with the
|
|
282
|
+
* Redis replay guard, M-B) wrapping the base verifier (the consumer `totpVerifier` override
|
|
283
|
+
* or the framework RFC-6238 one). ONE instance shared by the composed authenticator and the
|
|
284
|
+
* MFA step-up, so both paths verify — and replay-guard — identically.
|
|
285
|
+
*/
|
|
286
|
+
exports.SHIELD_LOGIN_TOTP_VERIFIER = Symbol('SHIELD_LOGIN_TOTP_VERIFIER');
|
|
248
287
|
/**
|
|
249
288
|
* The forgot-password IP {@link AttemptLimiter} (feature #141) — keyed
|
|
250
289
|
* `forgot:ip:<clientIp>`, default 5 per 15-min window. Consumed at the TOP of
|
|
@@ -252,14 +291,16 @@ exports.SHIELD_RECOVERY_LIMITER = Symbol('SHIELD_RECOVERY_LIMITER');
|
|
|
252
291
|
* would itself be an enumeration/probing signal), so this is a request-rate cap: the
|
|
253
292
|
* handler counts EVERY forgot request against the IP budget and, once the budget is
|
|
254
293
|
* exceeded, SILENTLY skips the email-send work while STILL returning 204 (the existing
|
|
255
|
-
* always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip.
|
|
256
|
-
*
|
|
257
|
-
*
|
|
294
|
+
* always-204 anti-enumeration posture is preserved). Fail-open on a Redis blip. Keyed on
|
|
295
|
+
* `req.ip`, so ENFORCED ONLY when the app set Express `trust proxy` explicitly; otherwise
|
|
296
|
+
* it is OFF (TrustProxyBootCheck), since a shared proxy IP would drop every reset email.
|
|
258
297
|
*/
|
|
259
298
|
exports.SHIELD_FORGOT_LIMITER = Symbol('SHIELD_FORGOT_LIMITER');
|
|
260
299
|
/**
|
|
261
300
|
* The forgot-password PER-HINT {@link AttemptLimiter} (feature #141) — keyed
|
|
262
|
-
* `forgot:<
|
|
301
|
+
* per ACCOUNT, `forgot:pid:<principalId>` (unknown hint → `forgot:<sha256(canonical)>`),
|
|
302
|
+
* same PII-free scheme as the login key,
|
|
303
|
+
* default 3 per 60-min window (generous). Caps how often ANY
|
|
263
304
|
* caller (across IPs) can trigger a reset email for ONE account, so a single victim
|
|
264
305
|
* can't be email-bombed from a rotating IP pool. Same request-rate-cap + silent-skip-
|
|
265
306
|
* still-204 semantics as {@link SHIELD_FORGOT_LIMITER}; consumed alongside it in
|
|
@@ -273,8 +314,9 @@ exports.SHIELD_FORGOT_HINT_LIMITER = Symbol('SHIELD_FORGOT_HINT_LIMITER');
|
|
|
273
314
|
* 256-bit single-use token), so this is DoS/abuse throttling, NOT anti-brute-force —
|
|
274
315
|
* and it MAY return a GENERIC 429 on lockout (a token-gated endpoint is not
|
|
275
316
|
* enumeration-sensitive). `recordFailure` on a bad/invalid token (the abuse signal);
|
|
276
|
-
* `reset` on a successful reset (204). Fail-open on a Redis blip.
|
|
277
|
-
* `
|
|
317
|
+
* `reset` on a successful reset (204). Fail-open on a Redis blip. Keyed on `req.ip`, so
|
|
318
|
+
* ENFORCED ONLY when the app set Express `trust proxy` explicitly; otherwise it is OFF
|
|
319
|
+
* (TrustProxyBootCheck), since a shared proxy IP would 429 every reset.
|
|
278
320
|
*/
|
|
279
321
|
exports.SHIELD_RESET_LIMITER = Symbol('SHIELD_RESET_LIMITER');
|
|
280
322
|
/**
|
|
@@ -286,3 +328,20 @@ exports.SHIELD_RESET_LIMITER = Symbol('SHIELD_RESET_LIMITER');
|
|
|
286
328
|
* or version, which would fail-OPEN).
|
|
287
329
|
*/
|
|
288
330
|
exports.SHIELD_PERMISSION_FACTS = Symbol('SHIELD_PERMISSION_FACTS');
|
|
331
|
+
/**
|
|
332
|
+
* The Redis permission-version cache (ADR-0035, GAPS #6) — a
|
|
333
|
+
* {@link import('../authz/permission-version-cache.js').RedisPermissionVersionCache} over
|
|
334
|
+
* {@link SHIELD_REDIS}. Provided and exported by `AuthModule` (`auth: true`). Consumed by the
|
|
335
|
+
* framework-built decision authority (gate 5 reads the version from Redis) and, through the
|
|
336
|
+
* `@Global` ShieldModule, by `@astram/identity`'s permission-version bumper (publish after commit).
|
|
337
|
+
* Absent (`auth: false`) → gate 5 keeps the app port's own version read.
|
|
338
|
+
*/
|
|
339
|
+
exports.SHIELD_PERMISSION_VERSION_CACHE = Symbol('SHIELD_PERMISSION_VERSION_CACHE');
|
|
340
|
+
/**
|
|
341
|
+
* The durable session-restriction store (Phase 3B review P1, ADR-0035 §3) — a
|
|
342
|
+
* {@link import('../session/session-restriction-store.js').SessionRestrictionStore}. Persists the
|
|
343
|
+
* recovery-login re-enroll restriction on the durable session and answers the refresh rebuild
|
|
344
|
+
* (restriction + account status). Provided by `AuthModule` over {@link SHIELD_DB_POOL}. Absent →
|
|
345
|
+
* the rebuild is refused (401) and recovery logins cannot be marked (they fail closed).
|
|
346
|
+
*/
|
|
347
|
+
exports.SHIELD_SESSION_RESTRICTIONS = Symbol('SHIELD_SESSION_RESTRICTIONS');
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LogEmailSender — the framework DEV-
|
|
3
|
-
* message (recipient + subject +
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* LogEmailSender — the framework DEV-ONLY EmailSender. Does NOT deliver mail; it LOGS the outgoing
|
|
3
|
+
* message (recipient + subject + the FULL text, i.e. the live `?token=` reset link) to stdout so
|
|
4
|
+
* forgot/reset-password works end-to-end in development with no external provider.
|
|
5
|
+
*
|
|
6
|
+
* ⛔ It prints usable reset tokens. AuthModule binds it by default ONLY when the RAW `NODE_ENV` is
|
|
7
|
+
* explicitly `development` or `test` (with a startup warning); in any other environment — including
|
|
8
|
+
* an UNSET `NODE_ENV` — auth composition fails unless the app supplies a real `emailSender` or
|
|
9
|
+
* `emailSender: 'disabled'` (security hardening #6). Never throws (a logging failure must not fail
|
|
10
|
+
* the forgot-password flow, which is fail-closed/no-op-safe by design).
|
|
6
11
|
*/
|
|
7
12
|
import type { EmailSender, EmailMessage } from '../auth/ports/email-sender.js';
|
|
8
13
|
export declare class LogEmailSender implements EmailSender {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-email-sender.d.ts","sourceRoot":"","sources":["../../src/authentication/log-email-sender.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"log-email-sender.d.ts","sourceRoot":"","sources":["../../src/authentication/log-email-sender.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE/E,qBAAa,cAAe,YAAW,WAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAA4B;IACzE,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;CAKjD"}
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* the plaintext. A store breach yields no usable key.
|
|
8
8
|
*
|
|
9
9
|
* `getByAccountId` returns the CURRENT (highest-version, non-superseded) key record, or `null` for
|
|
10
|
-
* an unknown account (anti-enumeration — the authenticator maps null → a uniform failure).
|
|
10
|
+
* an unknown account (anti-enumeration — the authenticator maps null → a uniform failure). Its
|
|
11
|
+
* scope is the account's LIVE `shield_app.service_account.sa_scope_subtree` (JOINed), never the
|
|
12
|
+
* copy stored on the key at issue time; a disabled account, a missing account row, or an account
|
|
13
|
+
* whose tenant disagrees with the key's also yields `null` (security hardening Phase 1C). A genuine
|
|
11
14
|
* store OUTAGE (the `query` rejecting) PROPAGATES (the authenticator catches it → fail-closed), never
|
|
12
15
|
* swallowed to null. Every query is parameterized; the hash is never logged.
|
|
13
16
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-api-key-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-api-key-store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mariadb-api-key-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-api-key-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA6C9D,qBAAa,kBAAmB,YAAW,WAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA8CrE;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;QACvC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;QAC3B,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBjB,iGAAiG;IAC3F,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQxD,+FAA+F;IACzF,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAQ7D"}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MariaDbApiKeyStore = void 0;
|
|
4
4
|
const AUTHZ = 'shield_authz';
|
|
5
|
+
/** The account rows (`service_account`, created by the identity migration `…018`). */
|
|
6
|
+
const APP = 'shield_app';
|
|
5
7
|
/**
|
|
6
8
|
* Read a JSON-array column into a `string[]`. The mariadb driver returns a JSON/`longtext` column
|
|
7
9
|
* EITHER already-parsed (an array/object — its default for JSON columns) OR as a raw string,
|
|
@@ -32,20 +34,42 @@ class MariaDbApiKeyStore {
|
|
|
32
34
|
async getByAccountId(accountId) {
|
|
33
35
|
// The CURRENT key = highest version for the account. (Rotation inserts a new version; the old
|
|
34
36
|
// may linger for an overlap window but only the current is returned for auth.)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
//
|
|
38
|
+
// LIVE SCOPE (security hardening Phase 1C): the key row keeps the scope COPIED at issue time,
|
|
39
|
+
// which goes stale the moment an admin narrows the account. So the account row is JOINed and
|
|
40
|
+
// its CURRENT `sa_scope_subtree` is the scope returned. An INNER JOIN: a key whose account row
|
|
41
|
+
// is gone matches nothing → null (deny). This is the same single per-request read as before
|
|
42
|
+
// (the machine path already reads this store on every `ask_…` request; it is not the session
|
|
43
|
+
// hot path of invariant #12), now one indexed PK join wider. `shield_core` holds SELECT on
|
|
44
|
+
// `shield_app.*`, so no grant change.
|
|
45
|
+
//
|
|
46
|
+
// LIVE ROLES (security hardening Phase 3, KI #22, ADR-0035): same reasoning for the roles — the
|
|
47
|
+
// key row's `sak_role_ids` is the issue-time copy; the account's CURRENT `sa_role_ids` (the same
|
|
48
|
+
// column gate 5's `readRoleIdsForTenant` reads) is what the principal carries, so role-keyed
|
|
49
|
+
// record rules, list scope and shares see a role change on the next request.
|
|
50
|
+
const rows = (await this.pool.query(`SELECT k.sak_account_id, k.sak_tenant_id, k.sak_role_ids,
|
|
51
|
+
k.sak_secret_hash, k.sak_version, k.sak_expires_at_ms, k.sak_revoked_at_ms,
|
|
52
|
+
sa.sa_scope_subtree, sa.sa_role_ids, sa.sa_status, sa.sa_tenant_id
|
|
53
|
+
FROM \`${AUTHZ}\`.\`service_account_key\` k
|
|
54
|
+
JOIN \`${APP}\`.\`service_account\` sa ON sa.sa_id = k.sak_account_id
|
|
55
|
+
WHERE k.sak_account_id = ?
|
|
56
|
+
ORDER BY k.sak_version DESC
|
|
40
57
|
LIMIT 1`, [accountId]));
|
|
41
58
|
const row = rows[0];
|
|
42
59
|
if (!row)
|
|
43
60
|
return null;
|
|
61
|
+
// Deny (the authenticator's uniform failure) when the account is not active, or when the
|
|
62
|
+
// account's tenant disagrees with the tenant the key was bound to — the key never carries a
|
|
63
|
+
// principal into a tenant its account is no longer in.
|
|
64
|
+
if (row.sa_status !== 'active')
|
|
65
|
+
return null;
|
|
66
|
+
if (row.sa_tenant_id !== row.sak_tenant_id)
|
|
67
|
+
return null;
|
|
44
68
|
return {
|
|
45
69
|
accountId: row.sak_account_id,
|
|
46
70
|
tenantId: row.sak_tenant_id,
|
|
47
|
-
scopeSubtreeNodeIds: parseJsonArray(row.
|
|
48
|
-
roleIds: parseJsonArray(row.
|
|
71
|
+
scopeSubtreeNodeIds: parseJsonArray(row.sa_scope_subtree),
|
|
72
|
+
roleIds: parseJsonArray(row.sa_role_ids),
|
|
49
73
|
secretHash: row.sak_secret_hash,
|
|
50
74
|
version: Number(row.sak_version),
|
|
51
75
|
...(row.sak_expires_at_ms !== null ? { expiresAtMs: Number(row.sak_expires_at_ms) } : {}),
|