@astram/guard 0.19.0 → 0.20.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 +79 -6
- package/dist/auth/admin-signin-controls.d.ts +80 -0
- package/dist/auth/admin-signin-controls.d.ts.map +1 -0
- package/dist/auth/admin-signin-controls.js +91 -0
- package/dist/auth/auth-audit.d.ts +59 -0
- package/dist/auth/auth-audit.d.ts.map +1 -0
- package/dist/auth/auth-audit.js +93 -0
- package/dist/auth/auth.controller.d.ts +53 -6
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +330 -44
- package/dist/auth/auth.module.d.ts +49 -0
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +149 -4
- package/dist/auth/captcha-gate.d.ts +103 -0
- package/dist/auth/captcha-gate.d.ts.map +1 -0
- package/dist/auth/captcha-gate.js +245 -0
- package/dist/auth/dto.d.ts +22 -1
- package/dist/auth/dto.d.ts.map +1 -1
- package/dist/auth/login-name-count-boot.d.ts +24 -0
- package/dist/auth/login-name-count-boot.d.ts.map +1 -0
- package/dist/auth/login-name-count-boot.js +47 -0
- package/dist/auth/logout-proof.d.ts +55 -0
- package/dist/auth/logout-proof.d.ts.map +1 -0
- package/dist/auth/logout-proof.js +127 -0
- package/dist/auth/pending-mfa-store.d.ts +5 -0
- package/dist/auth/pending-mfa-store.d.ts.map +1 -1
- package/dist/auth/pending-mfa-store.js +2 -0
- package/dist/auth/ports/audit-sink.d.ts +5 -2
- package/dist/auth/ports/audit-sink.d.ts.map +1 -1
- package/dist/auth/ports/mfa-device-store.d.ts +7 -0
- package/dist/auth/ports/mfa-device-store.d.ts.map +1 -1
- package/dist/auth/profile-resolver.d.ts +6 -0
- package/dist/auth/profile-resolver.d.ts.map +1 -1
- package/dist/auth/rate-limit/attempt-limiter.d.ts +6 -0
- package/dist/auth/rate-limit/attempt-limiter.d.ts.map +1 -1
- package/dist/auth/rate-limit/attempt-limiter.js +14 -0
- package/dist/auth/tokens.d.ts +59 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +61 -1
- package/dist/authentication/credential-provisioner.d.ts +7 -0
- package/dist/authentication/credential-provisioner.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-provisioner.d.ts +1 -0
- package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-provisioner.js +7 -0
- package/dist/authentication/mariadb-credential-resolver.d.ts +23 -2
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +45 -1
- package/dist/authentication/mariadb-mfa-device-store.d.ts +7 -0
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-mfa-device-store.js +32 -0
- package/dist/authentication/mariadb-mfa-requirement-source.d.ts +55 -0
- package/dist/authentication/mariadb-mfa-requirement-source.d.ts.map +1 -0
- package/dist/authentication/mariadb-mfa-requirement-source.js +164 -0
- package/dist/authentication/mariadb-mfa-resetter.d.ts +42 -0
- package/dist/authentication/mariadb-mfa-resetter.d.ts.map +1 -0
- package/dist/authentication/mariadb-mfa-resetter.js +59 -0
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +5 -0
- package/dist/authentication/sign-in-with.d.ts +14 -0
- package/dist/authentication/sign-in-with.d.ts.map +1 -0
- package/dist/authentication/sign-in-with.js +36 -0
- package/dist/authz/core-access.d.ts +11 -0
- package/dist/authz/core-access.d.ts.map +1 -0
- package/dist/authz/core-access.js +14 -0
- package/dist/authz/list-scope-sql.d.ts.map +1 -1
- package/dist/authz/list-scope-sql.js +15 -5
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +21 -3
- package/dist/captcha/captcha.providers.d.ts +18 -0
- package/dist/captcha/captcha.providers.d.ts.map +1 -0
- package/dist/captcha/captcha.providers.js +78 -0
- package/dist/captcha/recaptcha-verifier.d.ts +20 -0
- package/dist/captcha/recaptcha-verifier.d.ts.map +1 -0
- package/dist/captcha/recaptcha-verifier.js +48 -0
- package/dist/captcha/siteverify.d.ts +47 -0
- package/dist/captcha/siteverify.d.ts.map +1 -0
- package/dist/captcha/siteverify.js +98 -0
- package/dist/captcha/turnstile-verifier.d.ts +18 -0
- package/dist/captcha/turnstile-verifier.d.ts.map +1 -0
- package/dist/captcha/turnstile-verifier.js +41 -0
- package/dist/config/captcha-config.d.ts +47 -0
- package/dist/config/captcha-config.d.ts.map +1 -0
- package/dist/config/captcha-config.js +163 -0
- package/dist/config/reserved-principal-ids.d.ts +15 -0
- package/dist/config/reserved-principal-ids.d.ts.map +1 -1
- package/dist/config/reserved-principal-ids.js +20 -3
- package/dist/enforcement/global-enforcement.guard.d.ts +9 -1
- package/dist/enforcement/global-enforcement.guard.d.ts.map +1 -1
- package/dist/enforcement/global-enforcement.guard.js +52 -10
- package/dist/enforcement/reenroll-allowlist.d.ts +5 -0
- package/dist/enforcement/reenroll-allowlist.d.ts.map +1 -1
- package/dist/enforcement/reenroll-allowlist.js +5 -0
- package/dist/identity/auth-facts.controller.d.ts +31 -4
- package/dist/identity/auth-facts.controller.d.ts.map +1 -1
- package/dist/identity/auth-facts.controller.js +46 -11
- package/dist/identity/identity-actions.d.ts +2 -2
- package/dist/identity/identity-actions.d.ts.map +1 -1
- package/dist/identity/module-registry.bridge.d.ts.map +1 -1
- package/dist/identity/module-registry.bridge.js +3 -6
- package/dist/identity/permission-facts.adapter.d.ts +22 -0
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +26 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -10
- package/dist/internal.d.ts +27 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +36 -1
- package/dist/module/shield.module.d.ts +27 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +10 -0
- package/dist/request-auth/request-principal.d.ts +8 -1
- package/dist/request-auth/request-principal.d.ts.map +1 -1
- package/dist/request-auth/request-principal.js +11 -4
- package/dist/request-auth/session-token-binder.d.ts +82 -0
- package/dist/request-auth/session-token-binder.d.ts.map +1 -1
- package/dist/request-auth/session-token-binder.js +203 -1
- package/dist/session/mfa-requirement-service.d.ts +168 -0
- package/dist/session/mfa-requirement-service.d.ts.map +1 -0
- package/dist/session/mfa-requirement-service.js +461 -0
- package/dist/session/session-restriction-store.d.ts +36 -0
- package/dist/session/session-restriction-store.d.ts.map +1 -1
- package/dist/session/session-restriction-store.js +57 -6
- package/dist/session/session-restriction.d.ts +22 -0
- package/dist/session/session-restriction.d.ts.map +1 -0
- package/dist/session/session-restriction.js +23 -0
- package/package.json +5 -5
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import type { FactorVerifier, PasswordHasher } from '@astram/core';
|
|
46
46
|
import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
47
|
+
import type { MfaRequirementService } from '../session/mfa-requirement-service.js';
|
|
47
48
|
import type { AuditSink } from './ports/audit-sink.js';
|
|
48
49
|
import type { EmailSender } from './ports/email-sender.js';
|
|
49
50
|
import type { MessageDispatcher } from '../messaging/message-dispatcher.js';
|
|
@@ -87,7 +88,8 @@ export declare class AccountController {
|
|
|
87
88
|
private readonly emailChangeLimiter?;
|
|
88
89
|
private readonly auditSink?;
|
|
89
90
|
private readonly messageDispatcher?;
|
|
90
|
-
|
|
91
|
+
private readonly mfaRequirement?;
|
|
92
|
+
constructor(binder?: SessionTokenBinder | undefined, hasher?: PasswordHasher | undefined, writer?: (CredentialWriter | null) | undefined, sweeper?: (SessionSweeper | null) | undefined, recoveryCodes?: (RecoveryCodeStore | null) | undefined, totpVerifier?: (FactorVerifier | null) | undefined, profileResolver?: (ProfileResolver | null) | undefined, profileWriter?: (ProfileWriter | null) | undefined, mfaDevices?: (MfaDeviceStore | null) | undefined, sessionRestrictions?: (SessionRestrictionStore | null) | undefined, loginTotpVerifier?: (FactorVerifier | null) | undefined, mfaLimiter?: (AttemptLimiter | null) | undefined, loginLimiter?: (AttemptLimiter | null) | undefined, emailChangeTokens?: (EmailChangeTokenStore | null) | undefined, emailAddresses?: (EmailAddressChanger | null) | undefined, emailSender?: (EmailSender | null) | undefined, emailChangeLinkBaseUrl?: string | null | undefined, emailChangeLimiter?: (AttemptLimiter | null) | undefined, auditSink?: (AuditSink | null) | undefined, messageDispatcher?: (MessageDispatcher | null) | undefined, mfaRequirement?: (MfaRequirementService | null) | undefined);
|
|
91
93
|
/**
|
|
92
94
|
* Resolve a human-readable otpauth account LABEL for the caller (email, else display
|
|
93
95
|
* name) via the profile resolver. Best-effort: no resolver / null profile / a thrown
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAmBH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAa,MAAM,cAAc,CAAC;AAK9E,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"account.controller.d.ts","sourceRoot":"","sources":["../../src/auth/account.controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -64,6 +64,8 @@ const classification_decorator_js_1 = require("../enforcement/classification.dec
|
|
|
64
64
|
const request_principal_js_1 = require("../request-auth/request-principal.js");
|
|
65
65
|
const tokens_js_1 = require("../request-auth/tokens.js");
|
|
66
66
|
const tokens_js_2 = require("./tokens.js");
|
|
67
|
+
const session_restriction_js_1 = require("../session/session-restriction.js");
|
|
68
|
+
const auth_audit_js_1 = require("./auth-audit.js");
|
|
67
69
|
const deliver_email_js_1 = require("../messaging/deliver-email.js");
|
|
68
70
|
const email_address_js_1 = require("./email-address.js");
|
|
69
71
|
const email_change_messages_js_1 = require("./email-change-messages.js");
|
|
@@ -169,6 +171,10 @@ function unprocessable(error) {
|
|
|
169
171
|
* `code`. Missing → this 400 (the client then asks for the code); a wrong code is the generic
|
|
170
172
|
* {@link badRequest}.
|
|
171
173
|
*/
|
|
174
|
+
/** R2.7: the 409 for removing the last device while a mandatory-2FA policy applies. */
|
|
175
|
+
function mfaRequiredByPolicy() {
|
|
176
|
+
throw new common_1.HttpException({ error: 'mfa_required_by_policy' }, common_1.HttpStatus.CONFLICT);
|
|
177
|
+
}
|
|
172
178
|
function nameTaken() {
|
|
173
179
|
throw new common_1.HttpException({ error: 'name_taken' }, common_1.HttpStatus.CONFLICT);
|
|
174
180
|
}
|
|
@@ -214,7 +220,8 @@ let AccountController = class AccountController {
|
|
|
214
220
|
emailChangeLimiter;
|
|
215
221
|
auditSink;
|
|
216
222
|
messageDispatcher;
|
|
217
|
-
|
|
223
|
+
mfaRequirement;
|
|
224
|
+
constructor(binder, hasher, writer, sweeper, recoveryCodes, totpVerifier, profileResolver, profileWriter, mfaDevices, sessionRestrictions, loginTotpVerifier, mfaLimiter, loginLimiter, emailChangeTokens, emailAddresses, emailSender, emailChangeLinkBaseUrl, emailChangeLimiter, auditSink, messageDispatcher, mfaRequirement) {
|
|
218
225
|
this.binder = binder;
|
|
219
226
|
this.hasher = hasher;
|
|
220
227
|
this.writer = writer;
|
|
@@ -235,6 +242,7 @@ let AccountController = class AccountController {
|
|
|
235
242
|
this.emailChangeLimiter = emailChangeLimiter;
|
|
236
243
|
this.auditSink = auditSink;
|
|
237
244
|
this.messageDispatcher = messageDispatcher;
|
|
245
|
+
this.mfaRequirement = mfaRequirement;
|
|
238
246
|
}
|
|
239
247
|
/**
|
|
240
248
|
* Resolve a human-readable otpauth account LABEL for the caller (email, else display
|
|
@@ -281,6 +289,11 @@ let AccountController = class AccountController {
|
|
|
281
289
|
const claims = await this.binder.resolve(token);
|
|
282
290
|
if (claims === null)
|
|
283
291
|
unauthorized();
|
|
292
|
+
// Auth capabilities R2.5: re-evaluate the mandatory-2FA requirement when the permission version
|
|
293
|
+
// moved (no DB query otherwise; a failure is the 503 `authentication_unavailable`).
|
|
294
|
+
if (this.mfaRequirement !== undefined && this.mfaRequirement !== null) {
|
|
295
|
+
return this.mfaRequirement.refresh(token, claims);
|
|
296
|
+
}
|
|
284
297
|
return claims;
|
|
285
298
|
}
|
|
286
299
|
/**
|
|
@@ -307,8 +320,11 @@ let AccountController = class AccountController {
|
|
|
307
320
|
* A normal (`mustReenroll:false`) session is unaffected — this is a no-op for it.
|
|
308
321
|
*/
|
|
309
322
|
enforceReenroll(claims, allowedDuringReenroll) {
|
|
310
|
-
|
|
311
|
-
|
|
323
|
+
// D-2: the same per-handler policy serves the mandatory-2FA restriction (`mfa_enrollment`,
|
|
324
|
+
// including a passed grace deadline), each reason with its own 403 code.
|
|
325
|
+
const reason = (0, session_restriction_js_1.restrictionOf)(claims, Date.now());
|
|
326
|
+
if (reason !== null && !allowedDuringReenroll) {
|
|
327
|
+
throw new common_1.HttpException({ error: (0, session_restriction_js_1.restrictionErrorCode)(reason) }, common_1.HttpStatus.FORBIDDEN);
|
|
312
328
|
}
|
|
313
329
|
}
|
|
314
330
|
/**
|
|
@@ -812,6 +828,24 @@ let AccountController = class AccountController {
|
|
|
812
828
|
if (claims.mustReenroll === true && this.sessionRestrictions) {
|
|
813
829
|
await this.sessionRestrictions.clearMustReenroll(claims.sessionId).catch(() => undefined);
|
|
814
830
|
}
|
|
831
|
+
// MANDATORY 2FA (R2.3, AC-7): a confirmed factor lifts the `mfa_enrollment` restriction and any
|
|
832
|
+
// grace deadline on THIS session at once (Redis claims + durable column; the very next request
|
|
833
|
+
// succeeds) and deletes the grace clock (R2.4). Best-effort like the re-enrol clear: a failure
|
|
834
|
+
// leaves the session restricted (the safe direction).
|
|
835
|
+
if (this.mfaRequirement !== undefined && this.mfaRequirement !== null) {
|
|
836
|
+
await this.mfaRequirement.onFactorConfirmed(claims);
|
|
837
|
+
}
|
|
838
|
+
else if ((claims.restriction === 'mfa_enrollment' || claims.mfaEnrollmentDeadlineMs !== undefined) &&
|
|
839
|
+
this.binder !== undefined &&
|
|
840
|
+
this.binder !== null) {
|
|
841
|
+
// The feature was switched off since the session was restricted: still lift it.
|
|
842
|
+
await this.binder.clearRestriction(claims.sessionId);
|
|
843
|
+
if (claims.restriction === 'mfa_enrollment') {
|
|
844
|
+
await this.sessionRestrictions?.clearMfaRestriction?.(claims.sessionId).catch(() => { });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// R12.2: the session's own actor and tenant; no device id, name, secret or code.
|
|
848
|
+
(0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.authEvent)(auth_audit_js_1.AUTH_MFA_ENROLLED, claims.id, claims.tenantId, { firstDevice: wasFirst }));
|
|
815
849
|
if (wasFirst) {
|
|
816
850
|
const recoveryCodes = await this.replaceRecoveryCodes(claims.id, recoveryStore, hasher);
|
|
817
851
|
return { recoveryCodes };
|
|
@@ -869,20 +903,57 @@ let AccountController = class AccountController {
|
|
|
869
903
|
// Phase 11 #1: the canonical device id or 400 (after the session, before any proof is spent).
|
|
870
904
|
const deviceId = (0, engine_1.canonicalUuid)(id) ?? badRequest();
|
|
871
905
|
const code = typeof body?.code === 'string' ? body.code : '';
|
|
906
|
+
// R2.7 (AC-11): the LAST confirmed device cannot be removed while a mandatory-2FA policy
|
|
907
|
+
// applies to the caller. The requirement is evaluated first (DB reads are fine here; a failure
|
|
908
|
+
// is 503), then a cheap pre-check refuses before any proof is spent (no code is burnt), and the
|
|
909
|
+
// delete itself re-checks under a per-principal row lock (`removeKeepingOne`), so two
|
|
910
|
+
// concurrent removals can never leave zero devices.
|
|
911
|
+
const mfa = this.mfaRequirement;
|
|
912
|
+
let policyApplies = false;
|
|
913
|
+
if (mfa !== undefined && mfa !== null) {
|
|
914
|
+
try {
|
|
915
|
+
policyApplies = await mfa.requiresFactor(claims);
|
|
916
|
+
}
|
|
917
|
+
catch {
|
|
918
|
+
retryableUnavailable();
|
|
919
|
+
}
|
|
920
|
+
if (policyApplies) {
|
|
921
|
+
const confirmed = (await store.listAll(claims.id)).filter((d) => d.confirmed);
|
|
922
|
+
if (confirmed.length === 1 && confirmed[0].id === deviceId)
|
|
923
|
+
mfaRequiredByPolicy();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
872
926
|
// Proof BEFORE the delete: a TOTP from any confirmed device (incl. the one being
|
|
873
927
|
// removed — legit for the last-device disable) OR a valid recovery code. Rate-limited
|
|
874
928
|
// and replay-guarded like `/auth/mfa` (Phase 8 #1).
|
|
875
929
|
await this.requireMfaProof(store, claims.id, code);
|
|
876
|
-
|
|
930
|
+
let removed;
|
|
931
|
+
if (policyApplies && typeof store.removeKeepingOne === 'function') {
|
|
932
|
+
const outcome = await store.removeKeepingOne(claims.id, deviceId);
|
|
933
|
+
if (outcome === 'last_device')
|
|
934
|
+
mfaRequiredByPolicy();
|
|
935
|
+
removed = outcome === 'removed';
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
removed = await store.remove(claims.id, deviceId);
|
|
939
|
+
}
|
|
877
940
|
if (!removed) {
|
|
878
941
|
throw new common_1.HttpException({ error: 'not_found' }, common_1.HttpStatus.NOT_FOUND);
|
|
879
942
|
}
|
|
880
943
|
// Removing the last confirmed device disables MFA → wipe the account recovery codes.
|
|
881
|
-
|
|
944
|
+
const mfaOff = (await store.countConfirmed(claims.id)) === 0;
|
|
945
|
+
// R2.7 defence in depth: a store without the atomic removal (or any other path) still left no
|
|
946
|
+
// factor while a policy applies → restrict THIS session now, not at the next sign-in.
|
|
947
|
+
if (mfaOff && policyApplies && mfa !== undefined && mfa !== null) {
|
|
948
|
+
await mfa.restrictNow(claims);
|
|
949
|
+
}
|
|
950
|
+
if (mfaOff) {
|
|
882
951
|
if (this.recoveryCodes !== undefined && this.recoveryCodes !== null) {
|
|
883
952
|
await this.recoveryCodes.replaceAll(claims.id, []);
|
|
884
953
|
}
|
|
885
954
|
}
|
|
955
|
+
// R12.2: the session's own actor and tenant; no device id, secret or code.
|
|
956
|
+
(0, auth_audit_js_1.emitAuthAudit)(this.auditSink, (0, auth_audit_js_1.authEvent)(auth_audit_js_1.AUTH_MFA_REMOVED, claims.id, claims.tenantId, { mfaOff }));
|
|
886
957
|
}
|
|
887
958
|
/**
|
|
888
959
|
* `POST /account/mfa/recovery-codes/regenerate` — mint a fresh recovery-code batch
|
|
@@ -1045,5 +1116,7 @@ exports.AccountController = AccountController = __decorate([
|
|
|
1045
1116
|
__param(18, (0, common_1.Inject)(tokens_js_2.SHIELD_AUDIT_SINK)),
|
|
1046
1117
|
__param(19, (0, common_1.Optional)()),
|
|
1047
1118
|
__param(19, (0, common_1.Inject)(tokens_js_2.SHIELD_MESSAGE_DISPATCHER)),
|
|
1048
|
-
|
|
1119
|
+
__param(20, (0, common_1.Optional)()),
|
|
1120
|
+
__param(20, (0, common_1.Inject)(tokens_js_2.SHIELD_MFA_REQUIREMENT)),
|
|
1121
|
+
__metadata("design:paramtypes", [Function, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object])
|
|
1049
1122
|
], AccountController);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth capabilities R11 (task 18, B-5): the guard pieces behind `@astram/identity`'s admin sign-in
|
|
3
|
+
* controls (`POST /identity/users/:uuid/{reset-mfa,unlock,end-sessions}`). Guard never imports
|
|
4
|
+
* identity (ADR-0003): identity injects these through the tokens re-exported from
|
|
5
|
+
* `@astram/guard/internal` ({@link SHIELD_ADMIN_STEP_UP}, {@link SHIELD_ACCOUNT_UNLOCKER}).
|
|
6
|
+
*
|
|
7
|
+
* ── STEP-UP (R11.2) ─────────────────────────────────────────────────────────
|
|
8
|
+
* The admin's OWN current TOTP, checked the way the code-gated `/account` routes check it
|
|
9
|
+
* (`AccountController.requireMfaProof`): the attempt is counted on the admin's `mfa:pid:<id>`
|
|
10
|
+
* budget BEFORE verifying (atomic increment-first), the code is verified against every confirmed
|
|
11
|
+
* device through the replay-guarded login verifier (an accepted code is spent on every path), a
|
|
12
|
+
* success resets the budget. TOTP only: recovery codes are not accepted here (R11.2). The login
|
|
13
|
+
* step-up (`mfa-step-up.ts`) needs a pending password challenge, so it is not reused directly.
|
|
14
|
+
* No confirmed device ⇒ `no_factor` before anything is counted.
|
|
15
|
+
*
|
|
16
|
+
* ── UNLOCK (R11.1) ──────────────────────────────────────────────────────────
|
|
17
|
+
* Clears `login:pid:<id>`, `mfa:pid:<id>`, `recovery:<id>` (each on its own limiter) and the
|
|
18
|
+
* account CAPTCHA counter of every CAPTCHA endpoint (`shield:captcha:<ep>:pid:<id>`). The
|
|
19
|
+
* typed-text CAPTCHA keys hash the text a user typed and cannot be derived from the account; they
|
|
20
|
+
* expire with their window. Any Redis error rejects ({@link AdminControlUnavailableError}).
|
|
21
|
+
*/
|
|
22
|
+
import type { FactorVerifier } from '@astram/core';
|
|
23
|
+
import type { MfaDeviceStore } from './ports/mfa-device-store.js';
|
|
24
|
+
import type { AttemptLimiter } from './rate-limit/attempt-limiter.js';
|
|
25
|
+
/** A store the admin control needs could not answer; nothing is claimed (caller → 503). */
|
|
26
|
+
export declare class AdminControlUnavailableError extends Error {
|
|
27
|
+
constructor();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The step-up verdict:
|
|
31
|
+
* - `verified`: the code is right (budget reset);
|
|
32
|
+
* - `no_factor`: the admin has no confirmed device (caller → 409 `mfa_required`), nothing counted;
|
|
33
|
+
* - `missing`: blank code, nothing counted;
|
|
34
|
+
* - `rejected`: wrong code (stays counted);
|
|
35
|
+
* - `locked`: the admin's `mfa:pid` budget is spent;
|
|
36
|
+
* - `unavailable`: a store could not answer (fail closed).
|
|
37
|
+
*/
|
|
38
|
+
export type AdminStepUpOutcome = 'verified' | 'no_factor' | 'missing' | 'rejected' | 'locked' | 'unavailable';
|
|
39
|
+
/** The seam identity injects under {@link SHIELD_ADMIN_STEP_UP}. */
|
|
40
|
+
export interface AdminStepUp {
|
|
41
|
+
verify(principalId: string, code: string): Promise<AdminStepUpOutcome>;
|
|
42
|
+
}
|
|
43
|
+
export interface TotpAdminStepUpDeps {
|
|
44
|
+
readonly devices: Pick<MfaDeviceStore, 'listConfirmedSecrets'> | null;
|
|
45
|
+
/** The replay-guarded login TOTP verifier (`SHIELD_LOGIN_TOTP_VERIFIER`). */
|
|
46
|
+
readonly verifier: FactorVerifier | null;
|
|
47
|
+
/** The MFA step limiter (`SHIELD_MFA_LIMITER`, `mfa:pid:<id>`). */
|
|
48
|
+
readonly limiter: Pick<AttemptLimiter, 'consume' | 'refund' | 'reset'> | null;
|
|
49
|
+
}
|
|
50
|
+
export declare class TotpAdminStepUp implements AdminStepUp {
|
|
51
|
+
private readonly deps;
|
|
52
|
+
constructor(deps: TotpAdminStepUpDeps);
|
|
53
|
+
verify(principalId: string, code: string): Promise<AdminStepUpOutcome>;
|
|
54
|
+
}
|
|
55
|
+
/** The keys one unlock clears (limiter keys without the `shield:attempts:` namespace). */
|
|
56
|
+
export declare function accountUnlockKeys(principalId: string): {
|
|
57
|
+
login: string;
|
|
58
|
+
mfa: string;
|
|
59
|
+
recovery: string;
|
|
60
|
+
captcha: string[];
|
|
61
|
+
};
|
|
62
|
+
/** The seam identity injects under {@link SHIELD_ACCOUNT_UNLOCKER}. */
|
|
63
|
+
export interface AccountUnlocker {
|
|
64
|
+
/** Clear every sign-in counter of the principal; rejects {@link AdminControlUnavailableError}. */
|
|
65
|
+
unlock(principalId: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface RedisAccountUnlockerDeps {
|
|
68
|
+
readonly login: Pick<AttemptLimiter, 'clear'>;
|
|
69
|
+
readonly mfa: Pick<AttemptLimiter, 'clear'>;
|
|
70
|
+
readonly recovery: Pick<AttemptLimiter, 'clear'>;
|
|
71
|
+
readonly captcha: {
|
|
72
|
+
clearStrict(keys: readonly string[]): Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export declare class RedisAccountUnlocker implements AccountUnlocker {
|
|
76
|
+
private readonly deps;
|
|
77
|
+
constructor(deps: RedisAccountUnlockerDeps);
|
|
78
|
+
unlock(principalId: string): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=admin-signin-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-signin-controls.d.ts","sourceRoot":"","sources":["../../src/auth/admin-signin-controls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,2FAA2F;AAC3F,qBAAa,4BAA6B,SAAQ,KAAK;;CAKtD;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAC5B,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE/E,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,sBAAsB,CAAC,GAAG,IAAI,CAAC;IACtE,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/E;AAED,qBAAa,eAAgB,YAAW,WAAW;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,mBAAmB;IAEhD,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAmC7E;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAOA;AAED,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,kGAAkG;IAClG,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE;QAAE,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;CAC3E;AAED,qBAAa,oBAAqB,YAAW,eAAe;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,wBAAwB;IAErD,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAcjD"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisAccountUnlocker = exports.TotpAdminStepUp = exports.AdminControlUnavailableError = void 0;
|
|
4
|
+
exports.accountUnlockKeys = accountUnlockKeys;
|
|
5
|
+
const captcha_config_js_1 = require("../config/captcha-config.js");
|
|
6
|
+
/** A store the admin control needs could not answer; nothing is claimed (caller → 503). */
|
|
7
|
+
class AdminControlUnavailableError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('admin sign-in control unavailable');
|
|
10
|
+
this.name = 'AdminControlUnavailableError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.AdminControlUnavailableError = AdminControlUnavailableError;
|
|
14
|
+
class TotpAdminStepUp {
|
|
15
|
+
deps;
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
}
|
|
19
|
+
async verify(principalId, code) {
|
|
20
|
+
const { devices, verifier, limiter } = this.deps;
|
|
21
|
+
if (!devices || !verifier || !limiter || !principalId)
|
|
22
|
+
return 'unavailable';
|
|
23
|
+
let secrets;
|
|
24
|
+
try {
|
|
25
|
+
secrets = await devices.listConfirmedSecrets(principalId);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return 'unavailable';
|
|
29
|
+
}
|
|
30
|
+
if (!Array.isArray(secrets) || secrets.length === 0)
|
|
31
|
+
return 'no_factor';
|
|
32
|
+
const presented = typeof code === 'string' ? code.trim() : '';
|
|
33
|
+
if (presented === '')
|
|
34
|
+
return 'missing';
|
|
35
|
+
const key = `mfa:pid:${principalId}`;
|
|
36
|
+
let allowed;
|
|
37
|
+
try {
|
|
38
|
+
({ allowed } = await limiter.consume(key));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 'unavailable';
|
|
42
|
+
}
|
|
43
|
+
if (!allowed)
|
|
44
|
+
return 'locked';
|
|
45
|
+
let satisfied;
|
|
46
|
+
try {
|
|
47
|
+
satisfied = (await verifier.verify({ factor: 'totp', payload: { code: presented } }, secrets.join('\n')))?.satisfied;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// No verdict (replay guard / Redis down): give the attempt back, fail closed.
|
|
51
|
+
await limiter.refund(key);
|
|
52
|
+
return 'unavailable';
|
|
53
|
+
}
|
|
54
|
+
if (satisfied !== true)
|
|
55
|
+
return 'rejected';
|
|
56
|
+
await limiter.reset(key);
|
|
57
|
+
return 'verified';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.TotpAdminStepUp = TotpAdminStepUp;
|
|
61
|
+
/** The keys one unlock clears (limiter keys without the `shield:attempts:` namespace). */
|
|
62
|
+
function accountUnlockKeys(principalId) {
|
|
63
|
+
return {
|
|
64
|
+
login: `login:pid:${principalId}`,
|
|
65
|
+
mfa: `mfa:pid:${principalId}`,
|
|
66
|
+
recovery: `recovery:${principalId}`,
|
|
67
|
+
captcha: captcha_config_js_1.CAPTCHA_ENDPOINTS.map((ep) => `${ep}:pid:${principalId}`),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
class RedisAccountUnlocker {
|
|
71
|
+
deps;
|
|
72
|
+
constructor(deps) {
|
|
73
|
+
this.deps = deps;
|
|
74
|
+
}
|
|
75
|
+
async unlock(principalId) {
|
|
76
|
+
if (typeof principalId !== 'string' || principalId === '') {
|
|
77
|
+
throw new AdminControlUnavailableError();
|
|
78
|
+
}
|
|
79
|
+
const keys = accountUnlockKeys(principalId);
|
|
80
|
+
try {
|
|
81
|
+
await this.deps.login.clear(keys.login);
|
|
82
|
+
await this.deps.mfa.clear(keys.mfa);
|
|
83
|
+
await this.deps.recovery.clear(keys.recovery);
|
|
84
|
+
await this.deps.captcha.clearStrict(keys.captcha);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new AdminControlUnavailableError();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.RedisAccountUnlocker = RedisAccountUnlocker;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth audit events and counters (R12.2–R12.4, AC-18).
|
|
3
|
+
*
|
|
4
|
+
* Events (through the auth {@link AuditSink}, fire-and-forget, errors swallowed):
|
|
5
|
+
* - `auth.login.succeeded` / `auth.login.failed` (`authentication`), attribute `method`:
|
|
6
|
+
* `password`, or the MFA step's `totp` / `recovery_code`.
|
|
7
|
+
* - `auth.logout` (`session_lifecycle`), attribute `proven`.
|
|
8
|
+
* - `auth.mfa.enrolled` / `auth.mfa.removed` (`authentication`).
|
|
9
|
+
*
|
|
10
|
+
* Actor and tenant always come from the RESOLVED principal or session, never from the request.
|
|
11
|
+
* No event carries the typed hint, a password, a code or a token.
|
|
12
|
+
*
|
|
13
|
+
* Bounding (R12.3): a failure row is written only for a resolved account, and only by the attempt
|
|
14
|
+
* that OPENS the account's limiter window ({@link opensFailureWindow}: the increment-first count is
|
|
15
|
+
* 1). The limiter's counter is atomic (Lua `INCR`), so of N parallel failures exactly one sees 1,
|
|
16
|
+
* and the window is fixed (not extended by later attempts), so an account yields at most one
|
|
17
|
+
* failure row per lockout window, plus the existing lockout row on the edge. No extra Redis key and
|
|
18
|
+
* no extra round trip; with no limiter there is no window and no failure row. Unknown names write
|
|
19
|
+
* no row at all; they only count.
|
|
20
|
+
*
|
|
21
|
+
* Counters (R12.4): one structured log line per occurrence with fixed keys and no personal data,
|
|
22
|
+
* the style of `recordUnprovenLogout` (`logout-proof.ts`):
|
|
23
|
+
* - `{ counter: 'auth.login.failed', method, account: 'known' | 'unknown' }`
|
|
24
|
+
* - `{ counter: 'auth.lockout', method, account: 'known' | 'unknown' | 'ip' }`
|
|
25
|
+
*/
|
|
26
|
+
import { Logger } from '@nestjs/common';
|
|
27
|
+
import type { AuditSink, AuthAuditEvent } from './ports/audit-sink.js';
|
|
28
|
+
/** The sign-in factor an event or counter refers to. */
|
|
29
|
+
export type AuthMethod = 'password' | 'totp' | 'recovery_code';
|
|
30
|
+
/** Whether the failing name resolved to an account (`ip` = the per-IP lockout, no account). */
|
|
31
|
+
export type AccountLabel = 'known' | 'unknown' | 'ip';
|
|
32
|
+
export declare const AUTH_LOGIN_SUCCEEDED = "auth.login.succeeded";
|
|
33
|
+
export declare const AUTH_LOGIN_FAILED = "auth.login.failed";
|
|
34
|
+
export declare const AUTH_LOGOUT = "auth.logout";
|
|
35
|
+
export declare const AUTH_MFA_ENROLLED = "auth.mfa.enrolled";
|
|
36
|
+
export declare const AUTH_MFA_REMOVED = "auth.mfa.removed";
|
|
37
|
+
/** Counter keys (R12.4). */
|
|
38
|
+
export declare const LOGIN_FAILED_COUNTER = "auth.login.failed";
|
|
39
|
+
export declare const LOCKOUT_COUNTER = "auth.lockout";
|
|
40
|
+
/** Record one event, fire-and-forget. A missing sink is a no-op; a sink error never propagates. */
|
|
41
|
+
export declare function emitAuthAudit(sink: AuditSink | null | undefined, event: AuthAuditEvent): void;
|
|
42
|
+
/** An `authentication` event about `actorId` in `tenantId`. */
|
|
43
|
+
export declare function authEvent(action: string, actorId: string, tenantId: string, attributes: Record<string, unknown>, nowMs?: number): AuthAuditEvent;
|
|
44
|
+
/** The `auth.logout` event (`session_lifecycle`) for a revoked session. */
|
|
45
|
+
export declare function logoutEvent(actorId: string, tenantId: string, proven: boolean, nowMs?: number): AuthAuditEvent;
|
|
46
|
+
/**
|
|
47
|
+
* True when this failed attempt opened the account's limiter window (its increment-first count
|
|
48
|
+
* is 1), so it may write the window's one `auth.login.failed` row. `null` (no limiter) → false.
|
|
49
|
+
*/
|
|
50
|
+
export declare function opensFailureWindow(verdict: {
|
|
51
|
+
count: number;
|
|
52
|
+
} | null | undefined): boolean;
|
|
53
|
+
/** Count one failed sign-in (R12.4). */
|
|
54
|
+
export declare function countLoginFailure(method: AuthMethod, account: 'known' | 'unknown', logger?: Pick<Logger, 'log'>): void;
|
|
55
|
+
/** Count one lockout (the edge where a limiter trips; R12.4). */
|
|
56
|
+
export declare function countLockout(method: AuthMethod, account: AccountLabel, logger?: Pick<Logger, 'log'>): void;
|
|
57
|
+
/** The tenant id in resolved credential claims, or `null`. */
|
|
58
|
+
export declare function tenantOfClaims(claims: unknown): string | null;
|
|
59
|
+
//# sourceMappingURL=auth-audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-audit.d.ts","sourceRoot":"","sources":["../../src/auth/auth-audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvE,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,MAAM,GAAG,eAAe,CAAC;AAE/D,+FAA+F;AAC/F,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;AAEtD,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAC3D,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AACrD,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AACrD,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD,4BAA4B;AAC5B,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AACxD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAI9C,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAS7F;AAED,+DAA+D;AAC/D,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,KAAK,GAAE,MAAmB,GACzB,cAAc,CAEhB;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,KAAK,GAAE,MAAmB,GACzB,cAAc,CAShB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEzF;AAED,wCAAwC;AACxC,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAiB,GAC1C,IAAI,CAEN;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAC1B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,YAAY,EACrB,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAiB,GAC1C,IAAI,CAEN;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG7D"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth audit events and counters (R12.2–R12.4, AC-18).
|
|
4
|
+
*
|
|
5
|
+
* Events (through the auth {@link AuditSink}, fire-and-forget, errors swallowed):
|
|
6
|
+
* - `auth.login.succeeded` / `auth.login.failed` (`authentication`), attribute `method`:
|
|
7
|
+
* `password`, or the MFA step's `totp` / `recovery_code`.
|
|
8
|
+
* - `auth.logout` (`session_lifecycle`), attribute `proven`.
|
|
9
|
+
* - `auth.mfa.enrolled` / `auth.mfa.removed` (`authentication`).
|
|
10
|
+
*
|
|
11
|
+
* Actor and tenant always come from the RESOLVED principal or session, never from the request.
|
|
12
|
+
* No event carries the typed hint, a password, a code or a token.
|
|
13
|
+
*
|
|
14
|
+
* Bounding (R12.3): a failure row is written only for a resolved account, and only by the attempt
|
|
15
|
+
* that OPENS the account's limiter window ({@link opensFailureWindow}: the increment-first count is
|
|
16
|
+
* 1). The limiter's counter is atomic (Lua `INCR`), so of N parallel failures exactly one sees 1,
|
|
17
|
+
* and the window is fixed (not extended by later attempts), so an account yields at most one
|
|
18
|
+
* failure row per lockout window, plus the existing lockout row on the edge. No extra Redis key and
|
|
19
|
+
* no extra round trip; with no limiter there is no window and no failure row. Unknown names write
|
|
20
|
+
* no row at all; they only count.
|
|
21
|
+
*
|
|
22
|
+
* Counters (R12.4): one structured log line per occurrence with fixed keys and no personal data,
|
|
23
|
+
* the style of `recordUnprovenLogout` (`logout-proof.ts`):
|
|
24
|
+
* - `{ counter: 'auth.login.failed', method, account: 'known' | 'unknown' }`
|
|
25
|
+
* - `{ counter: 'auth.lockout', method, account: 'known' | 'unknown' | 'ip' }`
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.LOCKOUT_COUNTER = exports.LOGIN_FAILED_COUNTER = exports.AUTH_MFA_REMOVED = exports.AUTH_MFA_ENROLLED = exports.AUTH_LOGOUT = exports.AUTH_LOGIN_FAILED = exports.AUTH_LOGIN_SUCCEEDED = void 0;
|
|
29
|
+
exports.emitAuthAudit = emitAuthAudit;
|
|
30
|
+
exports.authEvent = authEvent;
|
|
31
|
+
exports.logoutEvent = logoutEvent;
|
|
32
|
+
exports.opensFailureWindow = opensFailureWindow;
|
|
33
|
+
exports.countLoginFailure = countLoginFailure;
|
|
34
|
+
exports.countLockout = countLockout;
|
|
35
|
+
exports.tenantOfClaims = tenantOfClaims;
|
|
36
|
+
const common_1 = require("@nestjs/common");
|
|
37
|
+
exports.AUTH_LOGIN_SUCCEEDED = 'auth.login.succeeded';
|
|
38
|
+
exports.AUTH_LOGIN_FAILED = 'auth.login.failed';
|
|
39
|
+
exports.AUTH_LOGOUT = 'auth.logout';
|
|
40
|
+
exports.AUTH_MFA_ENROLLED = 'auth.mfa.enrolled';
|
|
41
|
+
exports.AUTH_MFA_REMOVED = 'auth.mfa.removed';
|
|
42
|
+
/** Counter keys (R12.4). */
|
|
43
|
+
exports.LOGIN_FAILED_COUNTER = 'auth.login.failed';
|
|
44
|
+
exports.LOCKOUT_COUNTER = 'auth.lockout';
|
|
45
|
+
const counterLogger = new common_1.Logger('ShieldAuth');
|
|
46
|
+
/** Record one event, fire-and-forget. A missing sink is a no-op; a sink error never propagates. */
|
|
47
|
+
function emitAuthAudit(sink, event) {
|
|
48
|
+
if (sink === undefined || sink === null)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
void sink.record(event).catch(() => {
|
|
52
|
+
/* best-effort: an audit failure never changes the response */
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* a synchronous throw from a consumer sink is swallowed too */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** An `authentication` event about `actorId` in `tenantId`. */
|
|
60
|
+
function authEvent(action, actorId, tenantId, attributes, nowMs = Date.now()) {
|
|
61
|
+
return { type: 'authentication', occurredAtMs: nowMs, actorId, tenantId, action, attributes };
|
|
62
|
+
}
|
|
63
|
+
/** The `auth.logout` event (`session_lifecycle`) for a revoked session. */
|
|
64
|
+
function logoutEvent(actorId, tenantId, proven, nowMs = Date.now()) {
|
|
65
|
+
return {
|
|
66
|
+
type: 'session_lifecycle',
|
|
67
|
+
occurredAtMs: nowMs,
|
|
68
|
+
actorId,
|
|
69
|
+
tenantId,
|
|
70
|
+
action: exports.AUTH_LOGOUT,
|
|
71
|
+
attributes: { proven },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* True when this failed attempt opened the account's limiter window (its increment-first count
|
|
76
|
+
* is 1), so it may write the window's one `auth.login.failed` row. `null` (no limiter) → false.
|
|
77
|
+
*/
|
|
78
|
+
function opensFailureWindow(verdict) {
|
|
79
|
+
return verdict !== null && verdict !== undefined && verdict.count === 1;
|
|
80
|
+
}
|
|
81
|
+
/** Count one failed sign-in (R12.4). */
|
|
82
|
+
function countLoginFailure(method, account, logger = counterLogger) {
|
|
83
|
+
logger.log({ counter: exports.LOGIN_FAILED_COUNTER, method, account });
|
|
84
|
+
}
|
|
85
|
+
/** Count one lockout (the edge where a limiter trips; R12.4). */
|
|
86
|
+
function countLockout(method, account, logger = counterLogger) {
|
|
87
|
+
logger.log({ counter: exports.LOCKOUT_COUNTER, method, account });
|
|
88
|
+
}
|
|
89
|
+
/** The tenant id in resolved credential claims, or `null`. */
|
|
90
|
+
function tenantOfClaims(claims) {
|
|
91
|
+
const t = claims?.tenantId;
|
|
92
|
+
return typeof t === 'string' && t !== '' ? t : null;
|
|
93
|
+
}
|