From a221776694574a30f1869c923b7db9ec1c218391 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Tue, 7 Oct 2025 17:09:55 +0300 Subject: [PATCH 1/6] feat: enhance loading states and passkey handling in TwoFactors components. Require TOTP for passkey registation --- custom/TwoFactorsConfirmation.vue | 15 +++++-- custom/TwoFactorsPasskeysSettings.vue | 64 +++++++++++++++++++-------- index.ts | 10 +++++ 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/custom/TwoFactorsConfirmation.vue b/custom/TwoFactorsConfirmation.vue index ac071a4..2e122e4 100644 --- a/custom/TwoFactorsConfirmation.vue +++ b/custom/TwoFactorsConfirmation.vue @@ -8,7 +8,7 @@ }: {}" > -
+
@@ -44,7 +44,7 @@

Passkey

When you are ready, authenticate using the button below

-
@@ -68,6 +68,9 @@
+
+ +
@@ -79,7 +82,7 @@ import { useUserStore } from '@/stores/user'; import { callAdminForthApi, loadFile } from '@/utils'; import { showErrorTost } from '@/composables/useFrontendApi'; - import { Button, Link } from '@/afcl'; + import { Button, Link, Spinner } from '@/afcl'; import VOtpInput from "vue3-otp-input"; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router' @@ -95,6 +98,7 @@ const route = useRoute(); const router = useRouter(); const codeError = ref(null); + const isFetchingPasskey = ref(false); onBeforeMount(() => { if (localStorage.getItem('isAuthorized') === 'true') { @@ -131,6 +135,7 @@ const doesUserHavePasskeys = ref(false); const confirmationMode = ref("code"); const isPasskeysSupported = ref(false); + const isLoading = ref(true); onMounted(async () => { if (localStorage.getItem('isAuthorized') !== 'true') { @@ -145,6 +150,7 @@ const rootEl = otpRoot.value; rootEl && rootEl.addEventListener('focusout', handleFocusOut, true); } + isLoading.value = false; }); watch(route, (newRoute) => { @@ -231,10 +237,12 @@ } async function usePasskeyButton() { + isFetchingPasskey.value = true; const { _options, challengeId } = await createSignInRequest(); const options = PublicKeyCredential.parseRequestOptionsFromJSON(_options); const credential = await authenticate(options); if (!credential) { + isFetchingPasskey.value = false; return; } const result = JSON.stringify(credential); @@ -244,6 +252,7 @@ origin: window.location.origin, }; sendCode('', 'passkey', passkeyOptions); + isFetchingPasskey.value = false; } async function createSignInRequest() { diff --git a/custom/TwoFactorsPasskeysSettings.vue b/custom/TwoFactorsPasskeysSettings.vue index 77447c8..1a70023 100644 --- a/custom/TwoFactorsPasskeysSettings.vue +++ b/custom/TwoFactorsPasskeysSettings.vue @@ -22,12 +22,18 @@ header="Edit Passkey" >

Enter new passkey name:

@@ -48,11 +54,16 @@ header="Delete Passkey" >

Are you sure you want to delete this passkey?

@@ -66,7 +77,7 @@
- + +

Processing

@@ -135,7 +147,7 @@ import { callAdminForthApi } from '@/utils'; import adminforth from '@/adminforth'; import { onMounted, ref, Ref, onBeforeUnmount } from 'vue'; - import { Card, Dialog, ButtonGroup, Tooltip } from '@/afcl' + import { Card, Dialog, ButtonGroup, Tooltip, Spinner } from '@/afcl' import { IconTrashBinSolid, IconPenSolid, IconPlusOutline, IconCaretDownSolid, IconCheckOutline } from '@iconify-prerendered/vue-flowbite'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; @@ -152,6 +164,7 @@ const addPasskeyMode: Ref<'platform' | 'cross-platform'> = ref('platform'); const authenticatorAttachment = ref<'platform' | 'cross-platform' | 'both'>('platform'); const isInitialFinished = ref(false); + const isFetchingPasskey = ref(false); onMounted(async () => { await getPasskeys(); @@ -180,12 +193,22 @@ } async function addPasskey() { - const { options, challengeId } = await fetchInformationFromTheBackend(); + isFetchingPasskey.value = true; + const code = await window.adminforthTwoFaModal.getCode(); + + const { options } = await fetchInformationFromTheBackend(code); + if (!options ) { + isFetchingPasskey.value = false; + adminforth.alert({message: 'Verification failed.', variant: 'warning'}); + return; + } const creationResult = await callWebAuthn(options); if (!creationResult) { + isFetchingPasskey.value = false; return; } - finishRegisteringPasskey(creationResult, challengeId); + finishRegisteringPasskey(creationResult); + isFetchingPasskey.value = false; } async function getPasskeys() { @@ -267,20 +290,24 @@ } } - async function fetchInformationFromTheBackend() { + async function fetchInformationFromTheBackend(code) { let response; try { response = await callAdminForthApi({ path: `/plugin/passkeys/registerPasskeyRequest`, method: 'POST', body: { - mode: addPasskeyMode.value + mode: addPasskeyMode.value, + code: code }, }); } catch (error) { console.error('Error fetching passkeys info:', error); return; } + if (!response.ok) { + return {}; + } const _options = response.data; const challengeId = response.challengeId; const options = PublicKeyCredential.parseCreationOptionsFromJSON(_options); @@ -303,7 +330,7 @@ return result; } - async function finishRegisteringPasskey(credential: any, challengeId: string) { + async function finishRegisteringPasskey(credential: any) { let res try { res = await callAdminForthApi({ @@ -312,7 +339,6 @@ body: { credential: credential, origin: window.location.origin, - challengeId: challengeId, }, }); } catch (error) { diff --git a/index.ts b/index.ts index af66e18..93e9328 100644 --- a/index.ts +++ b/index.ts @@ -442,6 +442,16 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { noAuth: false, handler: async ({ body, adminUser, response }) => { const mode = body?.mode; + + const code = body?.code; + this.connectors = this.adminforth.connectors + const connector = this.connectors[this.authResource.dataSource]; + const reqUser = await connector.getRecordByPrimaryKey(this.authResource, adminUser.pk) + const verified = twofactor.verifyToken(reqUser[this.options.twoFaSecretFieldName], code, this.options.timeStepWindow) + if ( !verified ) { + return { ok: false, error: 'Wrong or expired OTP code' }; + } + const settingsOrigin = this.options.passkeys?.settings.expectedOrigin; const rp = { name: this.options.passkeys?.settings.rp.name || this.adminforth.config.customization.brandName, From e6577bffd9644d73fbb6dc71fe9f6738cd7ab22d Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Wed, 8 Oct 2025 17:46:49 +0300 Subject: [PATCH 2/6] BREAKING CHANGE: rename getCode() call to the get2FaConfirmationResult() and add support of passkeys in 2FA modal --- custom/TwoFAModal.vue | 132 ++++++++++++++++++++++---- custom/TwoFactorsPasskeysSettings.vue | 69 ++++++++++++-- custom/utils.js | 55 ++++++++++- index.ts | 74 ++++++++++----- 4 files changed, 276 insertions(+), 54 deletions(-) diff --git a/custom/TwoFAModal.vue b/custom/TwoFAModal.vue index ef25270..d119cb3 100644 --- a/custom/TwoFAModal.vue +++ b/custom/TwoFAModal.vue @@ -1,14 +1,15 @@