@adminforth/two-factors-auth 3.3.9 → 4.0.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/build.log +2 -2
- package/custom/TwoFAModal.vue +4 -4
- package/custom/TwoFactorsConfirmation.vue +9 -10
- package/custom/TwoFactorsPasskeysSettings.vue +10 -10
- package/custom/utils.js +7 -8
- package/dist/custom/TwoFAModal.vue +4 -4
- package/dist/custom/TwoFactorsConfirmation.vue +9 -10
- package/dist/custom/TwoFactorsPasskeysSettings.vue +10 -10
- package/dist/custom/utils.js +7 -8
- package/dist/endpoints/registerPasskeyEndpoints.d.ts +2 -0
- package/dist/endpoints/registerPasskeyEndpoints.js +59 -0
- package/dist/endpoints/registerTwoFaEndpoints.d.ts +2 -0
- package/dist/endpoints/registerTwoFaEndpoints.js +46 -0
- package/dist/handlers/createPasskeyHandlers.d.ts +48 -0
- package/dist/handlers/createPasskeyHandlers.js +99 -0
- package/dist/handlers/createTwoFaHandlers.d.ts +56 -0
- package/dist/handlers/createTwoFaHandlers.js +167 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +51 -858
- package/dist/repositories/passkeyRepository.d.ts +18 -0
- package/dist/repositories/passkeyRepository.js +89 -0
- package/dist/repositories/userRepository.d.ts +14 -0
- package/dist/repositories/userRepository.js +45 -0
- package/dist/services/cookieService.d.ts +52 -0
- package/dist/services/cookieService.js +144 -0
- package/dist/services/passkeyService.d.ts +84 -0
- package/dist/services/passkeyService.js +369 -0
- package/dist/services/totpService.d.ts +21 -0
- package/dist/services/totpService.js +43 -0
- package/dist/services/verifyMfaConfirmation.d.ts +30 -0
- package/dist/services/verifyMfaConfirmation.js +67 -0
- package/dist/types.d.ts +116 -0
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +9 -0
- package/dist/utils/parsePeriod.d.ts +1 -0
- package/dist/utils/parsePeriod.js +17 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/types.js +1 -0
- package/endpoints/registerPasskeyEndpoints.ts +61 -0
- package/endpoints/registerTwoFaEndpoints.ts +45 -0
- package/handlers/createPasskeyHandlers.ts +103 -0
- package/handlers/createTwoFaHandlers.ts +172 -0
- package/index.ts +57 -886
- package/package.json +1 -1
- package/repositories/passkeyRepository.ts +78 -0
- package/repositories/userRepository.ts +42 -0
- package/services/cookieService.ts +165 -0
- package/services/passkeyService.ts +375 -0
- package/services/totpService.ts +37 -0
- package/services/verifyMfaConfirmation.ts +92 -0
- package/tsconfig.json +1 -1
- package/utils/errors.ts +13 -0
- package/utils/parsePeriod.ts +19 -0
- package/utils/types.ts +17 -0
package/dist/index.js
CHANGED
|
@@ -7,11 +7,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { AdminForthPlugin,
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
10
|
+
import { AdminForthPlugin, suggestIfTypo } from "adminforth";
|
|
11
|
+
import { PasskeyService } from "./services/passkeyService.js";
|
|
12
|
+
import { TotpService } from "./services/totpService.js";
|
|
13
|
+
import { verifyMfaConfirmation } from "./services/verifyMfaConfirmation.js";
|
|
14
|
+
import { CookieService } from "./services/cookieService.js";
|
|
15
|
+
import { UserRepository } from "./repositories/userRepository.js";
|
|
16
|
+
import { registerPasskeyEndpoints } from "./endpoints/registerPasskeyEndpoints.js";
|
|
17
|
+
import { registerTwoFaEndpoints } from "./endpoints/registerTwoFaEndpoints.js";
|
|
18
|
+
import { createPasskeyHandlers } from "./handlers/createPasskeyHandlers.js";
|
|
19
|
+
import { createTwoFaHandlers } from "./handlers/createTwoFaHandlers.js";
|
|
20
|
+
import { parsePeriod } from "./utils/parsePeriod.js";
|
|
15
21
|
import crypto from 'crypto';
|
|
16
22
|
export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
17
23
|
constructor(options) {
|
|
@@ -23,66 +29,22 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
23
29
|
instanceUniqueRepresentation(pluginOptions) {
|
|
24
30
|
return `single`;
|
|
25
31
|
}
|
|
26
|
-
parseIfNeeded(value) {
|
|
27
|
-
if (typeof value === 'string') {
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(value);
|
|
30
|
-
}
|
|
31
|
-
catch (e) {
|
|
32
|
-
throw new Error('Failed to parse JSON string: ' + e.message);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
useChellenge(challenge, expiresIn) {
|
|
38
|
-
const expiresInSeconds = expiresIn ? convertPeriodToSeconds(expiresIn) : undefined;
|
|
39
|
-
this.options.passkeys.keyValueAdapter.set(challenge, 'stub_value', expiresInSeconds);
|
|
40
|
-
}
|
|
41
|
-
checkIfChellengeNotUsed(challenge) {
|
|
42
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
-
const res = yield this.options.passkeys.keyValueAdapter.get(challenge);
|
|
44
|
-
if (!res) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
validateCookiesForPasskeyLogin(cookies) {
|
|
51
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
-
var _a;
|
|
53
|
-
const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyLoginTemporaryJWT` });
|
|
54
|
-
if (!passkeysCookies) {
|
|
55
|
-
return { ok: false, error: 'Passkey token is required' };
|
|
56
|
-
}
|
|
57
|
-
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
|
|
58
|
-
const isChallangeValid = yield this.checkIfChellengeNotUsed(decodedPasskeysCookies.challenge);
|
|
59
|
-
if (isChallangeValid) {
|
|
60
|
-
this.useChellenge(decodedPasskeysCookies.challenge, ((_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.challengeValidityPeriod) || '2m');
|
|
61
|
-
}
|
|
62
|
-
if (!decodedPasskeysCookies || !isChallangeValid) {
|
|
63
|
-
return { ok: false, error: 'Invalid passkey' };
|
|
64
|
-
}
|
|
65
|
-
return { ok: true, decodedPasskeysCookies };
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
32
|
checkIfSkipSetupAllowSkipVerify(adminUser) {
|
|
69
33
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
34
|
if (this.options.usersFilterToAllowSkipSetup) {
|
|
71
|
-
const res =
|
|
35
|
+
const res = this.options.usersFilterToAllowSkipSetup(adminUser); // receive result of usersFilterToAllowSkipSetup
|
|
72
36
|
if (res === false) { // if false, user is not allowed to skip anyway, so doesn't matter if they have 2FA set up or not
|
|
73
37
|
return { skipAllowed: false };
|
|
74
38
|
}
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const userPkFieldName = usersPrimaryKeyColumn.name;
|
|
79
|
-
const userRecord = yield this.adminforth.resource(this.adminforth.config.auth.usersResourceId).get([Filters.EQ(userPkFieldName, adminUser.pk)]);
|
|
39
|
+
//receive user's record
|
|
40
|
+
const userPkFieldName = this.userRepository.getUserPkField();
|
|
41
|
+
const userRecord = yield this.userRepository.getAuthUser(adminUser.pk);
|
|
80
42
|
//check if user has 2FA set up
|
|
81
|
-
const users2FASecret =
|
|
43
|
+
const users2FASecret = this.userRepository.getSecret(userRecord);
|
|
82
44
|
//check if user has any passkeys registered
|
|
83
|
-
const
|
|
45
|
+
const userHasPasskeys = yield this.passkeyService.hasPasskeysForUser(adminUser.dbUser[userPkFieldName]);
|
|
84
46
|
// If user has either 2FA secret or any passkeys, they cannot skip
|
|
85
|
-
if (users2FASecret ||
|
|
47
|
+
if (users2FASecret || userHasPasskeys) {
|
|
86
48
|
return { skipAllowed: false };
|
|
87
49
|
}
|
|
88
50
|
return { skipAllowed: res };
|
|
@@ -90,61 +52,6 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
90
52
|
return { skipAllowed: false };
|
|
91
53
|
});
|
|
92
54
|
}
|
|
93
|
-
generateHashForStepUpMfaGraceCookie(headers, cookies) {
|
|
94
|
-
const ip = this.adminforth.auth.getClientIp(headers);
|
|
95
|
-
const userAgent = headers['user-agent'] || '';
|
|
96
|
-
const acceptLanguage = headers['accept-language'] || '';
|
|
97
|
-
const session_cookie = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "jwt" });
|
|
98
|
-
if (!ip || !userAgent || !acceptLanguage || !session_cookie) {
|
|
99
|
-
console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: missing required request headers to identify client ❗️❗️❗️");
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
const hmac = crypto.createHmac('sha256', process.env.ADMINFORTH_SECRET)
|
|
104
|
-
.update(`${acceptLanguage}_${userAgent}_${ip}_${session_cookie}`)
|
|
105
|
-
.digest('hex');
|
|
106
|
-
return hmac;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
issueTempSkip2FAGraceJWT(opts, cookies, response) {
|
|
110
|
-
if (response) {
|
|
111
|
-
if (opts.extra.headers) {
|
|
112
|
-
const hash = this.generateHashForStepUpMfaGraceCookie(opts.extra.headers, cookies);
|
|
113
|
-
if (!hash) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const jwt = this.adminforth.auth.issueJWT({ hash: hash }, 'MfaGrace', `${this.options.stepUpMfaGracePeriodSeconds}s`);
|
|
117
|
-
//TODO: fix ts-ignore after releasing new version of adminforth with updated types
|
|
118
|
-
//@ts-ignore
|
|
119
|
-
this.adminforth.auth.setCustomCookie({ response: response, payload: { name: "TempSkip2FA_Modal_JWT", sessionBased: true, value: jwt, httpOnly: true } });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
console.error("❗️❗️❗️ Cannot set step-up MFA grace cookie: response object is missing. You probably called verify() method without response parameter ❗️❗️❗️");
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
isTempSkip2FAGraceValid(headers_1, cookies_1) {
|
|
127
|
-
return __awaiter(this, arguments, void 0, function* (headers, cookies, checkIfJWTAboutToExpire = false) {
|
|
128
|
-
const hash = this.generateHashForStepUpMfaGraceCookie(headers, cookies);
|
|
129
|
-
if (!hash) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
const jwt = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "TempSkip2FA_Modal_JWT" });
|
|
133
|
-
const jwtVerificationResult = yield this.adminforth.auth.verify(jwt, 'MfaGrace', false);
|
|
134
|
-
if (!jwtVerificationResult) {
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
const jwtHash = jwtVerificationResult['hash'];
|
|
138
|
-
if (checkIfJWTAboutToExpire && (jwtVerificationResult["exp"] - (Date.now() / 1000)) < 30) {
|
|
139
|
-
console.error("❗️❗️❗️ Cannot validate step-up MFA grace cookie: token is expired or about to expire ❗️❗️❗️");
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
if (hash === jwtHash) {
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
55
|
waitForResponse(id, timeoutMs) {
|
|
149
56
|
return new Promise((resolve) => {
|
|
150
57
|
let timeout;
|
|
@@ -181,167 +88,34 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
181
88
|
}
|
|
182
89
|
verify(confirmationResult, opts) {
|
|
183
90
|
return __awaiter(this, void 0, void 0, function* () {
|
|
184
|
-
|
|
185
|
-
if (!confirmationResult)
|
|
186
|
-
return { error: "Confirmation result is required" };
|
|
187
|
-
if (!opts.adminUser)
|
|
188
|
-
return { error: "Admin user is required" };
|
|
189
|
-
if (!opts.userPk)
|
|
190
|
-
return { error: "User PK is required" };
|
|
191
|
-
const cookies = ((_a = opts.extra) === null || _a === void 0 ? void 0 : _a.cookies) || opts.cookies;
|
|
192
|
-
const response = ((_b = opts.extra) === null || _b === void 0 ? void 0 : _b.response) || opts.response;
|
|
193
|
-
if (this.options.usersFilterToApply) {
|
|
194
|
-
const res = this.options.usersFilterToApply(opts.adminUser);
|
|
195
|
-
if (res === false) {
|
|
196
|
-
return { ok: true };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
if (this.options.usersFilterToAllowSkipSetup) {
|
|
200
|
-
const res = yield this.checkIfSkipSetupAllowSkipVerify(opts.adminUser);
|
|
201
|
-
if (res.skipAllowed === true) {
|
|
202
|
-
return { ok: true };
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (this.options.stepUpMfaGracePeriodSeconds && ((_c = opts.extra) === null || _c === void 0 ? void 0 : _c.headers) && !confirmationResult.mode) {
|
|
206
|
-
const verificationResult = yield this.isTempSkip2FAGraceValid(opts.extra.headers, cookies);
|
|
207
|
-
if (verificationResult === true) {
|
|
208
|
-
return { ok: true };
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (confirmationResult.mode === "totp") {
|
|
212
|
-
const code = confirmationResult.result;
|
|
213
|
-
const authRes = this.adminforth.config.resources
|
|
214
|
-
.find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
215
|
-
if (!authRes)
|
|
216
|
-
return { error: "Auth resource not found" };
|
|
217
|
-
const connector = this.adminforth.connectors[authRes.dataSource];
|
|
218
|
-
const pkName = (_d = authRes.columns.find(c => c.primaryKey)) === null || _d === void 0 ? void 0 : _d.name;
|
|
219
|
-
if (!pkName)
|
|
220
|
-
return { error: "Primary key not found on auth resource" };
|
|
221
|
-
const pk = (_e = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _e !== void 0 ? _e : (_g = (_f = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _f === void 0 ? void 0 : _f.dbUser) === null || _g === void 0 ? void 0 : _g[pkName];
|
|
222
|
-
if (!pk)
|
|
223
|
-
return { error: "User PK is required" };
|
|
224
|
-
const user = yield connector.getRecordByPrimaryKey(authRes, pk);
|
|
225
|
-
if (!user)
|
|
226
|
-
return { error: "User not found" };
|
|
227
|
-
const secret = user[this.options.twoFaSecretFieldName];
|
|
228
|
-
if (!secret)
|
|
229
|
-
return { error: "2FA is not set up for this user" };
|
|
230
|
-
const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
|
|
231
|
-
//* SET GRACE COOKIE *//
|
|
232
|
-
if (verified) {
|
|
233
|
-
if (this.options.stepUpMfaGracePeriodSeconds) {
|
|
234
|
-
this.issueTempSkip2FAGraceJWT(opts, cookies, response);
|
|
235
|
-
}
|
|
236
|
-
return { ok: true };
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
return { error: "Wrong or expired OTP code" };
|
|
240
|
-
}
|
|
241
|
-
;
|
|
242
|
-
}
|
|
243
|
-
else if (confirmationResult.mode === "passkey") {
|
|
244
|
-
const cookiesValidationResult = yield this.validateCookiesForPasskeyLogin(cookies);
|
|
245
|
-
if (!cookiesValidationResult.ok) {
|
|
246
|
-
return { error: cookiesValidationResult.error };
|
|
247
|
-
}
|
|
248
|
-
const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
249
|
-
if (verificationResult.ok && verificationResult.passkeyConfirmed) {
|
|
250
|
-
//* SET GRACE COOKIE *//
|
|
251
|
-
if (this.options.stepUpMfaGracePeriodSeconds) {
|
|
252
|
-
this.issueTempSkip2FAGraceJWT(opts, cookies, response);
|
|
253
|
-
}
|
|
254
|
-
return { ok: true };
|
|
255
|
-
}
|
|
256
|
-
return { error: "Invalid passkey" };
|
|
257
|
-
}
|
|
258
|
-
return { error: "Unsupported confirmation mode" };
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
parsePeriod(period) {
|
|
262
|
-
if (!period)
|
|
263
|
-
return 0;
|
|
264
|
-
const match = /^(\d+)([dhms])$/.exec(period.trim());
|
|
265
|
-
if (!match)
|
|
266
|
-
throw new Error(`Invalid suggestionPeriod format: ${period}`);
|
|
267
|
-
const value = parseInt(match[1], 10);
|
|
268
|
-
const unit = match[2];
|
|
269
|
-
switch (unit) {
|
|
270
|
-
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
271
|
-
case 'h': return value * 60 * 60 * 1000;
|
|
272
|
-
case 'm': return value * 60 * 1000;
|
|
273
|
-
case 's': return value * 1000;
|
|
274
|
-
default: return value;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
verifyPasskeyResponse(body, user_id, cookies) {
|
|
278
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
279
|
-
var _a, _b, _c, _d, _e;
|
|
280
|
-
const settingsOrigin = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.expectedOrigin;
|
|
281
|
-
const expectedOrigin = body.origin;
|
|
282
|
-
const expectedChallenge = cookies.challenge;
|
|
283
|
-
const expectedRPID = ((_d = (_c = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings) === null || _c === void 0 ? void 0 : _c.rp) === null || _d === void 0 ? void 0 : _d.id) || (new URL(settingsOrigin)).hostname;
|
|
284
|
-
const response = JSON.parse(body.response);
|
|
285
|
-
try {
|
|
286
|
-
if (settingsOrigin !== expectedOrigin) {
|
|
287
|
-
throw new Error(`Origin mismatch. Allowed in settings: ${settingsOrigin}, received from client: ${expectedOrigin}`);
|
|
288
|
-
}
|
|
289
|
-
const cred = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, response.id)]);
|
|
290
|
-
if (!cred) {
|
|
291
|
-
throw new Error('Credential not found.');
|
|
292
|
-
}
|
|
293
|
-
const credMeta = this.parseIfNeeded(cred[this.options.passkeys.credentialMetaFieldName]);
|
|
294
|
-
if (!credMeta || !credMeta.public_key) {
|
|
295
|
-
throw new Error('Credential public key not found.');
|
|
296
|
-
}
|
|
297
|
-
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
298
|
-
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
299
|
-
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
300
|
-
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
301
|
-
const user = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, cred[this.options.passkeys.credentialUserIdFieldName])]);
|
|
302
|
-
if (!user || !user_id || user[usersPrimaryKeyFieldName] !== user_id) {
|
|
303
|
-
throw new Error('User not found.');
|
|
304
|
-
}
|
|
305
|
-
const { verified, authenticationInfo } = yield verifyAuthenticationResponse({
|
|
306
|
-
response,
|
|
307
|
-
expectedChallenge,
|
|
308
|
-
expectedOrigin: settingsOrigin,
|
|
309
|
-
expectedRPID,
|
|
310
|
-
credential: {
|
|
311
|
-
id: cred[this.options.passkeys.credentialIdFieldName],
|
|
312
|
-
publicKey: isoBase64URL.toBuffer(credMeta.public_key),
|
|
313
|
-
counter: credMeta.counter,
|
|
314
|
-
transports: credMeta.transports,
|
|
315
|
-
},
|
|
316
|
-
requireUserVerification: ((_e = this.options.passkeys) === null || _e === void 0 ? void 0 : _e.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
|
|
317
|
-
});
|
|
318
|
-
if (!verified) {
|
|
319
|
-
return { ok: false, error: 'User verification failed.' };
|
|
320
|
-
}
|
|
321
|
-
credMeta.counter = authenticationInfo.newCounter;
|
|
322
|
-
credMeta.last_used_at = new Date().toISOString();
|
|
323
|
-
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
324
|
-
const credResourcePKColumn = credResource === null || credResource === void 0 ? void 0 : credResource.columns.find(c => c.primaryKey);
|
|
325
|
-
if (!credResource || !credResourcePKColumn) {
|
|
326
|
-
throw new Error('Credential resource or its primary key is not configured correctly');
|
|
327
|
-
}
|
|
328
|
-
const credResourcePKName = credResourcePKColumn.name;
|
|
329
|
-
yield this.adminforth
|
|
330
|
-
.resource(this.options.passkeys.credentialResourceID)
|
|
331
|
-
.update(cred[credResourcePKName], { [this.options.passkeys.credentialMetaFieldName]: JSON.stringify(credMeta) });
|
|
332
|
-
return { ok: true, passkeyConfirmed: true };
|
|
333
|
-
}
|
|
334
|
-
catch (e) {
|
|
335
|
-
return { ok: false, error: 'Error authenticating passkey: ' + e };
|
|
336
|
-
}
|
|
91
|
+
return verifyMfaConfirmation(this.ctx, confirmationResult, opts);
|
|
337
92
|
});
|
|
338
93
|
}
|
|
339
94
|
modifyResourceConfig(adminforth, resourceConfig) {
|
|
340
|
-
var _a;
|
|
95
|
+
var _a, _b;
|
|
341
96
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
342
97
|
this.adminforth = adminforth;
|
|
343
98
|
this.adminForthAuth = adminforth.auth;
|
|
344
|
-
|
|
99
|
+
this.userRepository = new UserRepository(this.adminforth, this.options);
|
|
100
|
+
this.cookieService = new CookieService(this.adminforth, this.options);
|
|
101
|
+
this.totpService = new TotpService(this.options, this.userRepository);
|
|
102
|
+
this.passkeyService = new PasskeyService(this.options, this.adminforth, this.userRepository, undefined, this.cookieService);
|
|
103
|
+
this.ctx = {
|
|
104
|
+
adminforth: this.adminforth,
|
|
105
|
+
options: this.options,
|
|
106
|
+
authResource: resourceConfig,
|
|
107
|
+
userRepository: this.userRepository,
|
|
108
|
+
totpService: this.totpService,
|
|
109
|
+
passkeyService: this.passkeyService,
|
|
110
|
+
cookieService: this.cookieService,
|
|
111
|
+
verifyMfaConfirmation: (confirmationResult, opts) => verifyMfaConfirmation(this.ctx, confirmationResult, opts),
|
|
112
|
+
checkIfSkipSetupAllowSkipVerify: this.checkIfSkipSetupAllowSkipVerify.bind(this),
|
|
113
|
+
autoVerify: {
|
|
114
|
+
verifyAuto: this.verifyAuto.bind(this),
|
|
115
|
+
resolveResponse: this.resolveResponse.bind(this),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const suggestionPeriod = parsePeriod(((_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.suggestionPeriod) || "5d");
|
|
345
119
|
const isPasskeysEnabled = this.options.passkeys ? true : false;
|
|
346
120
|
const customPages = this.adminforth.config.customization.customPages;
|
|
347
121
|
customPages.push({
|
|
@@ -365,15 +139,15 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
365
139
|
slug: 'passkeys',
|
|
366
140
|
component: this.componentPath('TwoFactorsPasskeysSettings.vue'),
|
|
367
141
|
isVisible: (adminUser) => {
|
|
368
|
-
const secret = adminUser.dbUser
|
|
142
|
+
const secret = this.userRepository.getSecret(adminUser.dbUser);
|
|
369
143
|
if (!secret || secret.length === 0) {
|
|
370
144
|
return false;
|
|
371
145
|
}
|
|
372
146
|
return true;
|
|
373
147
|
}
|
|
374
148
|
});
|
|
375
|
-
|
|
376
|
-
|
|
149
|
+
const allowLoginWithPasskeys = (_b = this.options.passkeys.allowLoginWithPasskeys) !== null && _b !== void 0 ? _b : true;
|
|
150
|
+
if (allowLoginWithPasskeys) {
|
|
377
151
|
if (!this.adminforth.config.customization.loginPageInjections) {
|
|
378
152
|
this.adminforth.config.customization.loginPageInjections = { underLoginButton: [], panelHeader: [], underInputs: [] };
|
|
379
153
|
}
|
|
@@ -467,20 +241,18 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
467
241
|
const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation;
|
|
468
242
|
const beforeLoginConfirmationArray = Array.isArray(beforeLoginConfirmation) ? beforeLoginConfirmation : [beforeLoginConfirmation];
|
|
469
243
|
beforeLoginConfirmationArray.push((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, response, adminforth, extra, sessionDuration }) {
|
|
470
|
-
var _b
|
|
244
|
+
var _b;
|
|
471
245
|
if (((_b = extra === null || extra === void 0 ? void 0 : extra.meta) === null || _b === void 0 ? void 0 : _b.loginAllowedByPasskeyDirectSignIn) === true) {
|
|
472
246
|
return { body: { loginAllowed: true }, ok: true };
|
|
473
247
|
}
|
|
474
|
-
const secret = adminUser.dbUser
|
|
248
|
+
const secret = this.userRepository.getSecret(adminUser.dbUser);
|
|
475
249
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
|
|
476
250
|
const brandName = adminforth.config.customization.brandName;
|
|
477
251
|
const brandNameSlug = adminforth.config.customization.brandNameSlug;
|
|
478
252
|
const issuerName = (this.options.customBrandPrefix && this.options.customBrandPrefix.trim())
|
|
479
253
|
? this.options.customBrandPrefix.trim()
|
|
480
254
|
: brandName;
|
|
481
|
-
const
|
|
482
|
-
const authPk = authResource.columns.find((col) => col.primaryKey).name;
|
|
483
|
-
const userPk = adminUser.dbUser[authPk];
|
|
255
|
+
const userPk = this.userRepository.getUserPk(adminUser);
|
|
484
256
|
let newSecret = null;
|
|
485
257
|
const userNeeds2FA = this.options.usersFilterToApply ? this.options.usersFilterToApply(adminUser) : true;
|
|
486
258
|
if (!userNeeds2FA) {
|
|
@@ -488,10 +260,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
488
260
|
}
|
|
489
261
|
const userCanSkipSetup = this.options.usersFilterToAllowSkipSetup ? this.options.usersFilterToAllowSkipSetup(adminUser) : false;
|
|
490
262
|
if (!secret) {
|
|
491
|
-
|
|
492
|
-
newSecret
|
|
493
|
-
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: issuerName, pk: userPk, userCanSkipSetup, sessionDuration }, 'temp2FA', ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.challengeValidityPeriod) || '1m');
|
|
494
|
-
this.adminforth.auth.setCustomCookie({ response, payload: { name: "2FaTemporaryJWT", value: totpTemporaryJWT, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
|
|
263
|
+
newSecret = this.totpService.generateSecret(issuerName, userName);
|
|
264
|
+
this.cookieService.setTotpTemporary(response, { userName, newSecret, issuer: issuerName, pk: userPk, userCanSkipSetup, sessionDuration });
|
|
495
265
|
return {
|
|
496
266
|
body: {
|
|
497
267
|
loginAllowed: false,
|
|
@@ -501,8 +271,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
501
271
|
};
|
|
502
272
|
}
|
|
503
273
|
else {
|
|
504
|
-
|
|
505
|
-
this.adminforth.auth.setCustomCookie({ response, payload: { name: "2FaTemporaryJWT", value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
|
|
274
|
+
this.cookieService.setTotpTemporary(response, { userName, issuer: issuerName, pk: userPk, userCanSkipSetup, sessionDuration });
|
|
506
275
|
return {
|
|
507
276
|
body: {
|
|
508
277
|
loginAllowed: false,
|
|
@@ -514,583 +283,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
514
283
|
}));
|
|
515
284
|
}
|
|
516
285
|
setupEndpoints(server) {
|
|
517
|
-
server.
|
|
518
|
-
|
|
519
|
-
path: `/plugin/twofa/initSetup`,
|
|
520
|
-
noAuth: true,
|
|
521
|
-
handler: (server) => __awaiter(this, void 0, void 0, function* () {
|
|
522
|
-
const toReturn = { totpJWT: null, status: 'ok', };
|
|
523
|
-
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: server.cookies, name: "2FaTemporaryJWT" });
|
|
524
|
-
if (totpTemporaryJWT) {
|
|
525
|
-
toReturn.totpJWT = totpTemporaryJWT;
|
|
526
|
-
}
|
|
527
|
-
return toReturn;
|
|
528
|
-
})
|
|
529
|
-
});
|
|
530
|
-
// this enpoints sets final login cookie if 2nd factor is confirmed
|
|
531
|
-
server.endpoint({
|
|
532
|
-
method: 'POST',
|
|
533
|
-
path: `/plugin/twofa/confirmLogin`,
|
|
534
|
-
noAuth: true,
|
|
535
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
|
|
536
|
-
const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
|
|
537
|
-
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "2FaTemporaryJWT" });
|
|
538
|
-
if (!totpTemporaryJWT) {
|
|
539
|
-
return { error: 'Login session expired. Please log in again.' };
|
|
540
|
-
}
|
|
541
|
-
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'temp2FA');
|
|
542
|
-
if (!decoded) {
|
|
543
|
-
return { error: 'Login session expired. Please log in again.' };
|
|
544
|
-
}
|
|
545
|
-
if (decoded.newSecret) {
|
|
546
|
-
// set up standard TOTP request - ensured by presence of newSecret in temp2FA token
|
|
547
|
-
const verified = body.skip && decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code, this.options.timeStepWindow);
|
|
548
|
-
if (verified) {
|
|
549
|
-
this.connectors = this.adminforth.connectors;
|
|
550
|
-
if (!body.skip) {
|
|
551
|
-
const connector = this.connectors[this.authResource.dataSource];
|
|
552
|
-
yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
|
|
553
|
-
}
|
|
554
|
-
this.adminforth.auth.removeCustomCookie({ response, name: '2FaTemporaryJWT' });
|
|
555
|
-
this.adminforth.auth.setAuthCookie({ expireInDuration: decoded.sessionDuration, response, username: decoded.userName, pk: decoded.pk });
|
|
556
|
-
return { status: 'ok', allowedLogin: true };
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
return { error: 'Wrong or expired OTP code' };
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
else {
|
|
563
|
-
// login with confirming existing TOTP or Passkey
|
|
564
|
-
let verified = null;
|
|
565
|
-
if (body.usePasskey && this.options.passkeys) {
|
|
566
|
-
// passkeys are enabled and user wants to use them
|
|
567
|
-
const cookiesValidationResult = yield this.validateCookiesForPasskeyLogin(cookies);
|
|
568
|
-
if (!cookiesValidationResult.ok) {
|
|
569
|
-
return { error: cookiesValidationResult.error };
|
|
570
|
-
}
|
|
571
|
-
const res = yield this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, cookiesValidationResult.decodedPasskeysCookies);
|
|
572
|
-
if (res.ok && res.passkeyConfirmed) {
|
|
573
|
-
verified = true;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
// user already has TOTP secret, get it
|
|
578
|
-
this.connectors = this.adminforth.connectors;
|
|
579
|
-
const connector = this.connectors[this.authResource.dataSource];
|
|
580
|
-
const user = yield connector.getRecordByPrimaryKey(this.authResource, decoded.pk);
|
|
581
|
-
verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
|
|
582
|
-
}
|
|
583
|
-
if (verified) {
|
|
584
|
-
this.adminforth.auth.removeCustomCookie({ response, name: '2FaTemporaryJWT' });
|
|
585
|
-
this.adminforth.auth.setAuthCookie({ expireInDuration: decoded.sessionDuration, response, username: decoded.userName, pk: decoded.pk });
|
|
586
|
-
return { status: 'ok', allowedLogin: true };
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
response.setStatus(403, "Wrong or expired TOTP code");
|
|
590
|
-
return { error: 'Wrong or expired TOTP code', };
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
})
|
|
594
|
-
});
|
|
595
|
-
server.endpoint({
|
|
596
|
-
method: 'POST',
|
|
597
|
-
path: `/plugin/twofa/confirmLoginWithPasskey`,
|
|
598
|
-
noAuth: true,
|
|
599
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, response, cookies, headers, requestUrl, query }) {
|
|
600
|
-
if (this.options.passkeys.allowLoginWithPasskeys !== true) {
|
|
601
|
-
return { error: 'Login with passkeys is not allowed' };
|
|
602
|
-
}
|
|
603
|
-
const passkeyResponse = body.passkeyResponse;
|
|
604
|
-
if (!passkeyResponse) {
|
|
605
|
-
return { error: 'Passkey response is required' };
|
|
606
|
-
}
|
|
607
|
-
const cookiesValidationResult = yield this.validateCookiesForPasskeyLogin(cookies);
|
|
608
|
-
if (!cookiesValidationResult.ok) {
|
|
609
|
-
return { error: cookiesValidationResult.error };
|
|
610
|
-
}
|
|
611
|
-
let parsedPasskeyResponse;
|
|
612
|
-
try {
|
|
613
|
-
parsedPasskeyResponse = JSON.parse(passkeyResponse.response);
|
|
614
|
-
}
|
|
615
|
-
catch (e) {
|
|
616
|
-
return { error: 'Malformed passkey response' };
|
|
617
|
-
}
|
|
618
|
-
const credential_id = parsedPasskeyResponse.id;
|
|
619
|
-
if (!credential_id) {
|
|
620
|
-
return { error: 'Credential ID is required' };
|
|
621
|
-
}
|
|
622
|
-
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credential_id)]);
|
|
623
|
-
if (!passkeyRecord) {
|
|
624
|
-
return { error: 'No such passkey found, most likeley it was removed on this website but you still have it on your device' };
|
|
625
|
-
}
|
|
626
|
-
const userPk = passkeyRecord[this.options.passkeys.credentialUserIdFieldName];
|
|
627
|
-
if (!userPk) {
|
|
628
|
-
return { error: 'User ID not found in passkey record' };
|
|
629
|
-
}
|
|
630
|
-
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
631
|
-
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
632
|
-
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
633
|
-
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
634
|
-
const user = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, userPk)]);
|
|
635
|
-
if (!user) {
|
|
636
|
-
return { error: 'User not found' };
|
|
637
|
-
}
|
|
638
|
-
const verificationResult = yield this.verifyPasskeyResponse(passkeyResponse, userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
639
|
-
if (!verificationResult.ok || !verificationResult.passkeyConfirmed) {
|
|
640
|
-
return { error: 'Passkey verification failed' };
|
|
641
|
-
}
|
|
642
|
-
const username = user[this.adminforth.config.auth.usernameField];
|
|
643
|
-
const adminUser = {
|
|
644
|
-
dbUser: user,
|
|
645
|
-
pk: user.id,
|
|
646
|
-
username,
|
|
647
|
-
};
|
|
648
|
-
const toReturn = { allowedLogin: true, error: '' };
|
|
649
|
-
const rememberMe = (body === null || body === void 0 ? void 0 : body.rememberMe) || false;
|
|
650
|
-
yield this.adminforth.restApi.processLoginCallbacks(adminUser, toReturn, response, {
|
|
651
|
-
headers,
|
|
652
|
-
cookies,
|
|
653
|
-
requestUrl,
|
|
654
|
-
query,
|
|
655
|
-
body: {},
|
|
656
|
-
response,
|
|
657
|
-
meta: {
|
|
658
|
-
loginAllowedByPasskeyDirectSignIn: true
|
|
659
|
-
},
|
|
660
|
-
}, rememberMe ? this.adminforth.config.auth.rememberMeDuration || '30d' : '1d');
|
|
661
|
-
const rememberDaysAfterPasskeyLogin = (this.options.passkeys.rememberDaysAfterPasskeyLogin ? this.options.passkeys.rememberDaysAfterPasskeyLogin.toString().concat('d') : null);
|
|
662
|
-
if (toReturn.allowedLogin === true) {
|
|
663
|
-
this.adminforth.auth.setAuthCookie({
|
|
664
|
-
response,
|
|
665
|
-
username,
|
|
666
|
-
pk: user.id,
|
|
667
|
-
expireInDuration: rememberDaysAfterPasskeyLogin ? rememberDaysAfterPasskeyLogin : this.adminforth.config.auth.rememberMeDuration,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
return toReturn;
|
|
671
|
-
})
|
|
672
|
-
}),
|
|
673
|
-
server.endpoint({
|
|
674
|
-
method: "GET",
|
|
675
|
-
path: "/plugin/twofa/skip-allow",
|
|
676
|
-
noAuth: true,
|
|
677
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
|
|
678
|
-
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({
|
|
679
|
-
cookies,
|
|
680
|
-
name: `2FaTemporaryJWT`,
|
|
681
|
-
});
|
|
682
|
-
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "temp2FA");
|
|
683
|
-
if (!decoded) {
|
|
684
|
-
return { status: "error", message: "Invalid token" };
|
|
685
|
-
}
|
|
686
|
-
if (!decoded.newSecret) {
|
|
687
|
-
return { status: "ok", skipAllowed: false };
|
|
688
|
-
}
|
|
689
|
-
else {
|
|
690
|
-
return {
|
|
691
|
-
status: "ok",
|
|
692
|
-
skipAllowed: decoded.userCanSkipSetup,
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
}),
|
|
696
|
-
});
|
|
697
|
-
server.endpoint({
|
|
698
|
-
method: "GET",
|
|
699
|
-
path: "/plugin/twofa/skip-allow-modal",
|
|
700
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, headers, cookies }) {
|
|
701
|
-
if (this.options.usersFilterToApply) {
|
|
702
|
-
const res = this.options.usersFilterToApply(adminUser);
|
|
703
|
-
if (res === false) {
|
|
704
|
-
return { skipAllowed: true };
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
if (this.options.usersFilterToAllowSkipSetup) {
|
|
708
|
-
const res = yield this.checkIfSkipSetupAllowSkipVerify(adminUser);
|
|
709
|
-
if (res.skipAllowed === true) {
|
|
710
|
-
return { skipAllowed: true };
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (this.options.stepUpMfaGracePeriodSeconds) {
|
|
714
|
-
const verificationResult = yield this.isTempSkip2FAGraceValid(headers, cookies, true);
|
|
715
|
-
if (verificationResult === true) {
|
|
716
|
-
return { skipAllowed: true };
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return { skipAllowed: false };
|
|
720
|
-
}),
|
|
721
|
-
});
|
|
722
|
-
server.endpoint({
|
|
723
|
-
method: 'POST',
|
|
724
|
-
path: `/plugin/twofa/verify`,
|
|
725
|
-
noAuth: false,
|
|
726
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, body }) {
|
|
727
|
-
if (!(body === null || body === void 0 ? void 0 : body.code))
|
|
728
|
-
return { error: 'Code is required' };
|
|
729
|
-
const authRes = this.adminforth.config.resources
|
|
730
|
-
.find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
|
|
731
|
-
const connector = this.adminforth.connectors[authRes.dataSource];
|
|
732
|
-
const pkName = authRes.columns.find(c => c.primaryKey).name;
|
|
733
|
-
const user = yield connector.getRecordByPrimaryKey(authRes, adminUser.dbUser[pkName]);
|
|
734
|
-
const secret = user[this.options.twoFaSecretFieldName];
|
|
735
|
-
if (!secret)
|
|
736
|
-
return { error: '2FA is not set up for this user' };
|
|
737
|
-
const verified = twofactor.verifyToken(secret, body.code, this.options.timeStepWindow);
|
|
738
|
-
return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
|
|
739
|
-
})
|
|
740
|
-
});
|
|
741
|
-
server.endpoint({
|
|
742
|
-
method: 'POST',
|
|
743
|
-
path: `/plugin/passkeys/registerPasskeyRequest`,
|
|
744
|
-
noAuth: false,
|
|
745
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
|
|
746
|
-
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
747
|
-
const mode = body === null || body === void 0 ? void 0 : body.mode;
|
|
748
|
-
const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
|
|
749
|
-
const verificationResult = yield this.verify(confirmationResult, {
|
|
750
|
-
adminUser: adminUser,
|
|
751
|
-
userPk: adminUser.pk,
|
|
752
|
-
cookies: cookies,
|
|
753
|
-
response: response,
|
|
754
|
-
extra: {
|
|
755
|
-
headers,
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
if (!verificationResult || !('ok' in verificationResult)) {
|
|
759
|
-
return { ok: false, error: 'error' in verificationResult ? verificationResult.error : 'Verification failed' };
|
|
760
|
-
}
|
|
761
|
-
const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
|
|
762
|
-
const rp = {
|
|
763
|
-
name: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.rp.name) || this.adminforth.config.customization.brandName,
|
|
764
|
-
id: ((_f = (_e = (_d = this.options.passkeys) === null || _d === void 0 ? void 0 : _d.settings) === null || _e === void 0 ? void 0 : _e.rp) === null || _f === void 0 ? void 0 : _f.id) || (new URL(settingsOrigin)).hostname,
|
|
765
|
-
};
|
|
766
|
-
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
767
|
-
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
768
|
-
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
769
|
-
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
770
|
-
const userInfo = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, adminUser.pk)]);
|
|
771
|
-
const user = {
|
|
772
|
-
id: adminUser.pk,
|
|
773
|
-
name: userInfo[(_g = this.options.passkeys) === null || _g === void 0 ? void 0 : _g.settings.user.nameField],
|
|
774
|
-
displayName: userInfo[(_h = this.options.passkeys) === null || _h === void 0 ? void 0 : _h.settings.user.displayNameField] ? userInfo[(_j = this.options.passkeys) === null || _j === void 0 ? void 0 : _j.settings.user.displayNameField] : userInfo[(_k = this.options.passkeys) === null || _k === void 0 ? void 0 : _k.settings.user.nameField],
|
|
775
|
-
};
|
|
776
|
-
const excludeCredentials = [];
|
|
777
|
-
const temp = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
778
|
-
for (const rec of temp) {
|
|
779
|
-
if (rec.credential_id && rec.credential_id.length > 0) {
|
|
780
|
-
const meta = this.parseIfNeeded(rec[this.options.passkeys.credentialMetaFieldName]);
|
|
781
|
-
excludeCredentials.push({
|
|
782
|
-
id: rec.credential_id,
|
|
783
|
-
type: "public-key",
|
|
784
|
-
transports: JSON.parse(meta.transports || '[]')
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
const options = yield generateRegistrationOptions({
|
|
789
|
-
rpName: rp.name,
|
|
790
|
-
rpID: rp.id,
|
|
791
|
-
userID: isoUint8Array.fromUTF8String(user.id),
|
|
792
|
-
userName: user.name,
|
|
793
|
-
userDisplayName: user.displayName,
|
|
794
|
-
excludeCredentials,
|
|
795
|
-
authenticatorSelection: {
|
|
796
|
-
authenticatorAttachment: mode,
|
|
797
|
-
requireResidentKey: ((_l = this.options.passkeys) === null || _l === void 0 ? void 0 : _l.settings.authenticatorSelection.requireResidentKey) || true,
|
|
798
|
-
userVerification: ((_m = this.options.passkeys) === null || _m === void 0 ? void 0 : _m.settings.authenticatorSelection.userVerification) || "required"
|
|
799
|
-
},
|
|
800
|
-
});
|
|
801
|
-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge, "user_id": adminUser.pk }, 'registerTempPasskeyChallenge', ((_o = this.options.passkeys) === null || _o === void 0 ? void 0 : _o.challengeValidityPeriod) || '1m');
|
|
802
|
-
this.adminforth.auth.setCustomCookie({ response, payload: { name: "registerPasskeyTemporaryJWT", value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
|
|
803
|
-
return { ok: true, data: options };
|
|
804
|
-
})
|
|
805
|
-
});
|
|
806
|
-
server.endpoint({
|
|
807
|
-
method: 'POST',
|
|
808
|
-
path: `/plugin/passkeys/finishRegisteringPasskey`,
|
|
809
|
-
noAuth: false,
|
|
810
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, cookies }) {
|
|
811
|
-
var _b, _c, _d, _e, _f, _g;
|
|
812
|
-
const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "registerPasskeyTemporaryJWT" });
|
|
813
|
-
if (!passkeysCookies) {
|
|
814
|
-
return { error: 'Passkey token is required' };
|
|
815
|
-
}
|
|
816
|
-
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'registerTempPasskeyChallenge', false);
|
|
817
|
-
if (!decodedPasskeysCookies) {
|
|
818
|
-
return { error: 'Invalid passkey token' };
|
|
819
|
-
}
|
|
820
|
-
if (decodedPasskeysCookies.user_id !== adminUser.pk) {
|
|
821
|
-
return { error: 'Invalid user' };
|
|
822
|
-
}
|
|
823
|
-
const secret = adminUser.dbUser[this.options.twoFaSecretFieldName];
|
|
824
|
-
if (!secret || secret.length === 0) {
|
|
825
|
-
return { error: 'TOTP must be set up before registering a passkey' };
|
|
826
|
-
}
|
|
827
|
-
const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
|
|
828
|
-
const expectedOrigin = body.origin;
|
|
829
|
-
const expectedChallenge = decodedPasskeysCookies.challenge;
|
|
830
|
-
const expectedRPID = ((_e = (_d = (_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings) === null || _d === void 0 ? void 0 : _d.rp) === null || _e === void 0 ? void 0 : _e.id) || (new URL(settingsOrigin)).hostname;
|
|
831
|
-
const response = JSON.parse(body.credential);
|
|
832
|
-
try {
|
|
833
|
-
if (settingsOrigin !== expectedOrigin) {
|
|
834
|
-
throw new Error('Invalid origin');
|
|
835
|
-
}
|
|
836
|
-
const { verified, registrationInfo } = yield verifyRegistrationResponse({
|
|
837
|
-
response,
|
|
838
|
-
expectedChallenge,
|
|
839
|
-
expectedOrigin,
|
|
840
|
-
expectedRPID,
|
|
841
|
-
requireUserVerification: ((_f = this.options.passkeys) === null || _f === void 0 ? void 0 : _f.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
|
|
842
|
-
});
|
|
843
|
-
if (!verified) {
|
|
844
|
-
throw new Error('Verification failed.');
|
|
845
|
-
}
|
|
846
|
-
const { aaguid, credential, credentialBackedUp } = registrationInfo;
|
|
847
|
-
const provider_name = ((_g = aaguids[aaguid]) === null || _g === void 0 ? void 0 : _g.name) || 'Unknown provider';
|
|
848
|
-
const credentialPublicKey = credential.publicKey;
|
|
849
|
-
const credentialID = credential.id;
|
|
850
|
-
const base64CredentialID = credentialID;
|
|
851
|
-
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
|
|
852
|
-
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
853
|
-
if (!credResource) {
|
|
854
|
-
throw new Error('Credential resource not found.');
|
|
855
|
-
}
|
|
856
|
-
yield this.adminforth.createResourceRecord({
|
|
857
|
-
resource: credResource,
|
|
858
|
-
record: {
|
|
859
|
-
[this.options.passkeys.credentialIdFieldName]: base64CredentialID,
|
|
860
|
-
[this.options.passkeys.credentialUserIdFieldName]: adminUser.pk,
|
|
861
|
-
[this.options.passkeys.credentialMetaFieldName]: JSON.stringify({
|
|
862
|
-
public_key: base64PublicKey,
|
|
863
|
-
public_key_algorithm: response.response.publicKeyAlgorithm,
|
|
864
|
-
sign_count: 0,
|
|
865
|
-
transports: JSON.stringify(response.response.transports),
|
|
866
|
-
created_at: new Date().toISOString(),
|
|
867
|
-
last_used_at: new Date().toISOString(),
|
|
868
|
-
aaguid: aaguid,
|
|
869
|
-
name: provider_name,
|
|
870
|
-
}),
|
|
871
|
-
},
|
|
872
|
-
adminUser: adminUser
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
catch (error) {
|
|
876
|
-
console.error(error);
|
|
877
|
-
return { error: 'Error registering passkey: ' + error.message };
|
|
878
|
-
}
|
|
879
|
-
return { ok: true };
|
|
880
|
-
})
|
|
881
|
-
});
|
|
882
|
-
server.endpoint({
|
|
883
|
-
method: 'POST',
|
|
884
|
-
path: `/plugin/passkeys/signInRequest`,
|
|
885
|
-
noAuth: true,
|
|
886
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ response }) {
|
|
887
|
-
var _b, _c, _d;
|
|
888
|
-
try {
|
|
889
|
-
const options = yield generateAuthenticationOptions({
|
|
890
|
-
rpID: (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.rp.id,
|
|
891
|
-
userVerification: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.authenticatorSelection.userVerification) || "required"
|
|
892
|
-
});
|
|
893
|
-
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempLoginPasskeyChallenge', ((_d = this.options.passkeys) === null || _d === void 0 ? void 0 : _d.challengeValidityPeriod) || '1m');
|
|
894
|
-
this.adminforth.auth.setCustomCookie({ response, payload: { name: `passkeyLoginTemporaryJWT`, value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
|
|
895
|
-
return { ok: true, data: options };
|
|
896
|
-
}
|
|
897
|
-
catch (e) {
|
|
898
|
-
return { ok: false, error: e };
|
|
899
|
-
}
|
|
900
|
-
})
|
|
901
|
-
});
|
|
902
|
-
server.endpoint({
|
|
903
|
-
method: 'GET',
|
|
904
|
-
path: `/plugin/passkeys/getPasskeys`,
|
|
905
|
-
noAuth: false,
|
|
906
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
|
|
907
|
-
var _b, _c, _d;
|
|
908
|
-
let passkeys;
|
|
909
|
-
try {
|
|
910
|
-
passkeys = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
911
|
-
}
|
|
912
|
-
catch (error) {
|
|
913
|
-
return { ok: false, error: 'Error fetching passkeys: ' + error.message };
|
|
914
|
-
}
|
|
915
|
-
let dataToReturn = [];
|
|
916
|
-
for (const pk of passkeys) {
|
|
917
|
-
const parsedKey = this.parseIfNeeded(pk[this.options.passkeys.credentialMetaFieldName]);
|
|
918
|
-
dataToReturn.push({
|
|
919
|
-
name: parsedKey.name,
|
|
920
|
-
light_icon: ((_b = aaguids[parsedKey.aaguid]) === null || _b === void 0 ? void 0 : _b.icon_light) || null,
|
|
921
|
-
dark_icon: ((_c = aaguids[parsedKey.aaguid]) === null || _c === void 0 ? void 0 : _c.icon_dark) || null,
|
|
922
|
-
created_at: parsedKey.created_at,
|
|
923
|
-
last_used_at: parsedKey.last_used_at,
|
|
924
|
-
id: pk[this.options.passkeys.credentialIdFieldName],
|
|
925
|
-
});
|
|
926
|
-
}
|
|
927
|
-
return { ok: true, data: dataToReturn, authenticatorAttachment: ((_d = this.options.passkeys) === null || _d === void 0 ? void 0 : _d.settings.authenticatorSelection.authenticatorAttachment) || 'both' };
|
|
928
|
-
})
|
|
929
|
-
});
|
|
930
|
-
server.endpoint({
|
|
931
|
-
method: 'DELETE',
|
|
932
|
-
path: `/plugin/passkeys/deletePasskey`,
|
|
933
|
-
noAuth: false,
|
|
934
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
935
|
-
const credentialID = body.passkeyId;
|
|
936
|
-
if (!credentialID) {
|
|
937
|
-
return { ok: false, error: 'Passkey ID is required' };
|
|
938
|
-
}
|
|
939
|
-
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
940
|
-
if (!passkeyRecord) {
|
|
941
|
-
return { ok: false, error: 'Passkey not found' };
|
|
942
|
-
}
|
|
943
|
-
try {
|
|
944
|
-
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
945
|
-
if (!credResource) {
|
|
946
|
-
throw new Error('Credential resource not found.');
|
|
947
|
-
}
|
|
948
|
-
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
|
|
949
|
-
if (!credResourcePKColumn) {
|
|
950
|
-
throw new Error('Credential resource primary key not found.');
|
|
951
|
-
}
|
|
952
|
-
const credResourcePKName = credResourcePKColumn.name;
|
|
953
|
-
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
954
|
-
if (!passkeyRecord) {
|
|
955
|
-
return { ok: false, error: 'Passkey not found' };
|
|
956
|
-
}
|
|
957
|
-
yield this.adminforth.deleteResourceRecord({
|
|
958
|
-
resource: credResource,
|
|
959
|
-
recordId: passkeyRecord[credResourcePKName],
|
|
960
|
-
record: passkeyRecord,
|
|
961
|
-
adminUser: adminUser,
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
catch (error) {
|
|
965
|
-
return { ok: false, error: 'Error deleting passkey: ' + error.message };
|
|
966
|
-
}
|
|
967
|
-
return { ok: true };
|
|
968
|
-
})
|
|
969
|
-
});
|
|
970
|
-
server.endpoint({
|
|
971
|
-
method: 'POST',
|
|
972
|
-
path: `/plugin/passkeys/renamePasskey`,
|
|
973
|
-
noAuth: false,
|
|
974
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
975
|
-
const credentialID = body.passkeyId;
|
|
976
|
-
const newName = body.newName;
|
|
977
|
-
if (!credentialID) {
|
|
978
|
-
return { ok: false, error: 'Passkey ID is required' };
|
|
979
|
-
}
|
|
980
|
-
if (!newName) {
|
|
981
|
-
return { ok: false, error: 'New name is required' };
|
|
982
|
-
}
|
|
983
|
-
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
984
|
-
if (!credResource) {
|
|
985
|
-
throw new Error('Credential resource not found.');
|
|
986
|
-
}
|
|
987
|
-
const credResourcePKColumn = credResource.columns.find(c => c.primaryKey);
|
|
988
|
-
if (!credResourcePKColumn) {
|
|
989
|
-
throw new Error('Credential resource primary key not found.');
|
|
990
|
-
}
|
|
991
|
-
const credResourcePKName = credResourcePKColumn.name;
|
|
992
|
-
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, credentialID), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
993
|
-
if (!passkeyRecord) {
|
|
994
|
-
return { ok: false, error: 'Passkey not found' };
|
|
995
|
-
}
|
|
996
|
-
const meta = this.parseIfNeeded(passkeyRecord[this.options.passkeys.credentialMetaFieldName]);
|
|
997
|
-
meta.name = newName;
|
|
998
|
-
const newRecord = Object.assign(Object.assign({}, passkeyRecord), { [this.options.passkeys.credentialMetaFieldName]: JSON.stringify(meta) });
|
|
999
|
-
try {
|
|
1000
|
-
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
1001
|
-
if (!credResource) {
|
|
1002
|
-
throw new Error('Credential resource not found.');
|
|
1003
|
-
}
|
|
1004
|
-
yield this.adminforth.updateResourceRecord({
|
|
1005
|
-
resource: credResource,
|
|
1006
|
-
recordId: passkeyRecord[credResourcePKName],
|
|
1007
|
-
oldRecord: passkeyRecord,
|
|
1008
|
-
record: newRecord,
|
|
1009
|
-
adminUser: adminUser
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
catch (error) {
|
|
1013
|
-
return { ok: false, error: 'Error renaming passkey: ' + error.message };
|
|
1014
|
-
}
|
|
1015
|
-
return { ok: true };
|
|
1016
|
-
})
|
|
1017
|
-
});
|
|
1018
|
-
server.endpoint({
|
|
1019
|
-
method: 'POST',
|
|
1020
|
-
path: `/plugin/passkeys/checkIfUserHasPasskeys`,
|
|
1021
|
-
noAuth: true,
|
|
1022
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
|
|
1023
|
-
if (!this.options.passkeys) {
|
|
1024
|
-
return { ok: false, hasPasskeys: false };
|
|
1025
|
-
}
|
|
1026
|
-
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "2FaTemporaryJWT" });
|
|
1027
|
-
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'temp2FA');
|
|
1028
|
-
if (!decoded)
|
|
1029
|
-
return { ok: false, error: 'Invalid token' };
|
|
1030
|
-
if (decoded.newSecret) {
|
|
1031
|
-
return { ok: true, hasPasskeys: false };
|
|
1032
|
-
}
|
|
1033
|
-
const passkeys = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, decoded.pk)]);
|
|
1034
|
-
if (passkeys && passkeys.length > 0) {
|
|
1035
|
-
return { ok: true, hasPasskeys: true };
|
|
1036
|
-
}
|
|
1037
|
-
else {
|
|
1038
|
-
return { ok: true, hasPasskeys: false };
|
|
1039
|
-
}
|
|
1040
|
-
})
|
|
1041
|
-
});
|
|
1042
|
-
server.endpoint({
|
|
1043
|
-
method: 'POST',
|
|
1044
|
-
path: `/plugin/passkeys/resolveVerifyAuto`,
|
|
1045
|
-
noAuth: false,
|
|
1046
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, headers }) {
|
|
1047
|
-
const sessionsIds = body === null || body === void 0 ? void 0 : body.sessionsIds;
|
|
1048
|
-
const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
|
|
1049
|
-
const idsToResolve = Array.isArray(sessionsIds) ? sessionsIds : [];
|
|
1050
|
-
const resolveAllIdsAsFailed = (message) => {
|
|
1051
|
-
for (const id of idsToResolve) {
|
|
1052
|
-
this.resolveResponse(id, { ok: false, error: message });
|
|
1053
|
-
}
|
|
1054
|
-
return { ok: false, error: message };
|
|
1055
|
-
};
|
|
1056
|
-
try {
|
|
1057
|
-
if (!idsToResolve.length || !confirmationResult) {
|
|
1058
|
-
return (resolveAllIdsAsFailed('Confirmation window was closed or did not return required data'));
|
|
1059
|
-
}
|
|
1060
|
-
for (const id of idsToResolve) {
|
|
1061
|
-
const validationResult = yield this.adminforth.auth.verify(id, 'auto2FA', false);
|
|
1062
|
-
if (!validationResult) {
|
|
1063
|
-
return (resolveAllIdsAsFailed('Invalid session ID or confirmation result'));
|
|
1064
|
-
}
|
|
1065
|
-
if (validationResult.adminUserPk !== adminUser.pk) {
|
|
1066
|
-
return (resolveAllIdsAsFailed('Session does not belong to the authenticated user'));
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
const verificationResult = yield this.verify(confirmationResult, {
|
|
1070
|
-
adminUser: adminUser,
|
|
1071
|
-
userPk: adminUser.pk,
|
|
1072
|
-
cookies: cookies,
|
|
1073
|
-
response: response,
|
|
1074
|
-
extra: {
|
|
1075
|
-
headers: headers,
|
|
1076
|
-
}
|
|
1077
|
-
});
|
|
1078
|
-
if (!verificationResult || !('ok' in verificationResult)) {
|
|
1079
|
-
return (resolveAllIdsAsFailed('Verification failed'));
|
|
1080
|
-
}
|
|
1081
|
-
if ('ok' in verificationResult && verificationResult.ok) {
|
|
1082
|
-
for (const id of idsToResolve) {
|
|
1083
|
-
this.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
|
|
1084
|
-
}
|
|
1085
|
-
return { ok: true };
|
|
1086
|
-
}
|
|
1087
|
-
return (resolveAllIdsAsFailed('Verification failed'));
|
|
1088
|
-
}
|
|
1089
|
-
catch (error) {
|
|
1090
|
-
console.error('[AdminForth 2FA] Error resolving automatic 2FA verification', error);
|
|
1091
|
-
return (resolveAllIdsAsFailed(error instanceof Error ? error.message : 'Verification failed'));
|
|
1092
|
-
}
|
|
1093
|
-
})
|
|
1094
|
-
});
|
|
286
|
+
registerTwoFaEndpoints(server, createTwoFaHandlers(this.ctx));
|
|
287
|
+
registerPasskeyEndpoints(server, createPasskeyHandlers(this.ctx));
|
|
1095
288
|
}
|
|
1096
289
|
}
|