Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
fad1636
fix: allow to resolve multiple 2fa calls for the verifyAuto calls
yaroslav8765 Apr 28, 2026
1a2c3e1
fix: improve verifyAuto multiple calls handling and security
yaroslav8765 Apr 28, 2026
298dcdd
fix: fix security problems of the verifyAuto call
yaroslav8765 Apr 29, 2026
bc6e07d
feat: enhance TwoFAModal and TwoFactorsPasskeysSettings for improved …
SerVitasik Apr 29, 2026
a9545a3
Merge branch 'main' of https://github.com/devforth/adminforth-two-fac…
SerVitasik Apr 29, 2026
30f5fc0
fix: update error messages and translations in 2FA components
kulikp1 Apr 29, 2026
0654780
fix: update translation logic for multiple actions in 2FA modal
kulikp1 Apr 30, 2026
a4e5d31
fix: update adminforth verion
yaroslav8765 Apr 30, 2026
8b78beb
Merge pull request #24 from devforth/feature/AdminForth/1539/check-tr…
SerVitasik May 1, 2026
80748d4
fix: update adminforth verion
SerVitasik May 1, 2026
dcf0bea
fix: improve error messaging in TwoFAModal for better user feedback
kulikp1 May 5, 2026
742d560
Merge branch 'main' into feature/AdminForth/1540/show-proper-error-me…
kulikp1 May 5, 2026
500becc
Merge pull request #25 from devforth/feature/AdminForth/1540/show-pro…
SerVitasik May 5, 2026
3efb1e7
feat: enhance TwoFactorsConfirmation modal with improved layout and u…
kulikp1 May 6, 2026
2ef7b76
Merge pull request #26 from devforth/feature/AdminForth/1565/improve-…
SerVitasik May 7, 2026
d66a81e
fix: change toast type to error for passkey authentication failures
kulikp1 May 7, 2026
3a70ea0
fix: streamline error handling for passkey authentication by removing…
kulikp1 May 7, 2026
ddc7710
Merge branch 'main' into feature/AdminForth/1566/change-toast-type-to…
kulikp1 May 7, 2026
89b51cb
fix: enhance waitForResponse method with timeout handling and error r…
NoOne7135 May 7, 2026
fe59bc3
fix: dont show success toast when user resolves 2fa modal successfully
yaroslav8765 May 8, 2026
4590cf8
Merge branch 'main' of https://github.com/devforth/adminforth-two-fac…
yaroslav8765 May 8, 2026
88fd9f2
fix: update error handling for authentication to provide more specifi…
kulikp1 May 8, 2026
d3e01d5
fix: enhance error handling for authentication by adding a generic er…
kulikp1 May 8, 2026
234c580
fix: enhance error handling for authentication by adding a generic er…
kulikp1 May 8, 2026
a8a1fb4
return comment
kulikp1 May 8, 2026
e15cc21
fix: remove redundant error handling for NotAllowedError in OTP input…
kulikp1 May 8, 2026
362db37
Merge pull request #27 from devforth/feature/AdminForth/1566/change-t…
SerVitasik May 8, 2026
2024252
fix: update error response for failed verification to include status …
kulikp1 May 15, 2026
91f945f
fix: update response status handling for verification failure to use …
kulikp1 May 15, 2026
84e093c
fix: simplify error response for verification failure by removing red…
kulikp1 May 15, 2026
43a505c
fix: enhance error handling for passkey creation with specific messag…
kulikp1 May 15, 2026
651adef
fix: add new case for error
kulikp1 May 15, 2026
520f7fd
fix: update error message for verification failure to specify wrong o…
kulikp1 May 15, 2026
cdc90b0
fix: change alert variant to 'danger' for timeout or not allowed oper…
kulikp1 May 15, 2026
e411aaa
fix: update adminforth dependency version to ^2.66.0 in package.json …
kulikp1 May 22, 2026
cc25ab8
Merge pull request #30 from devforth/feature/AdminForth/1611/display-…
kulikp1 May 22, 2026
ceee634
fix: improve error handling and validation for 2FA confirmation process
NoOne7135 May 25, 2026
3d2c6cb
fix: update response status message for wrong or expired TOTP code
NoOne7135 May 25, 2026
77987c3
fix: increase af version
kulikp1 Jun 5, 2026
3ee05c9
fix: correct parsing of passkey meta, based if meta is a string or JSON
yaroslav8765 Jun 9, 2026
6d12b52
fix: test tracklify integration https://web.tracklify.com/project/2b7…
yaroslav8765 Jun 10, 2026
22acfd0
fix: enable declaration file generation in tsconfig
yaroslav8765 Jun 11, 2026
b2be9bb
chore: maintain legacy compatibility for passkey meta handling as JSO…
NoOne7135 Jun 11, 2026
2e5b28c
chore: remove debug logging for credential and passkeys fetching
NoOne7135 Jun 11, 2026
ce5cced
feat!: implement passkey and TOTP services with repositories for user…
NoOne7135 Jun 12, 2026
5e6dc2f
refactor: rename sign-in and registration endpoints for consistency a…
NoOne7135 Jun 15, 2026
52cf9bb
refactor: standardize error logging by removing translation function …
NoOne7135 Jun 15, 2026
00ba35d
refactor: enhance type definitions for cookies and MFA confirmation r…
NoOne7135 Jun 15, 2026
7570b67
refactor: simplify passkey login checks and update counter handling
NoOne7135 Jun 15, 2026
3717ac3
refactor: improve error handling during authentication and streamline…
NoOne7135 Jun 15, 2026
ecbf5d9
refactor: correct typos in comments and enhance user existence check …
NoOne7135 Jun 15, 2026
1737321
fix: correct typo in error message for passkey retrieval
NoOne7135 Jun 15, 2026
69c0ded
Merge pull request #31 from devforth/refactor
NoOne7135 Jun 15, 2026
38e56f5
fix: update adminforth verion
yaroslav8765 Jun 15, 2026
6aee661
fix: enhance websocket topic structure for user 2FA sessions
NoOne7135 Jun 16, 2026
b3e1492
Merge branch 'main' of github.com:devforth/adminforth-two-factors-auth
NoOne7135 Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions custom/TwoFAModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<div v-if="modalMode === 'totp'" class="af-two-factor-modal-totp flex flex-col items-center relative bg-white dark:bg-gray-700 rounded-lg shadow p-6 w-full max-w-md">
<div id="mfaCode-label" class="mb-4 text-gray-700 dark:text-gray-100 text-center">
<p> {{ customDialogTitle }} </p>
<p class="text-red-500 font-semibold text-xl" v-if="sessionsIdsToResolve.length > 1"> You are confirming {{ sessionsIdsToResolve.length }} actions</p>
<p>{{ $t('Please enter your authenticator code') }}</p>
</div>

