@astram/guard 0.9.0 → 0.10.1
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 +21 -13
- package/dist/auth/account.controller.d.ts.map +1 -1
- package/dist/auth/account.controller.js +91 -38
- package/dist/auth/auth.controller.d.ts +6 -5
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +127 -75
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +10 -7
- package/dist/auth/connections.d.ts.map +1 -1
- package/dist/auth/connections.js +3 -1
- package/dist/auth/rate-limit/attempt-limiter.d.ts +75 -11
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
- package/dist/auth/rate-limit/attempt-limiter.js +100 -11
- package/dist/auth/tokens.d.ts +13 -10
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +13 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/module/shield-tokens.d.ts +12 -0
- package/dist/module/shield-tokens.d.ts.map +1 -1
- package/dist/module/shield-tokens.js +13 -1
- package/dist/module/shield.module.d.ts +6 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +40 -18
- package/package.json +3 -3
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
* `DELETE /account/mfa/devices/:id` (code-gated), `POST
|
|
39
39
|
* /account/mfa/recovery-codes/regenerate` (code-gated).
|
|
40
40
|
*
|
|
41
|
-
* ── NO ORACLE ──
|
|
42
|
-
* (
|
|
43
|
-
*
|
|
41
|
+
* ── NO ORACLE ── no-token / bad token is one generic 401. A wrong current password is a
|
|
42
|
+
* 400 (Phase 9 #3; never 401, which clients read as "token expired" and replay), the
|
|
43
|
+
* same for a missing stored hash. Passwords/hashes are NEVER logged.
|
|
44
44
|
*/
|
|
45
45
|
import type { FactorVerifier, PasswordHasher } from '@astram/core';
|
|
46
46
|
import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
@@ -115,8 +115,15 @@ export declare class AccountController {
|
|
|
115
115
|
* • no/invalid token → 401 (generic)
|
|
116
116
|
* • newPassword shorter than the minimum → 422 `{ error: 'weak_password' }`
|
|
117
117
|
* • writer/hasher seam absent → 501 `{ error: 'not_available' }`
|
|
118
|
-
* • wrong current password →
|
|
119
|
-
*
|
|
118
|
+
* • wrong current password → 400 `{ error: 'invalid_current_password' }`
|
|
119
|
+
* (no write; counted; also for no stored hash)
|
|
120
|
+
* • account login budget exhausted → 429 `{ error: 'too_many_requests' }`
|
|
121
|
+
* (not verified, no write)
|
|
122
|
+
* • attempt limiter (Redis) unreachable → 503 `{ error: 'authentication_unavailable' }`
|
|
123
|
+
* (fail closed, not verified, no write)
|
|
124
|
+
*
|
|
125
|
+
* 401 is reserved for a missing/invalid session (Phase 9 #3): the client's refresh
|
|
126
|
+
* wrapper refreshes and replays on a bearer 401, so a credential mismatch must not use it.
|
|
120
127
|
* • success → 204 (no body; counter reset)
|
|
121
128
|
*/
|
|
122
129
|
changePassword(authorization: string | undefined, body: {
|
|
@@ -171,13 +178,13 @@ export declare class AccountController {
|
|
|
171
178
|
private verifyAgainstAnyDevice;
|
|
172
179
|
/**
|
|
173
180
|
* The shared second-factor proof for the code-gated account routes (Phase 8 #1),
|
|
174
|
-
* mirroring `/auth/mfa`: the `mfa:pid:<principalId>`
|
|
175
|
-
* or recovery-code verification (
|
|
176
|
-
*
|
|
177
|
-
* a
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
+
* mirroring `/auth/mfa`: the attempt is counted against `mfa:pid:<principalId>` BEFORE
|
|
182
|
+
* any TOTP or recovery-code verification (Phase 9 #10: atomic increment-first, so
|
|
183
|
+
* parallel guesses cannot all pass; a locked caller never triggers the Argon2 recovery
|
|
184
|
+
* scan) → a TOTP from any confirmed device, else a recovery code (consumed) → a failed
|
|
185
|
+
* proof keeps its count (400), a good one resets the counter. Locked → 429. A Redis
|
|
186
|
+
* error on the gate fails closed (503), as does the TOTP replay claim; an attempt that
|
|
187
|
+
* ended in such an error (no verdict) is given back.
|
|
181
188
|
*/
|
|
182
189
|
private requireMfaProof;
|
|
183
190
|
/**
|
|
@@ -245,7 +252,7 @@ export declare class AccountController {
|
|
|
245
252
|
* and additionally wipes the account's recovery codes.
|
|
246
253
|
*
|
|
247
254
|
* Outcomes: 401; store absent → 501; wrong code → 400 (no remove); not found/owned →
|
|
248
|
-
* 404; success → 204.
|
|
255
|
+
* 404; MFA budget exhausted → 429; limiter/replay guard unreachable → 503; success → 204.
|
|
249
256
|
*/
|
|
250
257
|
removeDevice(authorization: string | undefined, id: string, body: {
|
|
251
258
|
code?: string;
|
|
@@ -256,6 +263,7 @@ export declare class AccountController {
|
|
|
256
263
|
* — a TOTP from any confirmed device OR an existing recovery code.
|
|
257
264
|
*
|
|
258
265
|
* Outcomes: 401; store/recovery/hasher absent → 501; MFA off → 400; wrong code → 400;
|
|
266
|
+
* MFA budget exhausted → 429; limiter/replay guard unreachable → 503;
|
|
259
267
|
* success → 200 `{ recoveryCodes }`.
|
|
260
268
|
*/
|
|
261
269
|
regenerateRecoveryCodes(authorization: string | undefined, body: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAiBH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,cAAc,CAAC;AAI9E,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,yCAAyC,CAAC;AAejD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AACvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
* `DELETE /account/mfa/devices/:id` (code-gated), `POST
|
|
40
40
|
* /account/mfa/recovery-codes/regenerate` (code-gated).
|
|
41
41
|
*
|
|
42
|
-
* ── NO ORACLE ──
|
|
43
|
-
* (
|
|
44
|
-
*
|
|
42
|
+
* ── NO ORACLE ── no-token / bad token is one generic 401. A wrong current password is a
|
|
43
|
+
* 400 (Phase 9 #3; never 401, which clients read as "token expired" and replay), the
|
|
44
|
+
* same for a missing stored hash. Passwords/hashes are NEVER logged.
|
|
45
45
|
*/
|
|
46
46
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
47
47
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -83,14 +83,22 @@ const TOTP_ISSUER = 'Astram';
|
|
|
83
83
|
*/
|
|
84
84
|
const FALLBACK_OTPAUTH_LABEL = 'account';
|
|
85
85
|
/**
|
|
86
|
-
* The SINGLE generic 401 for EVERY unauthenticated
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
86
|
+
* The SINGLE generic 401 for EVERY unauthenticated path here (no token, no binder,
|
|
87
|
+
* resolve→null). Deliberately non-distinguishing — never leaks whether the principal
|
|
88
|
+
* exists. Never used for a credential mismatch by an authenticated caller (Phase 9 #3):
|
|
89
|
+
* clients treat a bearer 401 as an expired token and refresh + replay.
|
|
90
90
|
*/
|
|
91
91
|
function unauthorized() {
|
|
92
92
|
throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* The 400 for a wrong (or unset) current password on change-password (Phase 9 #3). The
|
|
96
|
+
* caller is authenticated and acting on their own account, so naming the field is safe;
|
|
97
|
+
* a missing stored hash gives the same answer (no "has no password" oracle).
|
|
98
|
+
*/
|
|
99
|
+
function invalidCurrentPassword() {
|
|
100
|
+
throw new common_1.HttpException({ error: 'invalid_current_password' }, common_1.HttpStatus.BAD_REQUEST);
|
|
101
|
+
}
|
|
94
102
|
/** The honest 501 when a required seam is unplugged (fail-closed, no mutation). */
|
|
95
103
|
function notAvailable() {
|
|
96
104
|
throw new common_1.HttpException({ error: 'not_available' }, common_1.HttpStatus.NOT_IMPLEMENTED);
|
|
@@ -112,10 +120,30 @@ function badRequest() {
|
|
|
112
120
|
function tooManyRequests() {
|
|
113
121
|
throw new common_1.HttpException({ error: 'too_many_requests' }, common_1.HttpStatus.TOO_MANY_REQUESTS);
|
|
114
122
|
}
|
|
115
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* The retryable 503 when the replay guard or the attempt limiter (Redis) cannot answer:
|
|
125
|
+
* fail closed, no mutation.
|
|
126
|
+
*/
|
|
116
127
|
function retryableUnavailable() {
|
|
117
128
|
throw new common_1.HttpException({ error: 'authentication_unavailable' }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
|
|
118
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Phase 9 #10 — count this attempt BEFORE verifying anything (atomic increment-first).
|
|
132
|
+
* Over budget → 429; limiter unreachable → 503 (fail closed). No limiter wired → no-op.
|
|
133
|
+
*/
|
|
134
|
+
async function consumeAttempt(limiter, key) {
|
|
135
|
+
if (limiter == null)
|
|
136
|
+
return;
|
|
137
|
+
let allowed;
|
|
138
|
+
try {
|
|
139
|
+
({ allowed } = await limiter.consume(key));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
retryableUnavailable();
|
|
143
|
+
}
|
|
144
|
+
if (!allowed)
|
|
145
|
+
tooManyRequests();
|
|
146
|
+
}
|
|
119
147
|
/**
|
|
120
148
|
* The 422 for a semantically invalid profile edit — an empty change (`no_changes`)
|
|
121
149
|
* or a malformed email (`invalid_email`). Distinct from {@link badRequest}: this is
|
|
@@ -239,8 +267,15 @@ let AccountController = class AccountController {
|
|
|
239
267
|
* • no/invalid token → 401 (generic)
|
|
240
268
|
* • newPassword shorter than the minimum → 422 `{ error: 'weak_password' }`
|
|
241
269
|
* • writer/hasher seam absent → 501 `{ error: 'not_available' }`
|
|
242
|
-
* • wrong current password →
|
|
243
|
-
*
|
|
270
|
+
* • wrong current password → 400 `{ error: 'invalid_current_password' }`
|
|
271
|
+
* (no write; counted; also for no stored hash)
|
|
272
|
+
* • account login budget exhausted → 429 `{ error: 'too_many_requests' }`
|
|
273
|
+
* (not verified, no write)
|
|
274
|
+
* • attempt limiter (Redis) unreachable → 503 `{ error: 'authentication_unavailable' }`
|
|
275
|
+
* (fail closed, not verified, no write)
|
|
276
|
+
*
|
|
277
|
+
* 401 is reserved for a missing/invalid session (Phase 9 #3): the client's refresh
|
|
278
|
+
* wrapper refreshes and replays on a bearer 401, so a credential mismatch must not use it.
|
|
244
279
|
* • success → 204 (no body; counter reset)
|
|
245
280
|
*/
|
|
246
281
|
async changePassword(authorization, body) {
|
|
@@ -263,21 +298,34 @@ let AccountController = class AccountController {
|
|
|
263
298
|
// Phase 8 #8: the current-password check spends the SAME per-account budget as
|
|
264
299
|
// password login (`login:pid:<principalId>`, shared counter): checked BEFORE the
|
|
265
300
|
// verify (a locked account never reaches the hasher), a wrong current password records
|
|
266
|
-
// a failure, and a success resets it — exactly as a completed login does. Locked →
|
|
267
|
-
//
|
|
301
|
+
// a failure, and a success resets it — exactly as a completed login does. Locked → 429
|
|
302
|
+
// (Phase 9 #3), like the MFA account routes: the caller is authenticated, so the
|
|
303
|
+
// throttle reveals nothing about another account, and a 401 would make the client
|
|
304
|
+
// refresh and replay the request.
|
|
305
|
+
//
|
|
306
|
+
// Phase 9 #10: the attempt is counted FIRST (atomic), so parallel guesses cannot all
|
|
307
|
+
// slip past a read-only check; a wrong password simply keeps its count. A Redis error
|
|
308
|
+
// fails closed (503).
|
|
268
309
|
const loginKey = (0, canonical_hint_js_1.loginHintKey)('', claims.id);
|
|
269
|
-
|
|
270
|
-
unauthorized();
|
|
271
|
-
}
|
|
310
|
+
await consumeAttempt(this.loginLimiter, loginKey);
|
|
272
311
|
// Verify the current password: read the stored hash BY PRINCIPAL ID via the writer
|
|
273
312
|
// seam's `getPasswordHash` (change-password holds the principal id from the token,
|
|
274
313
|
// whereas CredentialResolver keys by identityHint). A missing hash or a mismatch is
|
|
275
|
-
// the SAME
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
314
|
+
// the SAME 400 (no oracle), and no write happens. NOT 401 (Phase 9 #3): the session is
|
|
315
|
+
// valid, and the client's refresh wrapper treats a bearer 401 as an expired token —
|
|
316
|
+
// refresh + replay, which counted every typo twice against the shared login budget.
|
|
317
|
+
let matches;
|
|
318
|
+
try {
|
|
319
|
+
const stored = await writer.getPasswordHash(claims.id);
|
|
320
|
+
matches = stored !== null && (await hasher.verify(stored, currentPassword));
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
// No verdict (store/hasher fault): give the attempt back, then surface the error.
|
|
324
|
+
await this.loginLimiter?.refund(loginKey);
|
|
325
|
+
throw err;
|
|
280
326
|
}
|
|
327
|
+
if (!matches)
|
|
328
|
+
invalidCurrentPassword(); // the consumed attempt stays counted
|
|
281
329
|
await this.loginLimiter?.reset(loginKey);
|
|
282
330
|
const newHash = await hasher.hash(newPassword);
|
|
283
331
|
await writer.setPassword(claims.id, newHash);
|
|
@@ -393,28 +441,32 @@ let AccountController = class AccountController {
|
|
|
393
441
|
}
|
|
394
442
|
/**
|
|
395
443
|
* The shared second-factor proof for the code-gated account routes (Phase 8 #1),
|
|
396
|
-
* mirroring `/auth/mfa`: the `mfa:pid:<principalId>`
|
|
397
|
-
* or recovery-code verification (
|
|
398
|
-
*
|
|
399
|
-
* a
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
444
|
+
* mirroring `/auth/mfa`: the attempt is counted against `mfa:pid:<principalId>` BEFORE
|
|
445
|
+
* any TOTP or recovery-code verification (Phase 9 #10: atomic increment-first, so
|
|
446
|
+
* parallel guesses cannot all pass; a locked caller never triggers the Argon2 recovery
|
|
447
|
+
* scan) → a TOTP from any confirmed device, else a recovery code (consumed) → a failed
|
|
448
|
+
* proof keeps its count (400), a good one resets the counter. Locked → 429. A Redis
|
|
449
|
+
* error on the gate fails closed (503), as does the TOTP replay claim; an attempt that
|
|
450
|
+
* ended in such an error (no verdict) is given back.
|
|
403
451
|
*/
|
|
404
452
|
async requireMfaProof(store, principalId, code) {
|
|
405
453
|
const mfaKey = `mfa:pid:${principalId}`;
|
|
406
|
-
|
|
407
|
-
|
|
454
|
+
await consumeAttempt(this.mfaLimiter, mfaKey);
|
|
455
|
+
let proven;
|
|
456
|
+
try {
|
|
457
|
+
const byTotp = await this.verifyAgainstAnyDevice(store, principalId, code);
|
|
458
|
+
proven =
|
|
459
|
+
byTotp ||
|
|
460
|
+
(this.recoveryCodes !== undefined &&
|
|
461
|
+
this.recoveryCodes !== null &&
|
|
462
|
+
(await this.recoveryCodes.consume(principalId, code)));
|
|
408
463
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
this.recoveryCodes !== null &&
|
|
413
|
-
(await this.recoveryCodes.consume(principalId, code));
|
|
414
|
-
if (!byTotp && !byRecovery) {
|
|
415
|
-
await this.mfaLimiter?.recordFailure(mfaKey);
|
|
416
|
-
badRequest();
|
|
464
|
+
catch (err) {
|
|
465
|
+
await this.mfaLimiter?.refund(mfaKey);
|
|
466
|
+
throw err;
|
|
417
467
|
}
|
|
468
|
+
if (!proven)
|
|
469
|
+
badRequest(); // the consumed attempt stays counted
|
|
418
470
|
await this.mfaLimiter?.reset(mfaKey);
|
|
419
471
|
}
|
|
420
472
|
/**
|
|
@@ -557,7 +609,7 @@ let AccountController = class AccountController {
|
|
|
557
609
|
* and additionally wipes the account's recovery codes.
|
|
558
610
|
*
|
|
559
611
|
* Outcomes: 401; store absent → 501; wrong code → 400 (no remove); not found/owned →
|
|
560
|
-
* 404; success → 204.
|
|
612
|
+
* 404; MFA budget exhausted → 429; limiter/replay guard unreachable → 503; success → 204.
|
|
561
613
|
*/
|
|
562
614
|
async removeDevice(authorization, id, body) {
|
|
563
615
|
const claims = await this.requireClaims(authorization);
|
|
@@ -591,6 +643,7 @@ let AccountController = class AccountController {
|
|
|
591
643
|
* — a TOTP from any confirmed device OR an existing recovery code.
|
|
592
644
|
*
|
|
593
645
|
* Outcomes: 401; store/recovery/hasher absent → 501; MFA off → 400; wrong code → 400;
|
|
646
|
+
* MFA budget exhausted → 429; limiter/replay guard unreachable → 503;
|
|
594
647
|
* success → 200 `{ recoveryCodes }`.
|
|
595
648
|
*/
|
|
596
649
|
async regenerateRecoveryCodes(authorization, body) {
|
|
@@ -379,11 +379,12 @@ export declare class AuthController {
|
|
|
379
379
|
* {@link MfaStepUp} (same `OrTotpVerifier` + replay guard as the authenticator, principal
|
|
380
380
|
* minted by the identity broker).
|
|
381
381
|
*
|
|
382
|
-
* - `mfa:pid:<principalId>`
|
|
383
|
-
* step-up is never
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
382
|
+
* - the attempt is counted against `mfa:pid:<principalId>` BEFORE verifying (Phase 9
|
|
383
|
+
* #10, atomic increment-first): over budget → generic 401 and the step-up is never
|
|
384
|
+
* consulted; limiter unreachable → 503 (fail closed).
|
|
385
|
+
* - a rejected code stays counted; the attempt whose count equals the threshold audits
|
|
386
|
+
* `auth.mfa.locked` ONCE (no notify — pre-auth, see recovery lockout).
|
|
387
|
+
* - a transient outage → 503, the attempt is given back (never counted).
|
|
387
388
|
* - success → establish + bind, then reset the TOTP counter AND the login counter (the
|
|
388
389
|
* login counter is reset only here, once authentication fully succeeded).
|
|
389
390
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAeH,OAAO,KAAK,EACV,aAAa,EAIb,cAAc,EAGd,gBAAgB,EAEjB,MAAM,cAAc,CAAC;AA+BtB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AACvF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAOjE,OAAO,EAA2C,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"auth.controller.d.ts","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":"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"}
|