@adminforth/two-factors-auth 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,8 +7,10 @@ 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 } from "adminforth";
10
+ import { AdminForthPlugin, Filters } from "adminforth";
11
11
  import twofactor from 'node-2fa';
12
+ import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
13
+ import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
12
14
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
13
15
  constructor(options) {
14
16
  super(options, import.meta.url);
@@ -43,22 +45,157 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
43
45
  return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
44
46
  });
45
47
  }
48
+ parsePeriod(period) {
49
+ if (!period)
50
+ return 0;
51
+ const match = /^(\d+)([dhms])$/.exec(period.trim());
52
+ if (!match)
53
+ throw new Error(`Invalid suggestionPeriod format: ${period}`);
54
+ const value = parseInt(match[1], 10);
55
+ const unit = match[2];
56
+ switch (unit) {
57
+ case 'd': return value * 24 * 60 * 60 * 1000;
58
+ case 'h': return value * 60 * 60 * 1000;
59
+ case 'm': return value * 60 * 1000;
60
+ case 's': return value * 1000;
61
+ default: return value;
62
+ }
63
+ }
64
+ verifyPasskeyResponse(body, user_id, cookies) {
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ var _a, _b, _c, _d, _e;
67
+ const settingsOrigin = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.expectedOrigin;
68
+ const expectedOrigin = body.origin;
69
+ const expectedChallenge = cookies.challenge;
70
+ 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;
71
+ const response = JSON.parse(body.response);
72
+ try {
73
+ if (settingsOrigin !== expectedOrigin) {
74
+ throw new Error('Invalid origin');
75
+ }
76
+ const cred = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, response.id)]);
77
+ if (!cred) {
78
+ throw new Error('Credential not found.');
79
+ }
80
+ const credMeta = JSON.parse(cred[this.options.passkeys.credentialMetaFieldName]);
81
+ if (!credMeta || !credMeta.public_key) {
82
+ throw new Error('Credential public key not found.');
83
+ }
84
+ const userResourceId = this.adminforth.config.auth.usersResourceId;
85
+ const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
86
+ const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
87
+ const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
88
+ const user = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, cred[this.options.passkeys.credentialUserIdFieldName])]);
89
+ if (!user || !user_id || user[usersPrimaryKeyFieldName] !== user_id) {
90
+ throw new Error('User not found.');
91
+ }
92
+ const { verified, authenticationInfo } = yield verifyAuthenticationResponse({
93
+ response,
94
+ expectedChallenge,
95
+ expectedOrigin,
96
+ expectedRPID,
97
+ credential: {
98
+ id: cred[this.options.passkeys.credentialIdFieldName],
99
+ publicKey: isoBase64URL.toBuffer(credMeta.public_key),
100
+ counter: credMeta.counter,
101
+ transports: credMeta.transports,
102
+ },
103
+ requireUserVerification: ((_e = this.options.passkeys) === null || _e === void 0 ? void 0 : _e.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
104
+ });
105
+ if (!verified) {
106
+ return { ok: false, error: 'User verification failed.' };
107
+ }
108
+ credMeta.counter = authenticationInfo.newCounter;
109
+ credMeta.last_used_at = new Date().toISOString();
110
+ yield this.adminforth.resource(this.options.passkeys.credentialResourceID).update(cred[this.options.passkeys.credentialIdFieldName], { [this.options.passkeys.credentialMetaFieldName]: JSON.stringify(credMeta) });
111
+ return { ok: true, passkeyConfirmed: true };
112
+ }
113
+ catch (e) {
114
+ return { ok: false, error: 'Error authenticating passkey: ' + e };
115
+ }
116
+ });
117
+ }
46
118
  modifyResourceConfig(adminforth, resourceConfig) {
119
+ var _a;
47
120
  super.modifyResourceConfig(adminforth, resourceConfig);
48
121
  this.adminforth = adminforth;
49
122
  this.adminForthAuth = adminforth.auth;
123
+ const suggestionPeriod = this.parsePeriod(((_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.suggestionPeriod) || "5d");
124
+ const isPasskeysEnabled = this.options.passkeys ? true : false;
50
125
  const customPages = this.adminforth.config.customization.customPages;
51
126
  customPages.push({
52
127
  path: '/confirm2fa',
53
- component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: { customLayout: true } }
128
+ component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: { customLayout: true, suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
54
129
  });
55
130
  customPages.push({
56
131
  path: '/setup2fa',
57
- component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true } }
132
+ component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true, suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
58
133
  });