Expand Down Expand Up @@ -51,6 +52,7 @@
<p class="text-4xl font-semibold mb-4 text:gray-900 dark:text-gray-200 ">{{$t('Passkey')}}</p>
<div class="mb-2 max-w-[300px] text:gray-900 dark:text-gray-200">
<p class="mb-2">{{customDialogTitle}} </p>
<p class="text-red-500 font-semibold text-xl text-center mb-12" v-if="sessionsIdsToResolve.length > 1"> You are confirming {{ sessionsIdsToResolve.length }} actions</p>
<p>{{$t('Authenticate yourself using the button below')}}</p>
</div>
<Button @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="w-full mx-16">
Expand Down Expand Up @@ -100,23 +102,47 @@

const { alert } = useAdminforth();

let currentSessionId: string | null = null;
const isAwaiting2FAResult = ref(false);
let allowAddNewSessions = true;
const ALLOW_NEW_SESSIONS_PERIOD = 1000;
const sessionsIdsToResolve = ref<string[]>([]);

watch(isAwaiting2FAResult, (awaiting) => {
if (awaiting) {
allowAddNewSessions = true;
setTimeout(() => {
if (isAwaiting2FAResult.value) {
allowAddNewSessions = false;
}
}, ALLOW_NEW_SESSIONS_PERIOD);
}
});

