@adminforth/two-factors-auth 1.10.6 → 1.11.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.
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="text-3xl text-gray-900 font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
2
+ <div class="text-3xl text-lightBreadcrumbsText dark:text-darkBreadcrumbsText font-semibold max-w-2xl mr-6 flex-col justify-center items-center">
3
3
  <p class="flex items-start justify-start leading-none">Passkeys</p>
4
4
  <div class="flex flex-col items-end">
5
5
  <Table
@@ -17,17 +17,23 @@
17
17
  class="w-96"
18
18
  :buttons="[
19
19
  { label: 'Save', onclick: (dialog) => { renamePasskey(item.id, passkeysNewName); dialog.hide(); } },
20
- { label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700 dark:border-gray-900'}, onclick: (dialog) => {passkeysNewName = ''; dialog.hide();} },
20
+ { label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!bg-gray-800 dark:!text-gray-300 dark:hover:!bg-gray-700 dark:border-gray-900 dark:hover:!border-gray-800 dark:focus:!ring-gray-800'}, onclick: (dialog) => {passkeysNewName = ''; dialog.hide();} },
21
21
  ]"
22
22
  header="Edit Passkey"
23
23
  >
24
24
  <template #trigger>
25
- <div
26
- @click="passkeysNewName = ''"
27
- class="w-7 h-7 flex items-center justify-center hover:border rounded-md hover:shadow-md text-blue-500 hover:text-blue-700 cursor-pointer"
28
- >
29
- <IconPenSolid class="w-5 h-5" />
30
- </div>
25
+ <Tooltip>
26
+ <div
27
+ @click="passkeysNewName = ''"
28
+ class="w-7 h-7 flex items-center justify-center rounded-md text-blue-500 hover:text-blue-700 cursor-pointer"
29
+ >
30
+ <IconPenSolid class="w-5 h-5" />
31
+ </div>
32
+
33
+ <template #tooltip>
34
+ Rename passkey
35
+ </template>
36
+ </Tooltip>
31
37
  </template>
32
38
  <div>
33
39
  <p>Enter new passkey name:</p>
@@ -42,17 +48,22 @@
42
48
  <Dialog
43
49
  class="w-96"
44
50
  :buttons="[
45
- { label: 'Delete', options: {class: 'bg-red-700 !text-white hover:!bg-red-600 focus:ring-2 focus:ring-red-500'}, onclick: (dialog) => { deletePasskey(item.id); dialog.hide(); } },
46
- { label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200'}, onclick: (dialog) => dialog.hide() },
51
+ { label: 'Delete', options: {class: 'bg-red-700 !text-white hover:!bg-red-600 focus:ring-2 focus:ring-red-500 dark:!bg-red-700 dark:hover:!bg-red-600 dark:focus:ring-red-500 dark:!border-red-800'}, onclick: (dialog) => { deletePasskey(item.id); dialog.hide(); } },
52
+ { label: 'Cancel', options: {class: 'bg-white focus:!ring-gray-200 !text-gray-900 hover:!bg-gray-200 dark:!text-white dark:!bg-gray-700 dark:hover:!bg-gray-800 dark:!border-gray-600 dark:focus:!ring-gray-800'}, onclick: (dialog) => dialog.hide() },
47
53
  ]"
48
54
  header="Delete Passkey"
49
55
  >
50
56
  <template #trigger>
51
- <div class="w-7 h-7 flex items-center justify-center hover:border rounded-md hover:shadow-md text-red-500 hover:text-red-700 cursor-pointer">
52
- <IconTrashBinSolid
53
- class="w-5 h-5"
54
- />
55
- </div>
57
+ <Tooltip>
58
+ <div class="w-7 h-7 flex items-center justify-center rounded-md text-red-500 hover:text-red-700 cursor-pointer">
59
+ <IconTrashBinSolid
60
+ class="w-5 h-5"
61
+ />
62
+ </div>
63
+ <template #tooltip>
64
+ Delete passkey
65
+ </template>
66
+ </Tooltip>
56
67
  </template>
57
68
  <div>
58
69
  <p>Are you sure you want to delete this passkey?</p>
@@ -66,9 +77,9 @@
66
77
  </template>
67
78
  </Table>
68
79
  <div class="flex space-x-4 mt-4" v-if="isInitialFinished">
69
- <ButtonGroup :solidColor="true">
80
+ <ButtonGroup :solidColor="true" v-if="isFetchingPasskey === false">
70
81
  <template #button:Profile>
71
- <div class="flex px-4 py-2" @click="addPasskey()">
82
+ <div class="flex px-4 py-2" @click="addPasskeyStartAction()">
72
83
  <IconPlusOutline class="w-5 h-5 me-2"/>
73
84
  <p>{{ addPasskeyMode === 'platform' ? 'Add Local Passkey' : 'Add External Passkey' }}</p>
74
85
  </div>
@@ -79,6 +90,7 @@
79
90
  </div>
80
91
  </template>
81
92
  </ButtonGroup>
93
+ <p v-else class="flex items-center justify-center gap-2 text-base">Processing <Spinner class="w-4 h-4 inline-block" /></p>
82
94
  </div>
83
95
  <div v-if="isCardsVisible" id="cards-container" class="w-80 mt-2 border-gray-400 p-2 bg-white rounded-lg shadow-md flex flex-col space-y-2">
84
96
  <div v-if="isPasskeySupported" class="flex justify-between gap-4" :class="!isPasskeySupported ? 'opacity-50 pointer-events-none' : ''">
@@ -127,6 +139,29 @@
127
139
  </div>
128
140
  </div>
129
141
  </div>
142
+ <Dialog
143
+ ref="confirmDialog"
144
+ class="w-96"
145
+ :click-to-close-outside="false"
146
+ :buttons="[]"
147
+ >
148
+ <div class="flex flex-col">
149
+ <button
150
+ type="button"
151
+ class="text-lightDialogCloseButton bg-transparent hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover"
152
+ @click="{ isFetchingPasskey = false; fetchedOptions = null; closeDialog(); }"
153
+ >
154
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
155
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
156
+ </svg>
157
+ <span class="sr-only">Close dialog</span>
158
+ </button>
159
+ <div class="flex items-center justify-center space-y-4">
160
+ Now you can add a passkey.
161
+ </div>
162
+ <Button class="mt-4" @click="addPasskeyFinishAction(fetchedOptions); closeDialog();">Add Passkey</Button>
163
+ </div>
164
+ </Dialog>
130
165
  </div>
131
166
  </template>
132
167
 
@@ -135,7 +170,7 @@
135
170
  import { callAdminForthApi } from '@/utils';
136
171
  import adminforth from '@/adminforth';
137
172
  import { onMounted, ref, Ref, onBeforeUnmount } from 'vue';
138
- import { Card, Dialog, ButtonGroup, Tooltip } from '@/afcl'
173
+ import { Card, Dialog, ButtonGroup, Tooltip, Spinner, Button } from '@/afcl'
139
174
  import { IconTrashBinSolid, IconPenSolid, IconPlusOutline, IconCaretDownSolid, IconCheckOutline } from '@iconify-prerendered/vue-flowbite';
140
175
  import dayjs from 'dayjs';
141
176
  import utc from 'dayjs/plugin/utc';
@@ -152,6 +187,19 @@
152
187
  const addPasskeyMode: Ref<'platform' | 'cross-platform'> = ref('platform');
153
188
  const authenticatorAttachment = ref<'platform' | 'cross-platform' | 'both'>('platform');
154
189
  const isInitialFinished = ref(false);
190
+ const isFetchingPasskey = ref(false);
191
+
192
+ const confirmDialog = ref(null);
193
+
194
+ const openDialog = () => {
195
+ confirmDialog.value.open();
196
+ }
197
+
198
+ const closeDialog = () => {
199
+ confirmDialog.value.close();
200
+ }
201
+
202
+ const fetchedOptions = ref(null);
155
203
 
156
204
  onMounted(async () => {
157
205
  await getPasskeys();
@@ -179,13 +227,39 @@
179
227
  }
180
228
  }
181
229
 
182
- async function addPasskey() {
183
- const { options, challengeId } = await fetchInformationFromTheBackend();
230
+ async function addPasskeyStartAction() {
231
+ isFetchingPasskey.value = true;
232
+ let confirmationResult;
233
+ try {
234
+ confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(undefined, "To add passkey first verify yourself");
235
+ } catch (e) {
236
+ isFetchingPasskey.value = false;
237
+ return;
238
+ }
239
+ if (!confirmationResult) {
240
+ isFetchingPasskey.value = false;
241
+ return;
242
+ }
243
+ const { options } = await fetchInformationFromTheBackend(confirmationResult);
244
+ if (!options ) {
245
+ isFetchingPasskey.value = false;
246
+ adminforth.alert({message: 'Verification failed.', variant: 'warning'});
247
+ return;
248
+ }
249
+ fetchedOptions.value = options;
250
+ openDialog();
251
+ }
252
+
253
+ async function addPasskeyFinishAction(options) {
184
254
  const creationResult = await callWebAuthn(options);
185
255
  if (!creationResult) {
256
+ isFetchingPasskey.value = false;
257
+ fetchedOptions.value = null;
186
258
  return;
187
259
  }
188
- finishRegisteringPasskey(creationResult, challengeId);
260
+ fetchedOptions.value = null;
261
+ finishRegisteringPasskey(creationResult);
262
+ isFetchingPasskey.value = false;
189
263
  }
190
264
 
191
265
  async function getPasskeys() {
@@ -267,20 +341,24 @@
267
341
  }
268
342
  }
269
343
 
270
- async function fetchInformationFromTheBackend() {
344
+ async function fetchInformationFromTheBackend(confirmationResult) {
271
345
  let response;
272
346
  try {
273
347
  response = await callAdminForthApi({
274
348
  path: `/plugin/passkeys/registerPasskeyRequest`,
275
349
  method: 'POST',
276
350
  body: {
277
- mode: addPasskeyMode.value
351
+ mode: addPasskeyMode.value,
352
+ confirmationResult: confirmationResult
278
353
  },
279
354
  });
280
355
  } catch (error) {
281
356
  console.error('Error fetching passkeys info:', error);
282
357
  return;
283
358
  }
359
+ if (!response.ok) {
360
+ return {};
361
+ }
284
362
  const _options = response.data;
285
363
  const challengeId = response.challengeId;
286
364
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options);
@@ -303,7 +381,7 @@
303
381
  return result;
304
382
  }
305
383
 
306
- async function finishRegisteringPasskey(credential: any, challengeId: string) {
384
+ async function finishRegisteringPasskey(credential: any) {
307
385
  let res
308
386
  try {
309
387
  res = await callAdminForthApi({
@@ -312,7 +390,6 @@
312
390
  body: {
313
391
  credential: credential,
314
392
  origin: window.location.origin,
315
- challengeId: challengeId,
316
393
  },
317
394
  });
318
395
  } catch (error) {
@@ -80,7 +80,7 @@ import VOtpInput from "vue3-otp-input";
80
80
  import adminforth from '@/adminforth';
81
81
  import { useI18n } from 'vue-i18n';
82
82
  import { useRoute } from 'vue-router';
83
- import { handlePasskeyAlert } from './TwoFactorsConfirmation.vue';
83
+ import { handlePasskeyAlert } from './utils.js';
84
84
  import ErrorMessage from '@/components/ErrorMessage.vue';
85
85
 
86
86
  const { t } = useI18n();
@@ -0,0 +1,106 @@
1
+ import { callAdminForthApi } from '@/utils';
2
+
3
+ export function handlePasskeyAlert(propSuggestionPeriod, router) {
4
+ const currentDate = Date.now();
5
+ window.localStorage.removeItem('suggestionPeriod');
6
+ window.localStorage.setItem('suggestionPeriod', propSuggestionPeriod);
7
+ let suggestionPeriod = window.localStorage.getItem('suggestionPeriod');
8
+ let lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
9
+ let suggestPasskey = window.localStorage.getItem('suggestPasskey');
10
+ if ( !lastSuggestionDate ) {
11
+ window.localStorage.setItem('lastSuggestionDate', currentDate.toString());
12
+ lastSuggestionDate = window.localStorage.getItem('lastSuggestionDate');
13
+ }
14
+ if ( !suggestPasskey ) {
15
+ window.localStorage.setItem('suggestPasskey', 'true');
16
+ suggestPasskey = window.localStorage.getItem('suggestPasskey');
17
+ }
18
+ if ( currentDate - parseInt(lastSuggestionDate) > parseInt(suggestionPeriod) ) {
19
+ suggestPasskey = window.localStorage.getItem('suggestPasskey');
20
+ if (suggestPasskey !== 'true'){
21
+ if ( suggestPasskey === 'false' || !suggestPasskey ) {
22
+ window.localStorage.setItem('suggestPasskey', 'true');
23
+ } else if ( suggestPasskey !== 'never' ) {
24
+ window.localStorage.setItem('suggestPasskey', 'false');
25
+ }
26
+ }
27
+ }
28
+ suggestPasskey = window.localStorage.getItem('suggestPasskey');
29
+
30
+ if ( suggestPasskey === 'true' ) {
31
+ adminforth.alert({
32
+ message: 'Do you want to add passkey?',
33
+ variant: 'info',
34
+ buttons: [
35
+ { value: 'yes', label: 'Add passkey' },
36
+ { value: 'later', label: 'Later' },
37
+ { value: 'never', label: 'Never' },
38
+ ],
39
+ timeout: 'unlimited'
40
+ }).then((value) => {
41
+ switch (value) {
42
+ case 'yes':
43
+ router.push({ name: 'settings', params: { page: 'passkeys' } });
44
+ break;
45
+ case 'later':
46
+ window.localStorage.setItem('suggestPasskey', 'false');
47
+ break;
48
+ case 'never':
49
+ window.localStorage.setItem('suggestPasskey', 'never');
50
+ break;
51
+ default:
52
+ window.localStorage.setItem('suggestPasskey', 'false');
53
+ break;
54
+ }
55
+ });
56
+ }
57
+ }
58
+
59
+ export async function getPasskey() {
60
+ const { _options } = await createSignInRequest();
61
+ const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options);
62
+ const credential = await authenticate(options);
63
+ if (!credential) {
64
+ isFetchingPasskey.value = false;
65
+ return;
66
+ }
67
+ const result = JSON.stringify(credential);
68
+ const passkeyOptions = {
69
+ response: result,
70
+ origin: window.location.origin,
71
+ };
72
+ return passkeyOptions;
73
+ }
74
+
75
+ async function createSignInRequest() {
76
+ let response;
77
+ try {
78
+ response = await callAdminForthApi({
79
+ path: `/plugin/passkeys/signInRequest`,
80
+ method: 'POST',
81
+ });
82
+ } catch (error) {
83
+ console.error('Error creating sign-in request:', error);
84
+ return;
85
+ }
86
+ if (response.ok === true) {
87
+ return { _options: response.data, challengeId: response.challengeId };
88
+ } else {
89
+ adminforth.alert({message: 'Error creating sign-in request.', variant: 'warning'});
90
+ codeError.value = 'Error creating sign-in request.';
91
+ }
92
+ }
93
+
94
+ async function authenticate(options) {
95
+ try {
96
+ const abortController = new AbortController();
97
+ const credential = await navigator.credentials.get({
98
+ publicKey: options,
99
+ signal: abortController.signal,
100
+ });
101
+ return credential;
102
+ } catch (error) {
103
+ adminforth.alert({message: 'Error during authentication', variant: 'warning'});
104
+ codeError.value = 'Error during authentication.';
105
+ }
106
+ }
package/dist/index.js CHANGED
@@ -19,30 +19,48 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
19
19
  instanceUniqueRepresentation(pluginOptions) {
20
20
  return `single`;
21
21
  }
22
- verify(code, opts) {
22
+ verify(confirmationResult, opts) {
23
23
  return __awaiter(this, void 0, void 0, function* () {
24
24
  var _a, _b, _c, _d;
25
- if (!code)
26
- return { error: "Code is required" };
27
- const authRes = this.adminforth.config.resources
28
- .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
29
- if (!authRes)
30
- return { error: "Auth resource not found" };
31
- const connector = this.adminforth.connectors[authRes.dataSource];
32
- const pkName = (_a = authRes.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name;
33
- if (!pkName)
34
- return { error: "Primary key not found on auth resource" };
35
- const pk = (_b = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _b !== void 0 ? _b : (_d = (_c = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _c === void 0 ? void 0 : _c.dbUser) === null || _d === void 0 ? void 0 : _d[pkName];
36
- if (!pk)
37
- return { error: "User PK is required" };
38
- const user = yield connector.getRecordByPrimaryKey(authRes, pk);
39
- if (!user)
40
- return { error: "User not found" };
41
- const secret = user[this.options.twoFaSecretFieldName];
42
- if (!secret)
43
- return { error: "2FA is not set up for this user" };
44
- const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
45
- return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
25
+ if (!confirmationResult)
26
+ return { error: "Confirmation result is required" };
27
+ if (confirmationResult.mode === "totp") {
28
+ const code = confirmationResult.result;
29
+ const authRes = this.adminforth.config.resources
30
+ .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
31
+ if (!authRes)
32
+ return { error: "Auth resource not found" };
33
+ const connector = this.adminforth.connectors[authRes.dataSource];
34
+ const pkName = (_a = authRes.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name;
35
+ if (!pkName)
36
+ return { error: "Primary key not found on auth resource" };
37
+ const pk = (_b = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _b !== void 0 ? _b : (_d = (_c = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _c === void 0 ? void 0 : _c.dbUser) === null || _d === void 0 ? void 0 : _d[pkName];
38
+ if (!pk)
39
+ return { error: "User PK is required" };
40
+ const user = yield connector.getRecordByPrimaryKey(authRes, pk);
41
+ if (!user)
42
+ return { error: "User not found" };
43
+ const secret = user[this.options.twoFaSecretFieldName];
44
+ if (!secret)
45
+ return { error: "2FA is not set up for this user" };
46
+ const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
47
+ return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
48
+ }
49
+ else if (confirmationResult.mode === "passkey") {
50
+ const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: opts.cookies, name: `passkeyLoginTemporaryJWT` });
51
+ if (!passkeysCookies) {
52
+ return { error: 'Passkey token is required' };
53
+ }
54
+ const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
55
+ if (!decodedPasskeysCookies) {
56
+ return { error: 'Invalid passkey' };
57
+ }
58
+ const verificationResult = yield this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies);
59
+ if (verificationResult.ok && verificationResult.passkeyConfirmed) {
60
+ return { ok: true };
61
+ }
62
+ return { error: "Invalid passkey" };
63
+ }
46
64
  });
47
65
  }
48
66
  parsePeriod(period) {
@@ -326,7 +344,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
326
344
  if (!passkeysCookies) {
327
345
  return { error: 'Passkey token is required' };
328
346
  }
329
- const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
347
+ const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
330
348
  if (!decodedPasskeysCookies) {
331
349
  return { error: 'Invalid passkey' };
332
350
  }
@@ -400,9 +418,18 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
400
418
  method: 'POST',
401
419
  path: `/plugin/passkeys/registerPasskeyRequest`,
402
420
  noAuth: false,
403
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
421
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
404
422
  var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
405
423
  const mode = body === null || body === void 0 ? void 0 : body.mode;
424
+ const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
425
+ const verificationResult = yield this.verify(confirmationResult, {
426
+ adminUser: adminUser,
427
+ userPk: adminUser.pk,
428
+ cookies: cookies
429
+ });
430
+ if (!verificationResult || !('ok' in verificationResult)) {
431
+ return { ok: false, error: 'Verification failed' };
432
+ }
406
433
  const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
407
434
  const rp = {
408
435
  name: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.rp.name) || this.adminforth.config.customization.brandName,
@@ -530,7 +557,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
530
557
  rpID: (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.rp.id,
531
558
  userVerification: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.authenticatorSelection.userVerification) || "required"
532
559
  });
533
- const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempPasskeyChallenge', '10m');
560
+ const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempLoginPasskeyChallenge', '10m');
534
561
  this.adminforth.auth.setCustomCookie({ response, payload: { name: `passkeyLoginTemporaryJWT`, value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true } });
535
562
  return { ok: true, data: options };
536
563
  }
package/index.ts CHANGED
@@ -27,31 +27,50 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
27
27
  }
28
28
 
29
29
  public async verify(
30
- code: string,
31
- opts?: { adminUser?: AdminUser; userPk?: string }
30
+ confirmationResult: Record<string, any>,
31
+ opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any }
32
32
  ): Promise<{ ok: true } | { error: string }> {
33
- if (!code) return { error: "Code is required" };
33
+ if (!confirmationResult) return { error: "Confirmation result is required" };
34
34
 
35
- const authRes = this.adminforth.config.resources
36
- .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
35
+ if (confirmationResult.mode === "totp") {
36
+ const code = confirmationResult.result;
37
+ const authRes = this.adminforth.config.resources
38
+ .find(r => r.resourceId === this.adminforth.config.auth.usersResourceId);
37
39
 
38
- if (!authRes) return { error: "Auth resource not found" };
40
+ if (!authRes) return { error: "Auth resource not found" };
39
41
 
40
- const connector = this.adminforth.connectors[authRes.dataSource];
41
- const pkName = authRes.columns.find(c => c.primaryKey)?.name;
42
- if (!pkName) return { error: "Primary key not found on auth resource" };
42
+ const connector = this.adminforth.connectors[authRes.dataSource];
43
+ const pkName = authRes.columns.find(c => c.primaryKey)?.name;
44
+ if (!pkName) return { error: "Primary key not found on auth resource" };
43
45
 
44
- const pk = opts?.userPk ?? opts?.adminUser?.dbUser?.[pkName];
45
- if (!pk) return { error: "User PK is required" };
46
+ const pk = opts?.userPk ?? opts?.adminUser?.dbUser?.[pkName];
47
+ if (!pk) return { error: "User PK is required" };
46
48
 
47
- const user = await connector.getRecordByPrimaryKey(authRes, pk);
48
- if (!user) return { error: "User not found" };
49
+ const user = await connector.getRecordByPrimaryKey(authRes, pk);
50
+ if (!user) return { error: "User not found" };
49
51
 
50
- const secret = user[this.options.twoFaSecretFieldName];
51
- if (!secret) return { error: "2FA is not set up for this user" };
52
+ const secret = user[this.options.twoFaSecretFieldName];
53
+ if (!secret) return { error: "2FA is not set up for this user" };
52
54
 
53
- const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
54
- return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
55
+ const verified = twofactor.verifyToken(secret, code, this.options.timeStepWindow);
56
+ return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
57
+ } else if (confirmationResult.mode === "passkey") {
58
+ const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: opts.cookies, name: `passkeyLoginTemporaryJWT`});
59
+ if (!passkeysCookies) {
60
+ return { error: 'Passkey token is required' };
61
+ }
62
+
63
+ const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
64
+ if (!decodedPasskeysCookies) {
65
+ return { error: 'Invalid passkey' };
66
+ }
67
+ const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies );
68
+
69
+ if (verificationResult.ok && verificationResult.passkeyConfirmed) {
70
+ return { ok: true }
71
+ }
72
+ return { error: "Invalid passkey" }
73
+ }
55
74
  }