59
134
  const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || [];
60
135
  everyPageBottomInjections.push({ file: this.componentPath('TwoFAModal.vue'), meta: {} });
61
136
  this.activate(resourceConfig, adminforth);
137
+ if (this.options.passkeys) {
138
+ if (!this.adminforth.config.auth.userMenuSettingsPages) {
139
+ this.adminforth.config.auth.userMenuSettingsPages = [];
140
+ }
141
+ this.adminforth.config.auth.userMenuSettingsPages.push({
142
+ icon: 'flowbite:lock-solid',
143
+ pageLabel: 'Passkeys',
144
+ slug: 'passkeys',
145
+ component: this.componentPath('TwoFactorsPasskeysSettings.vue'),
146
+ });
147
+ }
148
+ }
149
+ validateConfigAfterDiscover(adminforth, resourceConfig) {
150
+ if (this.options.passkeys) {
151
+ const adminForthResources = [];
152
+ for (const res of adminforth.config.resources) {
153
+ adminForthResources.push(res.resourceId);
154
+ }
155
+ if (!this.options.passkeys.credentialResourceID) {
156
+ throw new Error('Passkeys credentialResourceID is required');
157
+ }
158
+ if (!(adminForthResources.includes(this.options.passkeys.credentialResourceID))) {
159
+ throw new Error('Passkeys credentialResourceID is not valid');
160
+ }
161
+ if (!this.options.passkeys.credentialIdFieldName) {
162
+ throw new Error('Passkeys credentialIdFieldName is required');
163
+ }
164
+ if (!this.options.passkeys.credentialMetaFieldName) {
165
+ throw new Error('Passkeys credentialMetaFieldName is required');
166
+ }
167
+ if (!this.options.passkeys.credentialUserIdFieldName) {
168
+ throw new Error('Passkeys credentialUserIdFieldName is required');
169
+ }
170
+ if (!this.options.passkeys.settings) {
171
+ throw new Error('Passkeys settings are required when passkeys option is enabled');
172
+ }
173
+ if (!this.options.passkeys.settings.expectedOrigin) {
174
+ throw new Error('Passkeys settings.expectedOrigin is required');
175
+ }
176
+ const origin = new URL(this.options.passkeys.settings.expectedOrigin).origin;
177
+ if (origin !== this.options.passkeys.settings.expectedOrigin) {
178
+ throw new Error('Passkeys settings.expectedOrigin is not valid');
179
+ }
180
+ if (this.options.passkeys.settings.authenticatorSelection) {
181
+ if (this.options.passkeys.settings.authenticatorSelection.authenticatorAttachment) {
182
+ if (!['platform', 'cross-platform', 'both'].includes(this.options.passkeys.settings.authenticatorSelection.authenticatorAttachment)) {
183
+ throw new Error('Passkeys settings.authenticatorSelection.authenticatorAttachment is not valid');
184
+ }
185
+ }
186
+ if (this.options.passkeys.settings.authenticatorSelection.userVerification) {
187
+ if (!['required', 'discouraged'].includes(this.options.passkeys.settings.authenticatorSelection.userVerification)) {
188
+ throw new Error('Passkeys settings.authenticatorSelection.userVerification is not valid');
189
+ }
190
+ }
191
+ }
192
+ if (!this.options.passkeys.settings.user) {
193
+ throw new Error('Passkeys settings.user is required');
194
+ }
195
+ if (!this.options.passkeys.settings.user.nameField) {
196
+ throw new Error('Passkeys settings.user.nameField is required');
197
+ }
198
+ }
62
199
  }