watch( props, () => {
if (props.adminUser) {
websocket.unsubscribeByPrefix(`/user2fa/`);
websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
currentSessionId = data.sessionId;
if (!allowAddNewSessions) {
alert({message: 'Some process or user tries to add new actions to confirm. Action was blocked', variant: 'warning'});
return;
}
sessionsIdsToResolve.value.push(data.sessionId);
let confirmationResult;
if (isAwaiting2FAResult.value) {
return;
}
try {
isAwaiting2FAResult.value = true;
confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
} catch (error) {
console.error('Error during 2FA confirmation:', error);
}
isAwaiting2FAResult.value = false;
try {
const response = await callAdminForthApi({
method: "POST",
path: "/plugin/passkeys/resolveVerifyAuto",
body: { confirmationResult, sessionId: data.sessionId }
body: { confirmationResult, sessionsIds: sessionsIdsToResolve.value }
});
if (!response.ok && response.error === 'No session ID or confirmation result'){
alert({message: 'Verification session finished or cancelled.', variant: 'warning'});
Expand All @@ -125,15 +151,16 @@
} else if (response.ok) {
alert({message: 'Verification successful', variant: 'success'});
}
sessionsIdsToResolve.value = [];
} catch (error) {
console.error('Error resolving automatic 2FA verification:', error);
}
currentSessionId = null;
allowAddNewSessions = true;
});
websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
if (currentSessionId === data.sessionId && rejectFn && modelShow.value) {
if (sessionsIdsToResolve.value.includes(data.sessionId) && rejectFn && modelShow.value) {
onCancel();
currentSessionId = null;
sessionsIdsToResolve.value = sessionsIdsToResolve.value.filter(id => id !== data.sessionId);
}
});
}
Expand Down
42 changes: 32 additions & 10 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {

public async verifyAuto(adminUser: AdminUser) {
const sessionId = crypto.randomUUID();
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId });
const result = await this.waitForResponse(sessionId);
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId });
const jwt = this.adminforth.auth.issueJWT({sessionId, adminUserPk: adminUser.pk}, 'auto2FA', '5m');
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId: jwt });
const result = await this.waitForResponse(jwt);
this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId: jwt });
return result;
}

Expand Down Expand Up @@ -1080,12 +1081,31 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
path: `/plugin/passkeys/resolveVerifyAuto`,
noAuth: false,
handler: async ({ body, adminUser, response, cookies, headers }) => {
const sessionId = body?.sessionId;
const sessionsIds = body?.sessionsIds;
const confirmationResult = body?.confirmationResult;
if (!sessionId || !confirmationResult) {
this.resolveResponse(sessionId, { ok: false, error: 'No session ID or confirmation result' });
return { ok: false, error: 'No session ID or confirmation result' };
const idsToResolve = sessionsIds;

const resolveAllIdsAsFailed = (message) => {
for (const id of idsToResolve) {
this.resolveResponse(id, { ok: false, error: message });
}
return { ok: false, error: message };
}

if (!(sessionsIds) || !confirmationResult) {
resolveAllIdsAsFailed('Confirmation window was closed or did not return required data');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yaroslav8765 after this code goes to bottom and creates a some not super clear because code still goes to resolveResponse? Second attempt might be no op but still very dangerous if soemthing else will pass it.

Please for all resolves as failed return from function at all

return resolveAllIdsAsFailed('Confirmation window was closed or did no

all resolveAllIdsAsFailed should stop execution of function

}

for (const id of idsToResolve) {
const validationResult = await this.adminforth.auth.verify(id, 'auto2FA', false);
if (!validationResult) {
resolveAllIdsAsFailed('Invalid session ID or confirmation result');
}
if (validationResult.adminUserPk !== adminUser.pk) {
resolveAllIdsAsFailed('Session does not belong to the authenticated user');
}
}

const verificationResult = await this.verify(confirmationResult, {
adminUser: adminUser,
userPk: adminUser.pk,
Expand All @@ -1096,10 +1116,12 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
} as HttpExtra
});
if ( !verificationResult || !('ok' in verificationResult) ) {
this.resolveResponse(sessionId, { ok: false, error: 'Verification failed' });
return { ok: false, error: 'Verification failed' };
resolveAllIdsAsFailed('Verification failed');
}

for (const id of idsToResolve) {
this.resolveResponse(id, { ok: true, passkeyConfirmed: verificationResult });
}
this.resolveResponse(sessionId, { ok: true, passkeyConfirmed: verificationResult });
return { ok: true };
}
});
Expand Down