@vsaas/loopback 12.1.1 → 12.2.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/README.md +6 -0
- package/common/models/rate-limit-state.json +121 -0
- package/common/models/two-factor-challenge.json +112 -0
- package/common/models/two-factor-credential.json +117 -0
- package/common/models/two-factor-recovery-code.json +63 -0
- package/common/models/user.json +30 -0
- package/dist/_virtual/_rolldown/runtime.cjs +9 -2
- package/dist/common/models/access-token.d.cts +3 -0
- package/dist/common/models/acl.cjs +1 -3
- package/dist/common/models/acl.d.cts +1 -0
- package/dist/common/models/application.d.cts +3 -0
- package/dist/common/models/change.cjs +21 -18
- package/dist/common/models/change.d.cts +1 -0
- package/dist/common/models/checkpoint.d.cts +3 -0
- package/dist/common/models/email.d.cts +3 -0
- package/dist/common/models/key-value-model.d.cts +3 -0
- package/dist/common/models/rate-limit-state.cjs +7 -0
- package/dist/common/models/rate-limit-state.d.cts +3 -0
- package/dist/common/models/rate-limit-state2.cjs +109 -0
- package/dist/common/models/role-mapping.d.cts +3 -0
- package/dist/common/models/role.cjs +12 -8
- package/dist/common/models/role.d.cts +1 -0
- package/dist/common/models/scope.d.cts +3 -0
- package/dist/common/models/two-factor-challenge.cjs +7 -0
- package/dist/common/models/two-factor-challenge.d.cts +3 -0
- package/dist/common/models/two-factor-challenge2.cjs +108 -0
- package/dist/common/models/two-factor-credential.cjs +7 -0
- package/dist/common/models/two-factor-credential.d.cts +3 -0
- package/dist/common/models/two-factor-credential2.cjs +103 -0
- package/dist/common/models/two-factor-recovery-code.cjs +7 -0
- package/dist/common/models/two-factor-recovery-code.d.cts +3 -0
- package/dist/common/models/two-factor-recovery-code2.cjs +57 -0
- package/dist/common/models/user.cjs +261 -21
- package/dist/common/models/user.d.cts +3 -0
- package/dist/common/models/user2.cjs +30 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +3 -0
- package/dist/lib/access-context.d.cts +33 -0
- package/dist/lib/application.d.cts +1 -0
- package/dist/lib/authentication-security-initializer.cjs +351 -0
- package/dist/lib/authentication-security-initializer.d.cts +4 -0
- package/dist/lib/builtin-models.cjs +12 -0
- package/dist/lib/builtin-models.d.cts +3 -0
- package/dist/lib/configure-shared-methods.cjs +7 -4
- package/dist/lib/configure-shared-methods.d.cts +3 -0
- package/dist/lib/connectors/base-connector.d.cts +6 -0
- package/dist/lib/connectors/mail-direct-transport.cjs +26 -23
- package/dist/lib/connectors/mail-direct-transport.d.cts +22 -0
- package/dist/lib/connectors/mail-stub-transport.d.cts +14 -0
- package/dist/lib/connectors/mail.d.cts +7 -0
- package/dist/lib/connectors/memory.d.cts +6 -0
- package/dist/lib/current-context.d.cts +3 -0
- package/dist/lib/globalize.d.cts +10 -0
- package/dist/lib/loopback.cjs +6 -0
- package/dist/lib/loopback.d.cts +1 -0
- package/dist/lib/model.d.cts +1 -0
- package/dist/lib/persisted-model.cjs +7 -12
- package/dist/lib/persisted-model.d.cts +1 -0
- package/dist/lib/rate-limit/algorithms/common.cjs +37 -0
- package/dist/lib/rate-limit/algorithms/failure-lockout.cjs +70 -0
- package/dist/lib/rate-limit/algorithms/fixed-window.cjs +61 -0
- package/dist/lib/rate-limit/algorithms/index.cjs +55 -0
- package/dist/lib/rate-limit/config.cjs +107 -0
- package/dist/lib/rate-limit/errors.cjs +58 -0
- package/dist/lib/rate-limit/index.cjs +15 -0
- package/dist/lib/rate-limit/index.d.cts +1 -0
- package/dist/lib/rate-limit/initializer.cjs +92 -0
- package/dist/lib/rate-limit/initializer.d.cts +4 -0
- package/dist/lib/rate-limit/limiter.cjs +368 -0
- package/dist/lib/rate-limit/registry.cjs +92 -0
- package/dist/lib/rate-limit/remote.cjs +180 -0
- package/dist/lib/rate-limit/remote.d.cts +6 -0
- package/dist/lib/rate-limit/stores/persisted-model.cjs +353 -0
- package/dist/lib/rate-limit/subject.cjs +127 -0
- package/dist/lib/rate-limit/types.cjs +1 -0
- package/dist/lib/rate-limit/types.d.cts +167 -0
- package/dist/lib/registry.d.cts +1 -0
- package/dist/lib/runtime.d.cts +10 -0
- package/dist/lib/server-app.d.cts +3 -0
- package/dist/lib/two-factor/config.cjs +199 -0
- package/dist/lib/two-factor/config.d.cts +55 -0
- package/dist/lib/two-factor/crypto.cjs +68 -0
- package/dist/lib/two-factor/crypto.d.cts +16 -0
- package/dist/lib/two-factor/errors.cjs +58 -0
- package/dist/lib/two-factor/errors.d.cts +18 -0
- package/dist/lib/two-factor/rate-limit.cjs +153 -0
- package/dist/lib/two-factor/rate-limit.d.cts +22 -0
- package/dist/lib/two-factor/recovery-codes.cjs +49 -0
- package/dist/lib/two-factor/recovery-codes.d.cts +16 -0
- package/dist/lib/two-factor/service.cjs +1108 -0
- package/dist/lib/two-factor/service.d.cts +78 -0
- package/dist/lib/two-factor/totp.cjs +50 -0
- package/dist/lib/two-factor/totp.d.cts +14 -0
- package/dist/lib/utils.d.cts +36 -0
- package/dist/package.cjs +46 -10
- package/dist/server/middleware/context.d.cts +3 -0
- package/dist/server/middleware/error-handler.d.cts +3 -0
- package/dist/server/middleware/favicon.d.cts +7 -0
- package/dist/server/middleware/rest.d.cts +6 -0
- package/dist/server/middleware/static.d.cts +3 -0
- package/dist/server/middleware/status.d.cts +19 -0
- package/dist/server/middleware/token.d.cts +3 -0
- package/dist/server/middleware/url-not-found.d.cts +13 -0
- package/docs/api-explorer-details.md +110 -0
- package/docs/application-guide.md +1110 -0
- package/docs/assets/explorer-api.png +0 -0
- package/docs/assets/explorer-endpoint.png +0 -0
- package/docs/assets/explorer-listing.png +0 -0
- package/docs/assets/explorer-req-res.png +0 -0
- package/docs/assets/lb-modules.png +0 -0
- package/docs/assets/loopback-architecture.png +0 -0
- package/docs/assets/loopback-concepts.png +0 -0
- package/docs/assets/loopback_ov.png +0 -0
- package/docs/rate-limiting.md +879 -0
- package/docs/two-factor-authentication.md +611 -0
- package/docs/two-factor-authenticator-loss-runbook.md +117 -0
- package/package.json +56 -20
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_lib_two_factor_errors = require("./errors.cjs");
|
|
4
|
+
const require_lib_two_factor_crypto = require("./crypto.cjs");
|
|
5
|
+
const require_lib_two_factor_totp = require("./totp.cjs");
|
|
6
|
+
const require_lib_two_factor_recovery_codes = require("./recovery-codes.cjs");
|
|
7
|
+
//#region src/lib/two-factor/service.ts
|
|
8
|
+
function count(result) {
|
|
9
|
+
return typeof result === "number" ? result : Number(result?.count || 0);
|
|
10
|
+
}
|
|
11
|
+
function optionalCallback(promise, callback) {
|
|
12
|
+
if (typeof callback !== "function") return promise;
|
|
13
|
+
promise.then((result) => callback(null, result), callback);
|
|
14
|
+
}
|
|
15
|
+
function invoke(target, name, args) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let completed = false;
|
|
18
|
+
const done = (error, value) => {
|
|
19
|
+
if (completed) return;
|
|
20
|
+
completed = true;
|
|
21
|
+
error ? reject(error) : resolve(value);
|
|
22
|
+
};
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
result = target[name](...args, done);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
done(error);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (result && typeof result.then === "function") result.then((value) => done(null, value), done);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function includeUser(include) {
|
|
34
|
+
if (Array.isArray(include)) return include.map(String).map((v) => v.toLowerCase()).includes("user");
|
|
35
|
+
return String(include || "").toLowerCase() === "user";
|
|
36
|
+
}
|
|
37
|
+
function loginError() {
|
|
38
|
+
const error = /* @__PURE__ */ new Error("login failed");
|
|
39
|
+
error.statusCode = 401;
|
|
40
|
+
error.code = "LOGIN_FAILED";
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
var TwoFactorService;
|
|
44
|
+
var init_service = require_runtime.__esmMin((() => {
|
|
45
|
+
require_lib_two_factor_errors.init_errors();
|
|
46
|
+
require_lib_two_factor_crypto.init_crypto();
|
|
47
|
+
require_lib_two_factor_totp.init_totp();
|
|
48
|
+
require_lib_two_factor_recovery_codes.init_recovery_codes();
|
|
49
|
+
TwoFactorService = class {
|
|
50
|
+
User;
|
|
51
|
+
config;
|
|
52
|
+
rateLimiter;
|
|
53
|
+
app;
|
|
54
|
+
Credential;
|
|
55
|
+
Challenge;
|
|
56
|
+
RecoveryCode;
|
|
57
|
+
dummyPasswordUser;
|
|
58
|
+
constructor(User, config, rateLimiter, app) {
|
|
59
|
+
this.User = User;
|
|
60
|
+
this.config = config;
|
|
61
|
+
this.rateLimiter = rateLimiter;
|
|
62
|
+
this.app = app;
|
|
63
|
+
this.Credential = app.models.TwoFactorCredential;
|
|
64
|
+
this.Challenge = app.models.TwoFactorChallenge;
|
|
65
|
+
this.RecoveryCode = app.models.TwoFactorRecoveryCode;
|
|
66
|
+
this.dummyPasswordUser = {
|
|
67
|
+
password: this.User._hashPasswordWithoutValidation(require_lib_two_factor_crypto.randomToken(24)),
|
|
68
|
+
hasPassword: this.User.prototype.hasPassword
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
login(credentials, include, options, callback) {
|
|
72
|
+
return optionalCallback(this.loginInternal(credentials || {}, include, options || {}), callback);
|
|
73
|
+
}
|
|
74
|
+
verifyTwoFactor(verification, options, callback) {
|
|
75
|
+
if (typeof options === "function") {
|
|
76
|
+
callback = options;
|
|
77
|
+
options = {};
|
|
78
|
+
}
|
|
79
|
+
return optionalCallback(this.verifyChallengeInternal(verification || {}, options || {}), callback);
|
|
80
|
+
}
|
|
81
|
+
beginEnrollment(userId, request, options, callback) {
|
|
82
|
+
return optionalCallback(this.beginEnrollmentInternal(userId, request || {}, options || {}), callback);
|
|
83
|
+
}
|
|
84
|
+
confirmEnrollment(userId, request, options, callback) {
|
|
85
|
+
return optionalCallback(this.confirmEnrollmentInternal(userId, request || {}, options || {}), callback);
|
|
86
|
+
}
|
|
87
|
+
regenerateRecoveryCodes(userId, request, options, callback) {
|
|
88
|
+
return optionalCallback(this.regenerateRecoveryCodesInternal(userId, request || {}, options || {}), callback);
|
|
89
|
+
}
|
|
90
|
+
disable(userId, request, options, callback) {
|
|
91
|
+
return optionalCallback(this.disableInternal(userId, request || {}, options || {}), callback);
|
|
92
|
+
}
|
|
93
|
+
deletePrincipals(principalIds, callback) {
|
|
94
|
+
return optionalCallback(this.deletePrincipalsInternal(principalIds.map(String)), callback);
|
|
95
|
+
}
|
|
96
|
+
rotateEncryption(options = {}, callback) {
|
|
97
|
+
return optionalCallback(this.rotateEncryptionInternal(options || {}), callback);
|
|
98
|
+
}
|
|
99
|
+
requireConfig(enrollment = false) {
|
|
100
|
+
if (!this.config) throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_CONFIGURED", 404);
|
|
101
|
+
if (enrollment && (this.config.mode === "disabled" || this.config.mode === "verify-only")) throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_DISABLED", 403);
|
|
102
|
+
return this.config;
|
|
103
|
+
}
|
|
104
|
+
async loginInternal(credentials, include, options) {
|
|
105
|
+
const normalizedInclude = Array.isArray(include) ? include.map((value) => String(value).toLowerCase()) : String(include || "").toLowerCase();
|
|
106
|
+
const realmRequired = !!(this.User.settings.realmRequired || this.User.settings.realmDelimiter);
|
|
107
|
+
const query = this.User.normalizeCredentials(credentials, realmRequired, this.User.settings.realmDelimiter);
|
|
108
|
+
this.validateLoginQuery(query, realmRequired);
|
|
109
|
+
const identifier = this.canonicalIdentifier(query);
|
|
110
|
+
const observed = [];
|
|
111
|
+
const source = await this.resolveSource(options);
|
|
112
|
+
if (this.rateLimiter) {
|
|
113
|
+
if (source && this.rateLimiter.sourceEnabled) await this.rateLimiter.consume(this.rateLimiter.sourceRule("login", source));
|
|
114
|
+
}
|
|
115
|
+
let user;
|
|
116
|
+
try {
|
|
117
|
+
user = await this.User.findOne({ where: query });
|
|
118
|
+
} catch {
|
|
119
|
+
throw loginError();
|
|
120
|
+
}
|
|
121
|
+
const resolvedPrincipalId = user ? String(user[this.User.definition.idName() || "id"]) : null;
|
|
122
|
+
if (this.rateLimiter) {
|
|
123
|
+
const rule = resolvedPrincipalId ? this.rateLimiter.rule("password", "principal", resolvedPrincipalId) : this.rateLimiter.rule("password", "identifier", identifier);
|
|
124
|
+
const result = await this.rateLimiter.inspect(rule);
|
|
125
|
+
observed.push({
|
|
126
|
+
rule,
|
|
127
|
+
revision: result.decision.revision
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (!await this.hasPassword(user || this.dummyPasswordUser, credentials.password)) {
|
|
131
|
+
if (this.rateLimiter) await this.consumePasswordFailures(identifier, user);
|
|
132
|
+
throw loginError();
|
|
133
|
+
}
|
|
134
|
+
if (this.User.settings.emailVerificationRequired && !user.emailVerified) {
|
|
135
|
+
const error = /* @__PURE__ */ new Error("login failed as the email has not been verified");
|
|
136
|
+
error.statusCode = 401;
|
|
137
|
+
error.code = "LOGIN_FAILED_EMAIL_NOT_VERIFIED";
|
|
138
|
+
error.details = { userId: user.id };
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const principalId = resolvedPrincipalId;
|
|
142
|
+
const credential = this.config && this.config.mode !== "disabled" ? await this.Credential.findOne({ where: {
|
|
143
|
+
principalType: this.User.modelName,
|
|
144
|
+
principalId,
|
|
145
|
+
provider: "totp",
|
|
146
|
+
status: "enabled"
|
|
147
|
+
} }) : null;
|
|
148
|
+
if (!credential) {
|
|
149
|
+
if (this.rateLimiter) await this.rateLimiter.reset(observed);
|
|
150
|
+
return this.issueAccessToken(user, credentials, normalizedInclude, false);
|
|
151
|
+
}
|
|
152
|
+
const secondFactorObserved = [];
|
|
153
|
+
if (this.rateLimiter) {
|
|
154
|
+
const rule = this.rateLimiter.rule("secondFactor", "principal", principalId);
|
|
155
|
+
const result = await this.rateLimiter.inspect(rule);
|
|
156
|
+
secondFactorObserved.push({
|
|
157
|
+
rule,
|
|
158
|
+
revision: result.decision.revision
|
|
159
|
+
});
|
|
160
|
+
if (source && this.rateLimiter.sourceEnabled && credentials.twoFactorCode) await this.rateLimiter.consume(this.rateLimiter.sourceRule("secondFactor", source));
|
|
161
|
+
}
|
|
162
|
+
if (credentials.twoFactorCode) {
|
|
163
|
+
const method = credentials.twoFactorMethod || "totp";
|
|
164
|
+
const binding = await this.principalBinding(user);
|
|
165
|
+
const inlineChallenge = method === "recovery" ? await this.createInlineRecoveryOperation(user, credential, binding) : null;
|
|
166
|
+
const factor = await this.verifyCredentialFactor(credential, method, credentials.twoFactorCode, inlineChallenge ? `challenge:${inlineChallenge.id}` : `inline:${require_lib_two_factor_crypto.randomToken(12)}`);
|
|
167
|
+
if (!factor.valid) {
|
|
168
|
+
if (inlineChallenge) await this.Challenge.updateAll({
|
|
169
|
+
id: inlineChallenge.id,
|
|
170
|
+
status: "verifying"
|
|
171
|
+
}, { status: "locked" });
|
|
172
|
+
if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("secondFactor", "principal", principalId));
|
|
173
|
+
throw require_lib_two_factor_errors.invalidFactor();
|
|
174
|
+
}
|
|
175
|
+
let token;
|
|
176
|
+
try {
|
|
177
|
+
if (inlineChallenge) {
|
|
178
|
+
if (count(await this.Challenge.updateAll({
|
|
179
|
+
id: inlineChallenge.id,
|
|
180
|
+
status: "verifying"
|
|
181
|
+
}, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.invalidChallenge();
|
|
182
|
+
}
|
|
183
|
+
const reloadedUser = await this.User.findById(principalId);
|
|
184
|
+
if (!reloadedUser) throw require_lib_two_factor_errors.invalidChallenge();
|
|
185
|
+
await this.assertPrincipalBinding(reloadedUser, binding, options);
|
|
186
|
+
if (this.rateLimiter) await this.rateLimiter.reset([...observed, ...secondFactorObserved]);
|
|
187
|
+
token = await this.issueAccessToken(reloadedUser, credentials, normalizedInclude, true, options);
|
|
188
|
+
if (inlineChallenge) {
|
|
189
|
+
if (count(await this.Challenge.updateAll({
|
|
190
|
+
id: inlineChallenge.id,
|
|
191
|
+
status: "consuming"
|
|
192
|
+
}, {
|
|
193
|
+
status: "consumed",
|
|
194
|
+
consumedAt: /* @__PURE__ */ new Date()
|
|
195
|
+
})) !== 1) throw new Error("Inline challenge completion conflict");
|
|
196
|
+
}
|
|
197
|
+
if (factor.reservation) await this.confirmRecoveryReservation(factor.reservation);
|
|
198
|
+
return token;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (inlineChallenge) await this.compensateChallengeFailure(inlineChallenge.id, token || error?.issuedToken, factor.reservation);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("challengeIssuance", "principal", principalId));
|
|
205
|
+
const protectedTokenOptions = await this.buildTwoFactorTokenOptions(credentials, options);
|
|
206
|
+
const challenge = await this.createChallenge(user, credential, {
|
|
207
|
+
tokenOptions: protectedTokenOptions,
|
|
208
|
+
rateLimitObserved: observed
|
|
209
|
+
}, includeUser(normalizedInclude), options);
|
|
210
|
+
throw require_lib_two_factor_errors.twoFactorRequired(challenge.raw, Math.ceil(this.config.challengeTtlMs / 1e3), credential.activeRecoveryCodeSetId ? ["totp", "recovery"] : ["totp"]);
|
|
211
|
+
}
|
|
212
|
+
validateLoginQuery(query, realmRequired) {
|
|
213
|
+
const invalid = (code, message) => {
|
|
214
|
+
const error = new Error(message);
|
|
215
|
+
error.statusCode = 400;
|
|
216
|
+
error.code = code;
|
|
217
|
+
throw error;
|
|
218
|
+
};
|
|
219
|
+
if (realmRequired && !query.realm) invalid("REALM_REQUIRED", "realm is required");
|
|
220
|
+
if (query.realm != null && typeof query.realm !== "string") invalid("INVALID_REALM", "Invalid realm");
|
|
221
|
+
if (!query.email && !query.username) invalid("USERNAME_EMAIL_REQUIRED", "username or email is required");
|
|
222
|
+
if (query.username != null && typeof query.username !== "string") invalid("INVALID_USERNAME", "Invalid username");
|
|
223
|
+
if (query.email != null && typeof query.email !== "string") invalid("INVALID_EMAIL", "Invalid email");
|
|
224
|
+
}
|
|
225
|
+
canonicalIdentifier(query) {
|
|
226
|
+
const kind = query.email ? "email" : "username";
|
|
227
|
+
let value = String(query[kind]).trim();
|
|
228
|
+
if (kind === "email" && !this.User.settings.caseSensitiveEmail) value = value.toLowerCase();
|
|
229
|
+
return JSON.stringify([
|
|
230
|
+
String(query.realm || ""),
|
|
231
|
+
kind,
|
|
232
|
+
value
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
async consumePasswordFailures(identifier, user) {
|
|
236
|
+
if (user) {
|
|
237
|
+
await this.rateLimiter.consume(this.rateLimiter.rule("password", "principal", String(user[this.User.definition.idName() || "id"])));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await this.rateLimiter.consume(this.rateLimiter.rule("password", "identifier", identifier));
|
|
241
|
+
}
|
|
242
|
+
async resolveSource(options) {
|
|
243
|
+
if (!this.rateLimiter) return null;
|
|
244
|
+
if (typeof this.User.resolveAuthenticationRateLimitSource === "function") {
|
|
245
|
+
const value = await invoke(this.User, "resolveAuthenticationRateLimitSource", [options]);
|
|
246
|
+
return value == null || value === "" ? null : String(value);
|
|
247
|
+
}
|
|
248
|
+
return options.authenticationSource ? String(options.authenticationSource) : null;
|
|
249
|
+
}
|
|
250
|
+
async hasPassword(user, password) {
|
|
251
|
+
try {
|
|
252
|
+
return await invoke(user, "hasPassword", [password]);
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async issueAccessToken(user, tokenInput, include, protectedPath, securityOptions = {}) {
|
|
258
|
+
let tokenOptions = tokenInput;
|
|
259
|
+
if (protectedPath) tokenOptions = await this.buildTwoFactorTokenOptions(tokenInput, securityOptions);
|
|
260
|
+
let token;
|
|
261
|
+
if (user.createAccessToken.length === 2) token = await invoke(user, "createAccessToken", [tokenOptions.ttl]);
|
|
262
|
+
else token = await invoke(user, "createAccessToken", [tokenOptions.ttl, tokenOptions]);
|
|
263
|
+
if (!token?.id) {
|
|
264
|
+
const error = require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
265
|
+
error.issuedToken = token;
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
if (includeUser(include)) token.__data.user = user;
|
|
269
|
+
return token;
|
|
270
|
+
}
|
|
271
|
+
async buildTwoFactorTokenOptions(credentials, options) {
|
|
272
|
+
return invoke(this.User, "buildTwoFactorLoginContext", [credentials, options]);
|
|
273
|
+
}
|
|
274
|
+
secretAad(credential) {
|
|
275
|
+
return [
|
|
276
|
+
"two-factor-secret:v1",
|
|
277
|
+
this.User.modelName,
|
|
278
|
+
String(credential.principalId),
|
|
279
|
+
String(credential.id),
|
|
280
|
+
String(credential.version)
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
challengeAad(challenge) {
|
|
284
|
+
return [
|
|
285
|
+
"two-factor-login-context:v1",
|
|
286
|
+
this.User.modelName,
|
|
287
|
+
String(challenge.principalId),
|
|
288
|
+
String(challenge.id),
|
|
289
|
+
String(challenge.credentialVersion)
|
|
290
|
+
];
|
|
291
|
+
}
|
|
292
|
+
async principalBinding(user, keyId) {
|
|
293
|
+
const config = this.requireConfig();
|
|
294
|
+
const material = await invoke(this.User, "getTwoFactorPrincipalBindingMaterial", [user, {}]);
|
|
295
|
+
if (typeof material !== "string" && !Buffer.isBuffer(material) || Buffer.byteLength(material) > config.loginContextMaxBytes) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
296
|
+
const principalId = String(user[this.User.definition.idName() || "id"]);
|
|
297
|
+
return require_lib_two_factor_crypto.keyedDigest("two-factor-principal-binding:v1", [
|
|
298
|
+
this.User.modelName,
|
|
299
|
+
principalId,
|
|
300
|
+
material
|
|
301
|
+
], config.principalBindingHmac, keyId);
|
|
302
|
+
}
|
|
303
|
+
async assertPrincipalBinding(user, expected, options) {
|
|
304
|
+
const current = await this.principalBinding(user, expected.keyId);
|
|
305
|
+
if (!require_lib_two_factor_crypto.timingSafeEqual(current.digest, expected.digest)) throw require_lib_two_factor_errors.invalidChallenge();
|
|
306
|
+
if (typeof this.User.validateTwoFactorLoginPrincipal === "function") {
|
|
307
|
+
if (await invoke(this.User, "validateTwoFactorLoginPrincipal", [user, options]) === false) throw require_lib_two_factor_errors.invalidChallenge();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async createChallenge(user, credential, tokenContext, shouldIncludeUser, options) {
|
|
311
|
+
const config = this.requireConfig();
|
|
312
|
+
const raw = require_lib_two_factor_crypto.randomToken(32);
|
|
313
|
+
const id = require_lib_two_factor_crypto.sha256(raw);
|
|
314
|
+
const principalId = String(user[this.User.definition.idName() || "id"]);
|
|
315
|
+
const binding = await this.principalBinding(user);
|
|
316
|
+
const base = {
|
|
317
|
+
id,
|
|
318
|
+
principalType: this.User.modelName,
|
|
319
|
+
principalId,
|
|
320
|
+
credentialId: String(credential.id),
|
|
321
|
+
credentialVersion: Number(credential.version),
|
|
322
|
+
flowType: "external",
|
|
323
|
+
principalBindingKeyId: binding.keyId,
|
|
324
|
+
principalBindingDigest: binding.digest,
|
|
325
|
+
status: "pending",
|
|
326
|
+
attempts: 0,
|
|
327
|
+
maxAttempts: config.maxAttempts,
|
|
328
|
+
includeUser: shouldIncludeUser,
|
|
329
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
330
|
+
expiresAt: new Date(Date.now() + config.challengeTtlMs)
|
|
331
|
+
};
|
|
332
|
+
const serialized = Buffer.from(JSON.stringify(tokenContext));
|
|
333
|
+
if (serialized.length > config.loginContextMaxBytes) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
334
|
+
const encrypted = require_lib_two_factor_crypto.encrypt(serialized, config.encryption, this.challengeAad(base));
|
|
335
|
+
await this.Challenge.create({
|
|
336
|
+
...base,
|
|
337
|
+
encryptedLoginContext: encrypted.envelope,
|
|
338
|
+
loginContextKeyId: encrypted.keyId
|
|
339
|
+
});
|
|
340
|
+
this.audit("twoFactorChallengeCreated", {
|
|
341
|
+
principalId,
|
|
342
|
+
credentialId: String(credential.id),
|
|
343
|
+
challengeDigestPrefix: id.slice(0, 12)
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
raw,
|
|
347
|
+
id
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async createInlineRecoveryOperation(user, credential, binding, flowType = "inline-recovery") {
|
|
351
|
+
const config = this.requireConfig();
|
|
352
|
+
const id = require_lib_two_factor_crypto.sha256(require_lib_two_factor_crypto.randomToken(32));
|
|
353
|
+
const principalId = String(user[this.User.definition.idName() || "id"]);
|
|
354
|
+
const base = {
|
|
355
|
+
id,
|
|
356
|
+
principalType: this.User.modelName,
|
|
357
|
+
principalId,
|
|
358
|
+
credentialId: String(credential.id),
|
|
359
|
+
credentialVersion: Number(credential.version),
|
|
360
|
+
flowType,
|
|
361
|
+
principalBindingKeyId: binding.keyId,
|
|
362
|
+
principalBindingDigest: binding.digest,
|
|
363
|
+
status: "verifying",
|
|
364
|
+
attempts: 1,
|
|
365
|
+
maxAttempts: 1,
|
|
366
|
+
includeUser: false,
|
|
367
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
368
|
+
expiresAt: new Date(Date.now() + config.challengeTtlMs),
|
|
369
|
+
claimedAt: /* @__PURE__ */ new Date()
|
|
370
|
+
};
|
|
371
|
+
const encrypted = require_lib_two_factor_crypto.encrypt("{}", config.encryption, this.challengeAad(base));
|
|
372
|
+
await this.Challenge.create({
|
|
373
|
+
...base,
|
|
374
|
+
encryptedLoginContext: encrypted.envelope,
|
|
375
|
+
loginContextKeyId: encrypted.keyId
|
|
376
|
+
});
|
|
377
|
+
return { id };
|
|
378
|
+
}
|
|
379
|
+
async verifyChallengeInternal(verification, options) {
|
|
380
|
+
const config = this.requireConfig();
|
|
381
|
+
if (config.mode === "disabled") throw require_lib_two_factor_errors.invalidChallenge();
|
|
382
|
+
if (typeof verification.challengeId !== "string") throw require_lib_two_factor_errors.invalidChallenge();
|
|
383
|
+
const source = await this.resolveSource(options);
|
|
384
|
+
if (this.rateLimiter?.sourceEnabled && source) await this.rateLimiter.consume(this.rateLimiter.sourceRule("secondFactor", source));
|
|
385
|
+
const id = require_lib_two_factor_crypto.sha256(verification.challengeId);
|
|
386
|
+
let challenge = await this.Challenge.findById(id);
|
|
387
|
+
if (challenge?.status === "verifying" && challenge.verificationLeaseExpiresAt && new Date(challenge.verificationLeaseExpiresAt).getTime() <= Date.now()) {
|
|
388
|
+
await this.Challenge.updateAll({
|
|
389
|
+
id,
|
|
390
|
+
status: "verifying",
|
|
391
|
+
verificationLeaseId: challenge.verificationLeaseId
|
|
392
|
+
}, {
|
|
393
|
+
status: "pending",
|
|
394
|
+
verificationLeaseId: null,
|
|
395
|
+
verificationLeaseExpiresAt: null
|
|
396
|
+
});
|
|
397
|
+
challenge = await this.Challenge.findById(id);
|
|
398
|
+
}
|
|
399
|
+
if (!challenge || challenge.principalType !== this.User.modelName || challenge.status !== "pending" || new Date(challenge.expiresAt).getTime() <= Date.now()) {
|
|
400
|
+
if (challenge?.status === "pending" && new Date(challenge.expiresAt).getTime() <= Date.now()) await this.Challenge.updateAll({
|
|
401
|
+
id,
|
|
402
|
+
status: "pending"
|
|
403
|
+
}, { status: "expired" });
|
|
404
|
+
throw require_lib_two_factor_errors.invalidChallenge();
|
|
405
|
+
}
|
|
406
|
+
const principalId = String(challenge.principalId);
|
|
407
|
+
const observed = [];
|
|
408
|
+
if (this.rateLimiter) {
|
|
409
|
+
const rule = this.rateLimiter.rule("secondFactor", "principal", principalId);
|
|
410
|
+
const result = await this.rateLimiter.inspect(rule);
|
|
411
|
+
observed.push({
|
|
412
|
+
rule,
|
|
413
|
+
revision: result.decision.revision
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const leaseId = require_lib_two_factor_crypto.randomToken(16);
|
|
417
|
+
const attempts = Number(challenge.attempts);
|
|
418
|
+
if (count(await this.Challenge.updateAll({
|
|
419
|
+
id,
|
|
420
|
+
status: "pending",
|
|
421
|
+
attempts
|
|
422
|
+
}, {
|
|
423
|
+
status: "verifying",
|
|
424
|
+
attempts: attempts + 1,
|
|
425
|
+
verificationLeaseId: leaseId,
|
|
426
|
+
verificationLeaseExpiresAt: new Date(Date.now() + config.verificationLeaseTtlMs),
|
|
427
|
+
claimedAt: /* @__PURE__ */ new Date()
|
|
428
|
+
})) !== 1) throw require_lib_two_factor_errors.invalidChallenge();
|
|
429
|
+
challenge = await this.Challenge.findById(id);
|
|
430
|
+
try {
|
|
431
|
+
const credential = await this.Credential.findOne({ where: {
|
|
432
|
+
id: challenge.credentialId,
|
|
433
|
+
principalType: this.User.modelName,
|
|
434
|
+
principalId,
|
|
435
|
+
status: "enabled",
|
|
436
|
+
version: challenge.credentialVersion
|
|
437
|
+
} });
|
|
438
|
+
const user = await this.User.findById(principalId);
|
|
439
|
+
if (!credential || !user) throw require_lib_two_factor_errors.invalidChallenge();
|
|
440
|
+
await this.assertPrincipalBinding(user, {
|
|
441
|
+
keyId: challenge.principalBindingKeyId,
|
|
442
|
+
digest: challenge.principalBindingDigest
|
|
443
|
+
}, options);
|
|
444
|
+
const factor = await this.verifyCredentialFactor(credential, verification.method, verification.code, `challenge:${id}`);
|
|
445
|
+
if (!factor.valid) {
|
|
446
|
+
const locked = attempts + 1 >= Number(challenge.maxAttempts);
|
|
447
|
+
await this.Challenge.updateAll({
|
|
448
|
+
id,
|
|
449
|
+
status: "verifying",
|
|
450
|
+
verificationLeaseId: leaseId
|
|
451
|
+
}, {
|
|
452
|
+
status: locked ? "locked" : "pending",
|
|
453
|
+
verificationLeaseId: null,
|
|
454
|
+
verificationLeaseExpiresAt: null
|
|
455
|
+
});
|
|
456
|
+
if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("secondFactor", "principal", principalId));
|
|
457
|
+
throw require_lib_two_factor_errors.invalidFactor();
|
|
458
|
+
}
|
|
459
|
+
let token;
|
|
460
|
+
try {
|
|
461
|
+
if (count(await this.Challenge.updateAll({
|
|
462
|
+
id,
|
|
463
|
+
status: "verifying",
|
|
464
|
+
verificationLeaseId: leaseId
|
|
465
|
+
}, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.invalidChallenge();
|
|
466
|
+
const tokenContext = JSON.parse(require_lib_two_factor_crypto.decrypt(challenge.encryptedLoginContext, config.encryption, this.challengeAad(challenge)).toString("utf8"));
|
|
467
|
+
if (this.rateLimiter) await this.rateLimiter.reset([...tokenContext.rateLimitObserved || [], ...observed]);
|
|
468
|
+
token = await this.issueAccessToken(user, tokenContext.tokenOptions || {}, challenge.includeUser ? "user" : "", false, options);
|
|
469
|
+
if (count(await this.Challenge.updateAll({
|
|
470
|
+
id,
|
|
471
|
+
status: "consuming"
|
|
472
|
+
}, {
|
|
473
|
+
status: "consumed",
|
|
474
|
+
consumedAt: /* @__PURE__ */ new Date()
|
|
475
|
+
})) !== 1) throw new Error("Challenge completion conflict");
|
|
476
|
+
if (factor.reservation) await this.confirmRecoveryReservation(factor.reservation);
|
|
477
|
+
return token;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
await this.compensateChallengeFailure(id, token || error?.issuedToken, factor.reservation);
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error instanceof require_lib_two_factor_errors.AuthenticationSecurityError) throw error;
|
|
484
|
+
throw require_lib_two_factor_errors.invalidChallenge();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async verifyCredentialFactor(credential, method, code, ownerId) {
|
|
488
|
+
const config = this.requireConfig();
|
|
489
|
+
if (method === "recovery") {
|
|
490
|
+
const reservation = await this.reserveRecoveryCode(credential, code, ownerId);
|
|
491
|
+
return {
|
|
492
|
+
valid: !!reservation,
|
|
493
|
+
reservation: reservation || void 0
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (method !== "totp") return { valid: false };
|
|
497
|
+
let secret;
|
|
498
|
+
try {
|
|
499
|
+
secret = require_lib_two_factor_crypto.decrypt(credential.encryptedSecret, config.encryption, this.secretAad(credential)).toString("utf8");
|
|
500
|
+
} catch {
|
|
501
|
+
throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
502
|
+
}
|
|
503
|
+
const verification = require_lib_two_factor_totp.verifyTotp(secret, code, {
|
|
504
|
+
algorithm: credential.totpAlgorithm,
|
|
505
|
+
digits: credential.totpDigits,
|
|
506
|
+
period: credential.totpPeriod
|
|
507
|
+
}, config.maxClockSkewSteps);
|
|
508
|
+
if (!verification.valid || verification.counter <= Number(credential.lastUsedCounter)) return { valid: false };
|
|
509
|
+
return { valid: count(await this.Credential.updateAll({
|
|
510
|
+
id: credential.id,
|
|
511
|
+
status: "enabled",
|
|
512
|
+
version: credential.version,
|
|
513
|
+
lastUsedCounter: credential.lastUsedCounter
|
|
514
|
+
}, {
|
|
515
|
+
lastUsedCounter: verification.counter,
|
|
516
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
517
|
+
})) === 1 };
|
|
518
|
+
}
|
|
519
|
+
async reserveRecoveryCode(credential, code, ownerId) {
|
|
520
|
+
const config = this.requireConfig();
|
|
521
|
+
const normalized = require_lib_two_factor_recovery_codes.normalizeRecoveryCode(code);
|
|
522
|
+
if (!normalized || !credential.activeRecoveryCodeSetId || credential.activeRecoveryCodeFormatVersion !== 1 || normalized.length !== Number(credential.activeRecoveryCodeLength)) return null;
|
|
523
|
+
for (const keyId of Object.keys(config.recoveryCodeHmac.keys)) {
|
|
524
|
+
const candidate = require_lib_two_factor_recovery_codes.recoveryCodeId(String(credential.id), credential.activeRecoveryCodeSetId, normalized, config.recoveryCodeHmac, keyId);
|
|
525
|
+
const reservationId = require_lib_two_factor_crypto.randomToken(16);
|
|
526
|
+
if (count(await this.RecoveryCode.updateAll({
|
|
527
|
+
id: candidate.digest,
|
|
528
|
+
credentialId: String(credential.id),
|
|
529
|
+
setId: credential.activeRecoveryCodeSetId,
|
|
530
|
+
keyId,
|
|
531
|
+
status: "unused"
|
|
532
|
+
}, {
|
|
533
|
+
status: "reserved",
|
|
534
|
+
reservationId,
|
|
535
|
+
reservationExpiresAt: new Date(Date.now() + config.challengeTtlMs),
|
|
536
|
+
reservationOwnerType: "two-factor-operation",
|
|
537
|
+
reservationOwnerId: ownerId
|
|
538
|
+
})) === 1) return {
|
|
539
|
+
id: candidate.digest,
|
|
540
|
+
reservationId,
|
|
541
|
+
ownerId
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
async confirmRecoveryReservation(reservation) {
|
|
547
|
+
if (count(await this.RecoveryCode.updateAll({
|
|
548
|
+
id: reservation.id,
|
|
549
|
+
status: "reserved",
|
|
550
|
+
reservationId: reservation.reservationId,
|
|
551
|
+
reservationOwnerId: reservation.ownerId
|
|
552
|
+
}, {
|
|
553
|
+
status: "used",
|
|
554
|
+
usedAt: /* @__PURE__ */ new Date(),
|
|
555
|
+
reservationId: null,
|
|
556
|
+
reservationExpiresAt: null,
|
|
557
|
+
reservationOwnerType: null,
|
|
558
|
+
reservationOwnerId: null
|
|
559
|
+
})) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
560
|
+
}
|
|
561
|
+
async releaseRecoveryReservation(reservation) {
|
|
562
|
+
if (count(await this.RecoveryCode.updateAll({
|
|
563
|
+
id: reservation.id,
|
|
564
|
+
status: "reserved",
|
|
565
|
+
reservationId: reservation.reservationId,
|
|
566
|
+
reservationOwnerId: reservation.ownerId
|
|
567
|
+
}, {
|
|
568
|
+
status: "unused",
|
|
569
|
+
reservationId: null,
|
|
570
|
+
reservationExpiresAt: null,
|
|
571
|
+
reservationOwnerType: null,
|
|
572
|
+
reservationOwnerId: null
|
|
573
|
+
})) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
574
|
+
}
|
|
575
|
+
async compensateChallengeFailure(challengeId, token, reservation) {
|
|
576
|
+
let revocationConfirmed = token == null;
|
|
577
|
+
let revocationError = token && !token.id ? /* @__PURE__ */ new Error("Issued access token has no durable identifier") : null;
|
|
578
|
+
if (token?.id) try {
|
|
579
|
+
await this.revokeToken(token);
|
|
580
|
+
revocationConfirmed = true;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
revocationError = error;
|
|
583
|
+
}
|
|
584
|
+
await this.Challenge.updateAll({
|
|
585
|
+
id: challengeId,
|
|
586
|
+
status: { inq: [
|
|
587
|
+
"pending",
|
|
588
|
+
"verifying",
|
|
589
|
+
"reconciling_no_token",
|
|
590
|
+
"consuming",
|
|
591
|
+
"consumed",
|
|
592
|
+
"token_revocation_pending"
|
|
593
|
+
] }
|
|
594
|
+
}, {
|
|
595
|
+
status: revocationConfirmed ? "token_failed" : "token_revocation_pending",
|
|
596
|
+
issuedTokenId: token?.id == null ? null : String(token.id),
|
|
597
|
+
verificationLeaseId: null,
|
|
598
|
+
verificationLeaseExpiresAt: null
|
|
599
|
+
}).catch(() => void 0);
|
|
600
|
+
if (reservation && revocationConfirmed) await this.releaseRecoveryReservation(reservation);
|
|
601
|
+
if (!revocationConfirmed) {
|
|
602
|
+
this.audit("twoFactorTokenRevocationPending", {
|
|
603
|
+
challengeId,
|
|
604
|
+
tokenId: String(token.id),
|
|
605
|
+
cause: revocationError?.message
|
|
606
|
+
});
|
|
607
|
+
throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Authentication security reconciliation required");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async beginEnrollmentInternal(userId, request, options) {
|
|
611
|
+
const config = this.requireConfig(true);
|
|
612
|
+
const user = await this.authenticatedUser(userId, options);
|
|
613
|
+
const managementObserved = await this.reauthenticateManagement(user, request.currentPassword);
|
|
614
|
+
const principalId = String(userId);
|
|
615
|
+
const existing = await this.Credential.findOne({ where: {
|
|
616
|
+
principalType: this.User.modelName,
|
|
617
|
+
principalId,
|
|
618
|
+
provider: "totp"
|
|
619
|
+
} });
|
|
620
|
+
if (existing?.status === "enabled") throw require_lib_two_factor_errors.authError("TWO_FACTOR_ALREADY_ENABLED", 409);
|
|
621
|
+
const id = existing ? String(existing.id) : require_lib_two_factor_crypto.randomToken(24);
|
|
622
|
+
const version = existing ? Number(existing.version) + 1 : 1;
|
|
623
|
+
const secret = require_lib_two_factor_totp.generateTotpSecret(config.totp.secretBytes);
|
|
624
|
+
const enrollmentId = require_lib_two_factor_crypto.randomToken(32);
|
|
625
|
+
const base = {
|
|
626
|
+
id,
|
|
627
|
+
principalType: this.User.modelName,
|
|
628
|
+
principalId,
|
|
629
|
+
provider: "totp",
|
|
630
|
+
totpAlgorithm: config.totp.algorithm,
|
|
631
|
+
totpDigits: config.totp.digits,
|
|
632
|
+
totpPeriod: config.totp.period,
|
|
633
|
+
status: "pending",
|
|
634
|
+
version,
|
|
635
|
+
enrollmentTokenDigest: require_lib_two_factor_crypto.sha256(enrollmentId),
|
|
636
|
+
activeRecoveryCodeSetId: null,
|
|
637
|
+
activeRecoveryCodeFormatVersion: null,
|
|
638
|
+
activeRecoveryCodeLength: null,
|
|
639
|
+
lastUsedCounter: -1,
|
|
640
|
+
pendingExpiresAt: new Date(Date.now() + config.enrollmentTtlMs),
|
|
641
|
+
confirmedAt: null,
|
|
642
|
+
disabledAt: null,
|
|
643
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
644
|
+
};
|
|
645
|
+
const encrypted = require_lib_two_factor_crypto.encrypt(secret, config.encryption, this.secretAad(base));
|
|
646
|
+
const data = {
|
|
647
|
+
...base,
|
|
648
|
+
encryptedSecret: encrypted.envelope,
|
|
649
|
+
secretKeyId: encrypted.keyId
|
|
650
|
+
};
|
|
651
|
+
if (existing) {
|
|
652
|
+
if (count(await this.Credential.updateAll({
|
|
653
|
+
id,
|
|
654
|
+
version: existing.version,
|
|
655
|
+
status: existing.status
|
|
656
|
+
}, data)) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
657
|
+
} else await this.Credential.create({
|
|
658
|
+
...data,
|
|
659
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
660
|
+
});
|
|
661
|
+
this.audit("twoFactorEnrollmentStarted", {
|
|
662
|
+
principalId,
|
|
663
|
+
credentialId: id
|
|
664
|
+
});
|
|
665
|
+
if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
|
|
666
|
+
return {
|
|
667
|
+
enrollmentId,
|
|
668
|
+
secret,
|
|
669
|
+
keyUri: require_lib_two_factor_totp.generateTotpUri(secret, config.issuer, String(user.email || user.username || principalId), config.totp),
|
|
670
|
+
expiresIn: Math.ceil(config.enrollmentTtlMs / 1e3)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
async confirmEnrollmentInternal(userId, request, options) {
|
|
674
|
+
const config = this.requireConfig(true);
|
|
675
|
+
await this.authenticatedUser(userId, options);
|
|
676
|
+
const principalId = String(userId);
|
|
677
|
+
let managementObserved = null;
|
|
678
|
+
if (this.rateLimiter) {
|
|
679
|
+
const rule = this.rateLimiter.rule("management", "principal", principalId);
|
|
680
|
+
managementObserved = {
|
|
681
|
+
rule,
|
|
682
|
+
revision: (await this.rateLimiter.inspect(rule)).decision.revision
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const credential = await this.Credential.findOne({ where: {
|
|
686
|
+
principalType: this.User.modelName,
|
|
687
|
+
principalId,
|
|
688
|
+
provider: "totp",
|
|
689
|
+
status: "pending"
|
|
690
|
+
} });
|
|
691
|
+
if (!credential) throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_EXPIRED", 410);
|
|
692
|
+
if (new Date(credential.pendingExpiresAt).getTime() <= Date.now()) throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_EXPIRED", 410);
|
|
693
|
+
if (typeof request.enrollmentId !== "string" || !require_lib_two_factor_crypto.timingSafeEqual(require_lib_two_factor_crypto.sha256(request.enrollmentId), credential.enrollmentTokenDigest)) {
|
|
694
|
+
await this.consumeManagementFailure(principalId);
|
|
695
|
+
throw require_lib_two_factor_errors.authError("TWO_FACTOR_ENROLLMENT_EXPIRED", 410);
|
|
696
|
+
}
|
|
697
|
+
const secret = require_lib_two_factor_crypto.decrypt(credential.encryptedSecret, config.encryption, this.secretAad(credential)).toString("utf8");
|
|
698
|
+
const verification = require_lib_two_factor_totp.verifyTotp(secret, request.code, {
|
|
699
|
+
algorithm: credential.totpAlgorithm,
|
|
700
|
+
digits: credential.totpDigits,
|
|
701
|
+
period: credential.totpPeriod
|
|
702
|
+
}, config.maxClockSkewSteps);
|
|
703
|
+
if (!verification.valid) {
|
|
704
|
+
await this.consumeManagementFailure(principalId);
|
|
705
|
+
throw require_lib_two_factor_errors.invalidFactor();
|
|
706
|
+
}
|
|
707
|
+
const generated = require_lib_two_factor_recovery_codes.generateRecoveryCodes(config.recoveryCodeCount, config.recoveryCodeBytes);
|
|
708
|
+
await this.persistRecoverySet(credential, generated);
|
|
709
|
+
try {
|
|
710
|
+
await this.revokeSessions(principalId, options, config.sessions.onEnable);
|
|
711
|
+
if (count(await this.Credential.updateAll({
|
|
712
|
+
id: credential.id,
|
|
713
|
+
status: "pending",
|
|
714
|
+
version: credential.version
|
|
715
|
+
}, {
|
|
716
|
+
status: "enabled",
|
|
717
|
+
enrollmentTokenDigest: null,
|
|
718
|
+
pendingExpiresAt: null,
|
|
719
|
+
lastUsedCounter: verification.counter,
|
|
720
|
+
activeRecoveryCodeSetId: generated.setId,
|
|
721
|
+
activeRecoveryCodeFormatVersion: 1,
|
|
722
|
+
activeRecoveryCodeLength: generated.normalized[0].length,
|
|
723
|
+
confirmedAt: /* @__PURE__ */ new Date(),
|
|
724
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
725
|
+
})) !== 1) throw new Error("Credential activation conflict");
|
|
726
|
+
} catch (error) {
|
|
727
|
+
await this.RecoveryCode.destroyAll({
|
|
728
|
+
credentialId: String(credential.id),
|
|
729
|
+
setId: generated.setId
|
|
730
|
+
}).catch(() => void 0);
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
this.audit("twoFactorEnabled", {
|
|
734
|
+
principalId,
|
|
735
|
+
credentialId: String(credential.id)
|
|
736
|
+
});
|
|
737
|
+
if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
|
|
738
|
+
return {
|
|
739
|
+
enabled: true,
|
|
740
|
+
recoveryCodes: generated.codes
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async regenerateRecoveryCodesInternal(userId, request, options) {
|
|
744
|
+
const config = this.requireConfig();
|
|
745
|
+
if (config.mode === "disabled") throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_ENABLED", 409);
|
|
746
|
+
const user = await this.authenticatedUser(userId, options);
|
|
747
|
+
const managementObserved = await this.reauthenticateManagement(user, request.currentPassword);
|
|
748
|
+
const credential = await this.enabledCredential(String(userId));
|
|
749
|
+
const recoveryOperation = request.method === "recovery" ? await this.createInlineRecoveryOperation(user, credential, await this.principalBinding(user), "management-recovery-regeneration") : null;
|
|
750
|
+
const factor = await this.verifyCredentialFactor(credential, request.method, request.code, recoveryOperation ? `challenge:${recoveryOperation.id}` : `management:${require_lib_two_factor_crypto.randomToken(16)}`);
|
|
751
|
+
if (!factor.valid) {
|
|
752
|
+
if (recoveryOperation) await this.Challenge.updateAll({
|
|
753
|
+
id: recoveryOperation.id,
|
|
754
|
+
status: "verifying"
|
|
755
|
+
}, { status: "locked" });
|
|
756
|
+
await this.consumeManagementFailure(String(userId));
|
|
757
|
+
throw require_lib_two_factor_errors.invalidFactor();
|
|
758
|
+
}
|
|
759
|
+
let generated = null;
|
|
760
|
+
let reservationConfirmed = false;
|
|
761
|
+
let switched = false;
|
|
762
|
+
const previousSetId = credential.activeRecoveryCodeSetId;
|
|
763
|
+
try {
|
|
764
|
+
if (recoveryOperation) {
|
|
765
|
+
if (count(await this.Challenge.updateAll({
|
|
766
|
+
id: recoveryOperation.id,
|
|
767
|
+
status: "verifying"
|
|
768
|
+
}, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
769
|
+
}
|
|
770
|
+
if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
|
|
771
|
+
generated = require_lib_two_factor_recovery_codes.generateRecoveryCodes(config.recoveryCodeCount, config.recoveryCodeBytes);
|
|
772
|
+
await this.persistRecoverySet(credential, generated);
|
|
773
|
+
await this.revokeSessions(String(userId), options, config.sessions.onRecoveryCodesRegenerated);
|
|
774
|
+
if (factor.reservation) {
|
|
775
|
+
await this.confirmRecoveryReservation(factor.reservation);
|
|
776
|
+
reservationConfirmed = true;
|
|
777
|
+
}
|
|
778
|
+
if (count(await this.Credential.updateAll({
|
|
779
|
+
id: credential.id,
|
|
780
|
+
status: "enabled",
|
|
781
|
+
activeRecoveryCodeSetId: previousSetId
|
|
782
|
+
}, {
|
|
783
|
+
activeRecoveryCodeSetId: generated.setId,
|
|
784
|
+
activeRecoveryCodeFormatVersion: 1,
|
|
785
|
+
activeRecoveryCodeLength: generated.normalized[0].length,
|
|
786
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
787
|
+
})) !== 1) throw new Error("Recovery set switch conflict");
|
|
788
|
+
switched = true;
|
|
789
|
+
if (recoveryOperation) await this.Challenge.updateAll({
|
|
790
|
+
id: recoveryOperation.id,
|
|
791
|
+
status: "consuming"
|
|
792
|
+
}, {
|
|
793
|
+
status: "consumed",
|
|
794
|
+
consumedAt: /* @__PURE__ */ new Date()
|
|
795
|
+
}).catch((error) => this.reportCleanupError(error));
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (generated && !switched) await this.RecoveryCode.destroyAll({
|
|
798
|
+
credentialId: String(credential.id),
|
|
799
|
+
setId: generated.setId
|
|
800
|
+
}).catch((cleanupError) => this.reportCleanupError(cleanupError));
|
|
801
|
+
if (factor.reservation && !reservationConfirmed) await this.releaseRecoveryReservation(factor.reservation);
|
|
802
|
+
if (recoveryOperation) await this.Challenge.updateAll({
|
|
803
|
+
id: recoveryOperation.id,
|
|
804
|
+
status: { inq: ["verifying", "consuming"] }
|
|
805
|
+
}, { status: "token_failed" }).catch((cleanupError) => this.reportCleanupError(cleanupError));
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
if (previousSetId) await this.RecoveryCode.destroyAll({
|
|
809
|
+
credentialId: String(credential.id),
|
|
810
|
+
setId: previousSetId
|
|
811
|
+
}).catch((error) => this.reportCleanupError(error));
|
|
812
|
+
this.audit("twoFactorRecoveryCodesRegenerated", {
|
|
813
|
+
principalId: String(userId),
|
|
814
|
+
credentialId: String(credential.id)
|
|
815
|
+
});
|
|
816
|
+
return { recoveryCodes: generated.codes };
|
|
817
|
+
}
|
|
818
|
+
async disableInternal(userId, request, options) {
|
|
819
|
+
const config = this.requireConfig();
|
|
820
|
+
if (config.mode === "disabled") throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_ENABLED", 409);
|
|
821
|
+
const user = await this.authenticatedUser(userId, options);
|
|
822
|
+
const managementObserved = await this.reauthenticateManagement(user, request.currentPassword);
|
|
823
|
+
const credential = await this.enabledCredential(String(userId));
|
|
824
|
+
const recoveryOperation = request.method === "recovery" ? await this.createInlineRecoveryOperation(user, credential, await this.principalBinding(user), "management-recovery-disable") : null;
|
|
825
|
+
const factor = await this.verifyCredentialFactor(credential, request.method, request.code, recoveryOperation ? `challenge:${recoveryOperation.id}` : `management:${require_lib_two_factor_crypto.randomToken(16)}`);
|
|
826
|
+
if (!factor.valid) {
|
|
827
|
+
if (recoveryOperation) await this.Challenge.updateAll({
|
|
828
|
+
id: recoveryOperation.id,
|
|
829
|
+
status: "verifying"
|
|
830
|
+
}, { status: "locked" });
|
|
831
|
+
await this.consumeManagementFailure(String(userId));
|
|
832
|
+
throw require_lib_two_factor_errors.invalidFactor();
|
|
833
|
+
}
|
|
834
|
+
let reservationConfirmed = false;
|
|
835
|
+
try {
|
|
836
|
+
if (recoveryOperation) {
|
|
837
|
+
if (count(await this.Challenge.updateAll({
|
|
838
|
+
id: recoveryOperation.id,
|
|
839
|
+
status: "verifying"
|
|
840
|
+
}, { status: "consuming" })) !== 1) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
841
|
+
}
|
|
842
|
+
if (this.rateLimiter && managementObserved) await this.rateLimiter.reset([managementObserved]);
|
|
843
|
+
await this.revokeSessions(String(userId), options, config.sessions.onDisable);
|
|
844
|
+
if (factor.reservation) {
|
|
845
|
+
await this.confirmRecoveryReservation(factor.reservation);
|
|
846
|
+
reservationConfirmed = true;
|
|
847
|
+
}
|
|
848
|
+
if (count(await this.Credential.updateAll({
|
|
849
|
+
id: credential.id,
|
|
850
|
+
status: "enabled",
|
|
851
|
+
version: credential.version
|
|
852
|
+
}, {
|
|
853
|
+
status: "disabled",
|
|
854
|
+
version: Number(credential.version) + 1,
|
|
855
|
+
encryptedSecret: null,
|
|
856
|
+
secretKeyId: null,
|
|
857
|
+
activeRecoveryCodeSetId: null,
|
|
858
|
+
activeRecoveryCodeFormatVersion: null,
|
|
859
|
+
activeRecoveryCodeLength: null,
|
|
860
|
+
disabledAt: /* @__PURE__ */ new Date(),
|
|
861
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
862
|
+
})) !== 1) throw new Error("Credential disable conflict");
|
|
863
|
+
if (recoveryOperation) await this.Challenge.updateAll({
|
|
864
|
+
id: recoveryOperation.id,
|
|
865
|
+
status: "consuming"
|
|
866
|
+
}, {
|
|
867
|
+
status: "consumed",
|
|
868
|
+
consumedAt: /* @__PURE__ */ new Date()
|
|
869
|
+
}).catch((error) => this.reportCleanupError(error));
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (factor.reservation && !reservationConfirmed) await this.releaseRecoveryReservation(factor.reservation);
|
|
872
|
+
if (recoveryOperation) await this.Challenge.updateAll({
|
|
873
|
+
id: recoveryOperation.id,
|
|
874
|
+
status: { inq: ["verifying", "consuming"] }
|
|
875
|
+
}, { status: "token_failed" }).catch((cleanupError) => this.reportCleanupError(cleanupError));
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
await Promise.all([this.RecoveryCode.destroyAll({ credentialId: String(credential.id) }), this.Challenge.updateAll({
|
|
879
|
+
credentialId: String(credential.id),
|
|
880
|
+
status: { inq: ["pending", "verifying"] }
|
|
881
|
+
}, { status: "expired" })]).catch((error) => this.reportCleanupError(error));
|
|
882
|
+
this.audit("twoFactorDisabled", {
|
|
883
|
+
principalId: String(userId),
|
|
884
|
+
credentialId: String(credential.id)
|
|
885
|
+
});
|
|
886
|
+
return { enabled: false };
|
|
887
|
+
}
|
|
888
|
+
async authenticatedUser(userId, options) {
|
|
889
|
+
const token = options?.accessToken;
|
|
890
|
+
if (!token || String(token.userId) !== String(userId) || "principalType" in token && token.principalType !== this.User.modelName) throw require_lib_two_factor_errors.authError("AUTHORIZATION_REQUIRED", 401, "Authorization required");
|
|
891
|
+
const user = await this.User.findById(userId);
|
|
892
|
+
if (!user) throw require_lib_two_factor_errors.authError("AUTHORIZATION_REQUIRED", 401, "Authorization required");
|
|
893
|
+
return user;
|
|
894
|
+
}
|
|
895
|
+
async reauthenticateManagement(user, currentPassword) {
|
|
896
|
+
const principalId = String(user[this.User.definition.idName() || "id"]);
|
|
897
|
+
let observed = null;
|
|
898
|
+
if (this.rateLimiter) {
|
|
899
|
+
const rule = this.rateLimiter.rule("management", "principal", principalId);
|
|
900
|
+
observed = {
|
|
901
|
+
rule,
|
|
902
|
+
revision: (await this.rateLimiter.inspect(rule)).decision.revision
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (!await this.hasPassword(user, currentPassword)) {
|
|
906
|
+
await this.consumeManagementFailure(principalId);
|
|
907
|
+
throw loginError();
|
|
908
|
+
}
|
|
909
|
+
return observed;
|
|
910
|
+
}
|
|
911
|
+
async consumeManagementFailure(principalId) {
|
|
912
|
+
if (this.rateLimiter) await this.rateLimiter.consume(this.rateLimiter.rule("management", "principal", principalId));
|
|
913
|
+
}
|
|
914
|
+
async enabledCredential(principalId) {
|
|
915
|
+
const credential = await this.Credential.findOne({ where: {
|
|
916
|
+
principalType: this.User.modelName,
|
|
917
|
+
principalId,
|
|
918
|
+
provider: "totp",
|
|
919
|
+
status: "enabled"
|
|
920
|
+
} });
|
|
921
|
+
if (!credential) throw require_lib_two_factor_errors.authError("TWO_FACTOR_NOT_ENABLED", 409);
|
|
922
|
+
return credential;
|
|
923
|
+
}
|
|
924
|
+
async persistRecoverySet(credential, generated) {
|
|
925
|
+
const config = this.requireConfig();
|
|
926
|
+
const rows = generated.normalized.map((code) => {
|
|
927
|
+
const digest = require_lib_two_factor_recovery_codes.recoveryCodeId(String(credential.id), generated.setId, code, config.recoveryCodeHmac);
|
|
928
|
+
return {
|
|
929
|
+
id: digest.digest,
|
|
930
|
+
credentialId: String(credential.id),
|
|
931
|
+
setId: generated.setId,
|
|
932
|
+
keyId: digest.keyId,
|
|
933
|
+
status: "unused",
|
|
934
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
await this.RecoveryCode.create(rows);
|
|
938
|
+
if (await this.RecoveryCode.count({
|
|
939
|
+
credentialId: String(credential.id),
|
|
940
|
+
setId: generated.setId,
|
|
941
|
+
status: "unused"
|
|
942
|
+
}) !== rows.length) throw new Error("Incomplete recovery code set");
|
|
943
|
+
}
|
|
944
|
+
async deletePrincipalsInternal(principalIds) {
|
|
945
|
+
if (!principalIds.length) return;
|
|
946
|
+
const credentialIds = (await this.Credential.find({
|
|
947
|
+
where: {
|
|
948
|
+
principalType: this.User.modelName,
|
|
949
|
+
principalId: { inq: principalIds }
|
|
950
|
+
},
|
|
951
|
+
fields: { id: true }
|
|
952
|
+
})).map((credential) => String(credential.id));
|
|
953
|
+
await Promise.all([
|
|
954
|
+
credentialIds.length ? this.RecoveryCode.destroyAll({ credentialId: { inq: credentialIds } }) : Promise.resolve(),
|
|
955
|
+
this.Challenge.destroyAll({
|
|
956
|
+
principalType: this.User.modelName,
|
|
957
|
+
principalId: { inq: principalIds }
|
|
958
|
+
}),
|
|
959
|
+
this.Credential.destroyAll({
|
|
960
|
+
principalType: this.User.modelName,
|
|
961
|
+
principalId: { inq: principalIds }
|
|
962
|
+
}),
|
|
963
|
+
this.rateLimiter ? Promise.all(principalIds.map((principalId) => this.rateLimiter.deletePrincipal(principalId))) : Promise.resolve()
|
|
964
|
+
]);
|
|
965
|
+
}
|
|
966
|
+
async rotateEncryptionInternal(options) {
|
|
967
|
+
const config = this.requireConfig();
|
|
968
|
+
const batchSize = options.batchSize == null ? 100 : Number(options.batchSize);
|
|
969
|
+
const maxBatches = options.maxBatches == null ? 1e4 : Number(options.maxBatches);
|
|
970
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 1e3) throw new Error("batchSize must be an integer between 1 and 1000");
|
|
971
|
+
if (!Number.isSafeInteger(maxBatches) || maxBatches < 1 || maxBatches > 1e5) throw new Error("maxBatches must be an integer between 1 and 100000");
|
|
972
|
+
const currentKeyId = config.encryption.currentKeyId;
|
|
973
|
+
const result = {
|
|
974
|
+
credentialsReencrypted: 0,
|
|
975
|
+
challengesReencrypted: 0,
|
|
976
|
+
conflicts: 0,
|
|
977
|
+
remainingCredentials: 0,
|
|
978
|
+
remainingChallenges: 0
|
|
979
|
+
};
|
|
980
|
+
for (let batch = 0; batch < maxBatches; batch++) {
|
|
981
|
+
const credentials = await this.Credential.find({
|
|
982
|
+
where: {
|
|
983
|
+
principalType: this.User.modelName,
|
|
984
|
+
encryptedSecret: { neq: null },
|
|
985
|
+
secretKeyId: { neq: currentKeyId }
|
|
986
|
+
},
|
|
987
|
+
limit: batchSize
|
|
988
|
+
});
|
|
989
|
+
if (!credentials.length) break;
|
|
990
|
+
let progress = 0;
|
|
991
|
+
for (const credential of credentials) {
|
|
992
|
+
let plaintext;
|
|
993
|
+
try {
|
|
994
|
+
plaintext = require_lib_two_factor_crypto.decrypt(credential.encryptedSecret, config.encryption, this.secretAad(credential));
|
|
995
|
+
} catch {
|
|
996
|
+
throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Unable to decrypt a two-factor credential during key rotation");
|
|
997
|
+
}
|
|
998
|
+
const encrypted = require_lib_two_factor_crypto.encrypt(plaintext, config.encryption, this.secretAad(credential));
|
|
999
|
+
if (count(await this.Credential.updateAll({
|
|
1000
|
+
id: credential.id,
|
|
1001
|
+
principalType: this.User.modelName,
|
|
1002
|
+
version: credential.version,
|
|
1003
|
+
encryptedSecret: credential.encryptedSecret,
|
|
1004
|
+
secretKeyId: credential.secretKeyId
|
|
1005
|
+
}, {
|
|
1006
|
+
encryptedSecret: encrypted.envelope,
|
|
1007
|
+
secretKeyId: encrypted.keyId,
|
|
1008
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1009
|
+
})) === 1) {
|
|
1010
|
+
result.credentialsReencrypted++;
|
|
1011
|
+
progress++;
|
|
1012
|
+
} else result.conflicts++;
|
|
1013
|
+
}
|
|
1014
|
+
if (progress === 0) break;
|
|
1015
|
+
}
|
|
1016
|
+
const liveChallengeWhere = {
|
|
1017
|
+
principalType: this.User.modelName,
|
|
1018
|
+
loginContextKeyId: { neq: currentKeyId },
|
|
1019
|
+
expiresAt: { gt: /* @__PURE__ */ new Date() },
|
|
1020
|
+
status: { inq: [
|
|
1021
|
+
"pending",
|
|
1022
|
+
"verifying",
|
|
1023
|
+
"reconciling_no_token",
|
|
1024
|
+
"consuming",
|
|
1025
|
+
"token_revocation_pending"
|
|
1026
|
+
] }
|
|
1027
|
+
};
|
|
1028
|
+
for (let batch = 0; batch < maxBatches; batch++) {
|
|
1029
|
+
const challenges = await this.Challenge.find({
|
|
1030
|
+
where: liveChallengeWhere,
|
|
1031
|
+
limit: batchSize
|
|
1032
|
+
});
|
|
1033
|
+
if (!challenges.length) break;
|
|
1034
|
+
let progress = 0;
|
|
1035
|
+
for (const challenge of challenges) {
|
|
1036
|
+
let plaintext;
|
|
1037
|
+
try {
|
|
1038
|
+
plaintext = require_lib_two_factor_crypto.decrypt(challenge.encryptedLoginContext, config.encryption, this.challengeAad(challenge));
|
|
1039
|
+
} catch {
|
|
1040
|
+
throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503, "Unable to decrypt a two-factor challenge during key rotation");
|
|
1041
|
+
}
|
|
1042
|
+
const encrypted = require_lib_two_factor_crypto.encrypt(plaintext, config.encryption, this.challengeAad(challenge));
|
|
1043
|
+
if (count(await this.Challenge.updateAll({
|
|
1044
|
+
id: challenge.id,
|
|
1045
|
+
principalType: this.User.modelName,
|
|
1046
|
+
status: challenge.status,
|
|
1047
|
+
encryptedLoginContext: challenge.encryptedLoginContext,
|
|
1048
|
+
loginContextKeyId: challenge.loginContextKeyId
|
|
1049
|
+
}, {
|
|
1050
|
+
encryptedLoginContext: encrypted.envelope,
|
|
1051
|
+
loginContextKeyId: encrypted.keyId
|
|
1052
|
+
})) === 1) {
|
|
1053
|
+
result.challengesReencrypted++;
|
|
1054
|
+
progress++;
|
|
1055
|
+
} else result.conflicts++;
|
|
1056
|
+
}
|
|
1057
|
+
if (progress === 0) break;
|
|
1058
|
+
}
|
|
1059
|
+
result.remainingCredentials = await this.Credential.count({
|
|
1060
|
+
principalType: this.User.modelName,
|
|
1061
|
+
encryptedSecret: { neq: null },
|
|
1062
|
+
secretKeyId: { neq: currentKeyId }
|
|
1063
|
+
});
|
|
1064
|
+
result.remainingChallenges = await this.Challenge.count(liveChallengeWhere);
|
|
1065
|
+
this.audit("twoFactorEncryptionRotated", {
|
|
1066
|
+
keyId: currentKeyId,
|
|
1067
|
+
...result
|
|
1068
|
+
});
|
|
1069
|
+
return result;
|
|
1070
|
+
}
|
|
1071
|
+
async revokeSessions(principalId, options, policy) {
|
|
1072
|
+
const AccessToken = this.User.relations.accessTokens?.modelTo;
|
|
1073
|
+
if (!AccessToken) return;
|
|
1074
|
+
const where = { userId: principalId };
|
|
1075
|
+
if (policy === "revoke-others" && options?.accessToken?.id) where.id = { neq: options.accessToken.id };
|
|
1076
|
+
if (AccessToken.relations?.user?.polymorphic) where.principalType = this.User.modelName;
|
|
1077
|
+
await AccessToken.destroyAll(where, options || {});
|
|
1078
|
+
}
|
|
1079
|
+
async revokeToken(token) {
|
|
1080
|
+
const AccessToken = this.User.relations.accessTokens?.modelTo;
|
|
1081
|
+
if (!AccessToken) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
1082
|
+
await AccessToken.destroyById(token.id);
|
|
1083
|
+
if (await AccessToken.findById(token.id)) throw require_lib_two_factor_errors.authError("AUTHENTICATION_SECURITY_UNAVAILABLE", 503);
|
|
1084
|
+
}
|
|
1085
|
+
reportCleanupError(error) {
|
|
1086
|
+
try {
|
|
1087
|
+
this.app.emit("authenticationSecurityCleanupError", error);
|
|
1088
|
+
} catch {}
|
|
1089
|
+
}
|
|
1090
|
+
audit(event, values) {
|
|
1091
|
+
this.User.emit(event, {
|
|
1092
|
+
principalType: this.User.modelName,
|
|
1093
|
+
modelName: this.User.modelName,
|
|
1094
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1095
|
+
...values
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
}));
|
|
1100
|
+
//#endregion
|
|
1101
|
+
init_service();
|
|
1102
|
+
exports.TwoFactorService = TwoFactorService;
|
|
1103
|
+
Object.defineProperty(exports, "init_service", {
|
|
1104
|
+
enumerable: true,
|
|
1105
|
+
get: function() {
|
|
1106
|
+
return init_service;
|
|
1107
|
+
}
|
|
1108
|
+
});
|