63
200
  activate(resourceConfig, adminforth) {
64
201
  if (!this.options.twoFaSecretFieldName) {
@@ -83,8 +220,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
83
220
  const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
84
221
  const brandName = adminforth.config.customization.brandName;
85
222
  const brandNameSlug = adminforth.config.customization.brandNameSlug;
86
- const issuerName = (this.options.customBrendPrefix && this.options.customBrendPrefix.trim())
87
- ? this.options.customBrendPrefix.trim()
223
+ const issuerName = (this.options.customBrandPrefix && this.options.customBrandPrefix.trim())
224
+ ? this.options.customBrandPrefix.trim()
88
225
  : brandName;
89
226
  const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.usersResourceId);
90
227
  const authPk = authResource.columns.find((col) => col.primaryKey).name;
@@ -100,10 +237,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
100
237
  if (!secret) {
101
238
  const tempSecret = twofactor.generateSecret({ name: issuerName, account: userName });
102
239
  newSecret = tempSecret.secret;
240
+ const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'temp2FA', '2h');
241
+ this.adminforth.auth.setCustomCookie({ response, payload: { name: "2FaTemporaryJWT", value: totpTemporaryJWT, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
242
+ return {
243
+ body: {
244
+ loginAllowed: false,
245
+ redirectTo: secret ? '/confirm2fa' : '/setup2fa',
246
+ },
247
+ ok: true
248
+ };
103
249
  }
104
250
  else {
105
- const value = this.adminforth.auth.issueJWT({ userName, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
106
- response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
251
+ const value = this.adminforth.auth.issueJWT({ userName, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'temp2FA', '2h');
252
+ this.adminforth.auth.setCustomCookie({ response, payload: { name: "2FaTemporaryJWT", value: value, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
107
253
  return {
108
254
  body: {
109
255
  loginAllowed: false,
@@ -112,15 +258,6 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
112
258
  ok: true
113
259
  };
114
260
  }
115
- const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
116
- response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString()} `);
117
- return {
118
- body: {
119
- loginAllowed: false,
120
- redirectTo: secret ? '/confirm2fa' : '/setup2fa',
121
- },
122
- ok: true
123
- };
124
261
  }));
125
262
  }
126
263
  setupEndpoints(server) {
@@ -129,28 +266,28 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
129
266
  path: `/plugin/twofa/initSetup`,
130
267
  noAuth: true,
131
268
  handler: (server) => __awaiter(this, void 0, void 0, function* () {
132
- var _a;
133
269
  const toReturn = { totpJWT: null, status: 'ok', };
134
- const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
135
- const totpTemporaryJWT = (_a = server.cookies.find((cookie) => cookie.key === `adminforth_${brandNameSlug}_totpTemporaryJWT`)) === null || _a === void 0 ? void 0 : _a.value;
270
+ const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: server.cookies, name: "2FaTemporaryJWT" });
136
271
  if (totpTemporaryJWT) {
137
272
  toReturn.totpJWT = totpTemporaryJWT;
138
273
  }
139
274
  return toReturn;
140
275
  })
141
276
  });
277
+ // this enpoints sets final login cookie if 2nd factor is confirmed
142
278
  server.endpoint({
143
279
  method: 'POST',
144
- path: `/plugin/twofa/confirmSetup`,
280
+ path: `/plugin/twofa/confirmLogin`,
145
281
  noAuth: true,
146
282
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
147
- var _b;
148
283
  const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
149
- const totpTemporaryJWT = (_b = cookies.find((cookie) => cookie.key === `adminforth_${brandNameSlug}_totpTemporaryJWT`)) === null || _b === void 0 ? void 0 : _b.value;
150
- const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'tempTotp');
151
- if (!decoded)
284
+ const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "2FaTemporaryJWT" });
285
+ const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'temp2FA');
286
+ if (!decoded) {
152
287
  return { status: 'error', message: 'Invalid token' };
288
+ }
153
289
  if (decoded.newSecret) {
290
+ // set up standard TOTP request - ensured by presence of newSecret in temp2FA token
154
291
  const verified = body.skip && decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code, this.options.timeStepWindow);
155
292
  if (verified) {
156
293
  this.connectors = this.adminforth.connectors;
@@ -158,7 +295,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
158
295
  const connector = this.connectors[this.authResource.dataSource];
159
296
  yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
160
297
  }
161
- this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
298
+ this.adminforth.auth.removeCustomCookie({ response, name: '2FaTemporaryJWT' });
162
299
  this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
163
300
  return { status: 'ok', allowedLogin: true };
164
301
  }
@@ -167,13 +304,32 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
167
304
  }
168
305
  }
169
306
  else {
170
- // user already has secret, get it
171
- this.connectors = this.adminforth.connectors;
172
- const connector = this.connectors[this.authResource.dataSource];
173
- const user = yield connector.getRecordByPrimaryKey(this.authResource, decoded.pk);
174
- const verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
307
+ // login with confirming existing TOTP or Passkey
308
+ let verified = null;
309
+ if (body.usePasskey && this.options.passkeys) {
310
+ // passkeys are enabled and user wants to use them
311
+ const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyTemporaryJWT` });
312
+ if (!passkeysCookies) {
313
+ return { error: 'Passkey token is required' };
314
+ }
315
+ const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
316
+ if (!decodedPasskeysCookies) {
317
+ return { error: 'Invalid passkey token' };
318
+ }
319
+ const res = yield this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
320
+ if (res.ok && res.passkeyConfirmed) {
321
+ verified = true;
322
+ }
323
+ }
324
+ else {
325
+ // user already has TOTP secret, get it
326
+ this.connectors = this.adminforth.connectors;
327
+ const connector = this.connectors[this.authResource.dataSource];
328
+ const user = yield connector.getRecordByPrimaryKey(this.authResource, decoded.pk);
329
+ verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
330
+ }
175
331
  if (verified) {
176
- this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
332
+ this.adminforth.auth.removeCustomCookie({ response, name: '2FaTemporaryJWT' });
177
333
  this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
178
334
  return { status: 'ok', allowedLogin: true };
179
335
  }
@@ -188,10 +344,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
188
344
  path: "/plugin/twofa/skip-allow",
189
345
  noAuth: true,
190
346
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
191
- var _b;
192
- const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
193
- const totpTemporaryJWT = (_b = cookies.find((cookie) => cookie.key === `adminforth_${brandNameSlug}_totpTemporaryJWT`)) === null || _b === void 0 ? void 0 : _b.value;
194
- const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "tempTotp");
347
+ const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({
348
+ cookies,
349
+ name: `2FaTemporaryJWT`,
350
+ });
351
+ const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "temp2FA");
195
352
  if (!decoded) {
196
353
  return { status: "error", message: "Invalid token" };
197
354
  }
@@ -225,5 +382,262 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
225
382
  return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
226
383
  })
227
384
  });