56
75
 
57
76
  public parsePeriod(period?: string): number {
@@ -365,7 +384,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
365
384
  if (!passkeysCookies) {
366
385
  return { error: 'Passkey token is required' };
367
386
  }
368
- const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
387
+ const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false);
369
388
  if (!decodedPasskeysCookies) {
370
389
  return { error: 'Invalid passkey' };
371
390
  }
@@ -440,8 +459,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
440
459
  method: 'POST',
441
460
  path: `/plugin/passkeys/registerPasskeyRequest`,
442
461
  noAuth: false,
443
- handler: async ({ body, adminUser, response }) => {
462
+ handler: async ({ body, adminUser, response, cookies }) => {
444
463
  const mode = body?.mode;
464
+
465
+ const confirmationResult = body?.confirmationResult;
466
+ const verificationResult = await this.verify(confirmationResult, {
467
+ adminUser: adminUser,
468
+ userPk: adminUser.pk,
469
+ cookies: cookies
470
+ });
471
+ if ( !verificationResult || !('ok' in verificationResult) ) {
472
+ return { ok: false, error: 'Verification failed' };
473
+ }
474
+
445
475
  const settingsOrigin = this.options.passkeys?.settings.expectedOrigin;
446
476
  const rp = {
447
477
  name: this.options.passkeys?.settings.rp.name || this.adminforth.config.customization.brandName,
@@ -574,7 +604,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
574
604
  rpID: this.options.passkeys?.settings.rp.id,
575
605
  userVerification: this.options.passkeys?.settings.authenticatorSelection.userVerification || "required"
576
606
  });
577
- const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempPasskeyChallenge', '10m');
607
+ const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempLoginPasskeyChallenge', '10m');
578
608
  this.adminforth.auth.setCustomCookie({response, payload: {name: `passkeyLoginTemporaryJWT`, value: value, expiry: undefined, expirySeconds: 10 * 60, httpOnly: true}});
579
609
  return { ok: true, data: options };
580
610
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "1.10.6",
3
+ "version": "1.11.0",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",