diff --git a/src/frontend/src/components/modals/ReauthenticateModal.tsx b/src/frontend/src/components/modals/ReauthenticateModal.tsx index 3a53a97633..21cb749279 100644 --- a/src/frontend/src/components/modals/ReauthenticateModal.tsx +++ b/src/frontend/src/components/modals/ReauthenticateModal.tsx @@ -25,10 +25,22 @@ import { useServerApiState } from '../../states/ServerApiState'; export function getReauthFlow(err: any): FlowEnum | null { const flows: any = err.response?.data?.data?.flows ?? []; - if (flows.find((flow: any) => flow.id == FlowEnum.MfaReauthenticate)) { + const mfaFlow = flows.find( + (flow: any) => flow.id == FlowEnum.MfaReauthenticate + ); + const mfaSupported = mfaFlow?.types.includes('totp'); + const passwordFlow = flows.find( + (flow: any) => flow.id == FlowEnum.Reauthenticate + ); + + if (mfaSupported) { return FlowEnum.MfaReauthenticate; - } else if (flows.find((flow: any) => flow.id == FlowEnum.Reauthenticate)) { + // this can happen if you only have webauthn and mail but not totp/recovery + } else if (passwordFlow) { return FlowEnum.Reauthenticate; + // fallback for other flows + } else if (mfaFlow) { + return FlowEnum.MfaReauthenticate; } else { // No match found return null; diff --git a/src/frontend/tests/helpers.ts b/src/frontend/tests/helpers.ts index 829d7e51ef..908164859f 100644 --- a/src/frontend/tests/helpers.ts +++ b/src/frontend/tests/helpers.ts @@ -291,3 +291,27 @@ export const waitForDataOutput = async ( page.off('popup', closePopup); } }; + +/** + * Handle a re-authentication (password) prompt, if one is displayed. + */ +// Handle a re-authentication prompt (if displayed) +export const reauthenticate = async ( + page: Page, + password: string, + time = 3000 +) => { + const dialog = page.getByRole('dialog', { name: 'Reauthenticate' }); + const visible = await dialog + .waitFor({ timeout: time }) + .then(() => true) + .catch(() => false); + + if (visible) { + await dialog.locator('input[type="password"]').fill(password); + await dialog.getByRole('button', { name: 'Submit', exact: true }).click(); + await dialog.waitFor({ state: 'hidden' }); + } + + return visible; +}; diff --git a/src/frontend/tests/pui_login.spec.ts b/src/frontend/tests/pui_login.spec.ts index 62a16509fd..8990a38d5b 100644 --- a/src/frontend/tests/pui_login.spec.ts +++ b/src/frontend/tests/pui_login.spec.ts @@ -2,7 +2,7 @@ import type { Locator, Page } from '@playwright/test'; import { TOTP } from 'otpauth'; import { expect, test } from './baseFixtures.js'; import { engineeruser, logoutUrl, mfauser, noaccessuser } from './defaults.js'; -import { navigate, openDetailAction } from './helpers.js'; +import { navigate, openDetailAction, reauthenticate } from './helpers.js'; import { doLogin } from './login.js'; const stripQueryAndHash = (url: string): string => { @@ -487,10 +487,21 @@ test('Login - MFA - TOTP', async ({ page }) => { await page.getByRole('textbox', { name: 'text-input-otp-code' }).fill(token); await page.getByRole('button', { name: 'Submit', exact: true }).click(); + + // re-authentication; needs a new login + const submitButton = page.getByRole('button', { + name: 'Submit', + exact: true + }); + if (await reauthenticate(page, noaccessuser.testcred ?? '', 5000)) { + await submitButton.click(); + } + await page.getByText('TOTP token registered successfully').waitFor(); // View recovery codes await page.getByRole('button', { name: 'view-recovery-codes' }).click(); + await reauthenticate(page, noaccessuser.testcred ?? ''); await page .getByText('The following one time recovery codes are available') .waitFor(); @@ -499,6 +510,7 @@ test('Login - MFA - TOTP', async ({ page }) => { // Remove TOTP token await page.getByRole('button', { name: 'remove-totp' }).click(); await page.getByRole('button', { name: 'Remove', exact: true }).click(); + await reauthenticate(page, noaccessuser.testcred ?? ''); await page.getByText('TOTP token removed successfully').waitFor(); diff --git a/src/frontend/tests/pui_webauthn.spec.ts b/src/frontend/tests/pui_webauthn.spec.ts index 3e85ebb735..97d6c5c6c5 100644 --- a/src/frontend/tests/pui_webauthn.spec.ts +++ b/src/frontend/tests/pui_webauthn.spec.ts @@ -9,7 +9,7 @@ import type { Page } from '@playwright/test'; import { createApi } from './api.js'; import { expect, generateUUID, test } from './baseFixtures.js'; import { logoutUrl } from './defaults.js'; -import { navigate } from './helpers.js'; +import { navigate, reauthenticate } from './helpers.js'; import { doLogin } from './login.js'; // Attach a virtual authenticator to the provided page @@ -51,20 +51,6 @@ const createUser = async (username: string, password: string) => { return user.pk; }; -// Handle a re-authentication prompt (if displayed) -const reauthenticate = async (page: Page, password: string) => { - const field = page.getByLabel('text-field-password'); - const visible = await field - .waitFor({ timeout: 3000 }) - .then(() => true) - .catch(() => false); - - if (visible) { - await field.fill(password); - await page.getByRole('button', { name: 'Submit' }).click(); - } -}; - test('Login - MFA - WebAuthn', async ({ page, browserName }) => { // The virtual authenticator requires the Chrome DevTools Protocol test.skip( @@ -97,7 +83,9 @@ test('Login - MFA - WebAuthn', async ({ page, browserName }) => { .getByRole('button', { name: 'Multi-Factor Authentication' }) .click(); await page.getByRole('button', { name: 'add-factor-webauthn' }).click(); - await reauthenticate(page, password); + if (await reauthenticate(page, password)) { + await page.getByRole('button', { name: 'add-factor-webauthn' }).click(); + } await page .getByText('WebAuthn credential registered successfully') .waitFor(); @@ -125,7 +113,9 @@ test('Login - MFA - WebAuthn', async ({ page, browserName }) => { await page.getByRole('button', { name: 'remove-webauthn' }).click(); await page.getByText('Remove WebAuthn Credential').waitFor(); await page.getByRole('button', { name: 'Remove', exact: true }).click(); - await reauthenticate(page, password); + if (await reauthenticate(page, password)) { + await page.getByRole('button', { name: 'Remove', exact: true }).click(); + } await page.getByText('WebAuthn credential removed successfully').waitFor(); } finally { const api = await createApi({});