385
+ server.endpoint({
386
+ method: 'POST',
387
+ path: `/plugin/passkeys/registerPasskeyRequest`,
388
+ noAuth: false,
389
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
390
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
391
+ const mode = body === null || body === void 0 ? void 0 : body.mode;
392
+ const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
393
+ const rp = {
394
+ name: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.rp.name) || this.adminforth.config.customization.brandName,
395
+ 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,
396
+ };
397
+ const userResourceId = this.adminforth.config.auth.usersResourceId;
398
+ const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
399
+ const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
400
+ const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
401
+ const userInfo = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, adminUser.pk)]);
402
+ const user = {
403
+ id: adminUser.pk,
404
+ name: userInfo[(_g = this.options.passkeys) === null || _g === void 0 ? void 0 : _g.settings.user.nameField],
405
+ 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],
406
+ };
407
+ const excludeCredentials = [];
408
+ const temp = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
409
+ for (const rec of temp) {
410
+ if (rec.credential_id && rec.credential_id.length > 0) {
411
+ const meta = JSON.parse(rec[this.options.passkeys.credentialMetaFieldName]);
412
+ excludeCredentials.push({
413
+ id: rec.credential_id,
414
+ type: "public-key",
415
+ transports: JSON.parse(meta.transports || '[]')
416
+ });
417
+ }
418
+ }
419
+ const options = yield generateRegistrationOptions({
420
+ rpName: rp.name,
421
+ rpID: rp.id,
422
+ userID: isoUint8Array.fromUTF8String(user.id),
423
+ userName: user.name,
424
+ userDisplayName: user.displayName,
425
+ excludeCredentials,
426
+ authenticatorSelection: {
427
+ authenticatorAttachment: mode,
428
+ requireResidentKey: ((_l = this.options.passkeys) === null || _l === void 0 ? void 0 : _l.settings.authenticatorSelection.requireResidentKey) || true,
429
+ userVerification: ((_m = this.options.passkeys) === null || _m === void 0 ? void 0 : _m.settings.authenticatorSelection.userVerification) || "required"
430
+ },
431
+ });
432
+ const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempPasskeyChallenge', '5m');
433
+ this.adminforth.auth.setCustomCookie({ response, payload: { name: "passkeyTemporaryJWT", value: value, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
434
+ return { ok: true, data: options };
435
+ })
436
+ });
437
+ server.endpoint({
438
+ method: 'POST',
439
+ path: `/plugin/passkeys/finishRegisteringPasskey`,
440
+ noAuth: false,
441
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, cookies }) {
442
+ var _b, _c, _d, _e, _f;
443
+ const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "passkeyTemporaryJWT" });
444
+ if (!passkeysCookies) {
445
+ return { error: 'Passkey token is required' };
446
+ }
447
+ const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
448
+ if (!decodedPasskeysCookies) {
449
+ return { error: 'Invalid passkey token' };
450
+ }
451
+ const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
452
+ const expectedOrigin = body.origin;
453
+ const expectedChallenge = decodedPasskeysCookies.challenge;
454
+ 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;
455
+ const response = JSON.parse(body.credential);
456
+ try {
457
+ if (settingsOrigin !== expectedOrigin) {
458
+ throw new Error('Invalid origin');
459
+ }
460
+ const { verified, registrationInfo } = yield verifyRegistrationResponse({
461
+ response,
462
+ expectedChallenge,
463
+ expectedOrigin,
464
+ expectedRPID,
465
+ requireUserVerification: ((_f = this.options.passkeys) === null || _f === void 0 ? void 0 : _f.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
466
+ });
467
+ if (!verified) {
468
+ throw new Error('Verification failed.');
469
+ }
470
+ const { aaguid, credential, credentialBackedUp } = registrationInfo;
471
+ const credentialPublicKey = credential.publicKey;
472
+ const credentialID = credential.id;
473
+ const base64CredentialID = credentialID;
474
+ const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
475
+ const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
476
+ if (!credResource) {
477
+ throw new Error('Credential resource not found.');
478
+ }
479
+ yield this.adminforth.createResourceRecord({
480
+ resource: credResource,
481
+ record: {
482
+ [this.options.passkeys.credentialIdFieldName]: base64CredentialID,
483
+ [this.options.passkeys.credentialUserIdFieldName]: adminUser.pk,
484
+ [this.options.passkeys.credentialMetaFieldName]: JSON.stringify({
485
+ public_key: base64PublicKey,
486
+ public_key_algorithm: response.response.publicKeyAlgorithm,
487
+ sign_count: 0,
488
+ transports: JSON.stringify(response.response.transports),
489
+ created_at: new Date().toISOString(),
490
+ last_used_at: new Date().toISOString(),
491
+ aaguid: aaguid,
492
+ name: `Passkey ${adminUser.username}`,
493
+ }),
494
+ },
495
+ adminUser: adminUser
496
+ });
497
+ }
498
+ catch (error) {
499
+ console.error(error);
500
+ return { error: 'Error registering passkey: ' + error.message };
501
+ }
502
+ return { ok: true };
503
+ })
504
+ });
505
+ server.endpoint({
506
+ method: 'POST',
507
+ path: `/plugin/passkeys/signInRequest`,
508
+ noAuth: true,
509
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ response }) {
510
+ var _b, _c;
511
+ try {
512
+ const options = yield generateAuthenticationOptions({
513
+ rpID: (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.rp.id,
514
+ userVerification: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.authenticatorSelection.userVerification) || "required"
515
+ });
516
+ const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempPasskeyChallenge', '5m');
517
+ this.adminforth.auth.setCustomCookie({ response, payload: { name: `passkeyTemporaryJWT`, value: value, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
518
+ return { ok: true, data: options };
519
+ }
520
+ catch (e) {
521
+ return { ok: false, error: e };
522
+ }
523
+ })
524
+ });
525
+ server.endpoint({
526
+ method: 'GET',
527
+ path: `/plugin/passkeys/getPasskeys`,
528
+ noAuth: false,
529
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
530
+ var _b;
531
+ let passkeys;
532
+ try {
533
+ passkeys = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
534
+ }
535
+ catch (error) {
536
+ return { ok: false, error: 'Error fetching passkeys: ' + error.message };
537
+ }
538
+ let dataToReturn = [];
539
+ for (const pk of passkeys) {
540
+ const parsedKey = JSON.parse(pk[this.options.passkeys.credentialMetaFieldName]);
541
+ dataToReturn.push({
542
+ name: parsedKey.name,
543
+ created_at: parsedKey.created_at,
544
+ last_used_at: parsedKey.last_used_at,
545
+ id: pk[this.options.passkeys.credentialIdFieldName],
546
+ });
547
+ }
548
+ return { ok: true, data: dataToReturn, authenticatorAttachment: ((_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.authenticatorSelection.authenticatorAttachment) || 'both' };
549
+ })
550
+ });
551
+ server.endpoint({
552
+ method: 'DELETE',
553
+ path: `/plugin/passkeys/deletePasskey`,
554
+ noAuth: false,
555
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
556
+ const passkeyId = body.passkeyId;
557
+ if (!passkeyId) {
558
+ return { ok: false, error: 'Passkey ID is required' };
559
+ }
560
+ const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, passkeyId), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
561
+ if (!passkeyRecord) {
562
+ return { ok: false, error: 'Passkey not found' };
563
+ }
564
+ try {
565
+ const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
566
+ if (!credResource) {
567
+ throw new Error('Credential resource not found.');
568
+ }
569
+ yield this.adminforth.deleteResourceRecord({
570
+ resource: credResource,
571
+ recordId: passkeyId,
572
+ record: passkeyRecord,
573
+ adminUser: adminUser,
574
+ });
575
+ }
576
+ catch (error) {
577
+ return { ok: false, error: 'Error deleting passkey: ' + error.message };
578
+ }
579
+ return { ok: true };
580
+ })
581
+ });
582
+ server.endpoint({
583
+ method: 'POST',
584
+ path: `/plugin/passkeys/renamePasskey`,
585
+ noAuth: false,
586
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
587
+ const passkeyId = body.passkeyId;
588
+ const newName = body.newName;
589
+ if (!passkeyId) {
590
+ return { ok: false, error: 'Passkey ID is required' };
591
+ }
592
+ if (!newName) {
593
+ return { ok: false, error: 'New name is required' };
594
+ }
595
+ const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, passkeyId), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
596
+ if (!passkeyRecord) {
597
+ return { ok: false, error: 'Passkey not found' };
598
+ }
599
+ const meta = JSON.parse(passkeyRecord[this.options.passkeys.credentialMetaFieldName]);
600
+ meta.name = newName;
601
+ const newRecord = Object.assign(Object.assign({}, passkeyRecord), { [this.options.passkeys.credentialMetaFieldName]: JSON.stringify(meta) });
602
+ try {
603
+ const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
604
+ if (!credResource) {
605
+ throw new Error('Credential resource not found.');
606
+ }
607
+ yield this.adminforth.updateResourceRecord({
608
+ resource: credResource,
609
+ recordId: passkeyId,
610
+ oldRecord: passkeyRecord,
611
+ record: newRecord,
612
+ adminUser: adminUser
613
+ });
614
+ }
615
+ catch (error) {
616
+ return { ok: false, error: 'Error renaming passkey: ' + error.message };
617
+ }
618
+ return { ok: true };
619
+ })
620
+ });
621
+ server.endpoint({
622
+ method: 'POST',
623
+ path: `/plugin/passkeys/checkIfUserHasPasskeys`,
624
+ noAuth: true,
625
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
626
+ const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "2FaTemporaryJWT" });
627
+ const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'temp2FA');
628
+ if (!decoded)
629
+ return { ok: false, error: 'Invalid token' };
630
+ if (decoded.newSecret) {
631
+ return { ok: true, hasPasskeys: false };
632
+ }
633
+ const passkeys = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, decoded.pk)]);
634
+ if (passkeys && passkeys.length > 0) {
635
+ return { ok: true, hasPasskeys: true };
636
+ }
637
+ else {
638
+ return { ok: true, hasPasskeys: false };
639
+ }
640
+ })
641
+ });
228
642
  }
229
643
  }