Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/thirty-badgers-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Catch FIDO/WebAuthn DOMExeceptions and send them to DaVinci
26 changes: 13 additions & 13 deletions e2e/davinci-app/components/fido.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -32,13 +32,13 @@ export default function fidoComponent(
console.log('fido.register response:', response);
if ('error' in response) {
console.error(response);
}

const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
await submitForm();
}
await submitForm();
}
};
} else if (collector.type === 'FidoAuthenticationCollector') {
Expand All @@ -54,13 +54,13 @@ export default function fidoComponent(
console.log('fido.authenticate response:', response);
if ('error' in response) {
console.error(response);
}

const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
await submitForm();
}
await submitForm();
}
};
}
Expand Down
221 changes: 197 additions & 24 deletions e2e/davinci-suites/src/fido.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
/*
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { test, expect, CDPSession } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';

const username = 'JSFidoUser@user.com';
const password = 'FakePassword#123';
let cdp: CDPSession | undefined;
let authenticatorId: string | undefined;

test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available
test.describe.configure({ mode: 'serial' });

test.beforeEach(async ({ context, page }) => {
cdp = await context.newCDPSession(page);
await cdp.send('WebAuthn.enable');

// A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys.
const response = await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal', // platform authenticator
hasResidentKey: true, // allow discoverable credentials (passkeys)
hasUserVerification: true, // device supports UV
isUserVerified: true, // simulate successful UV (PIN/biometric)
automaticPresenceSimulation: true, // auto "touch"/presence
},
});
authenticatorId = response.authenticatorId;
});
test.describe('FIDO/WebAuthn Success Tests', () => {
let cdp: CDPSession | undefined;
let authenticatorId: string | undefined;

test.afterEach(async () => {
await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
await cdp.send('WebAuthn.disable');
});
test.beforeEach(async ({ context, page }) => {
if (authenticatorId) {
await cdp?.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
authenticatorId = undefined;
}

cdp = await context.newCDPSession(page);
await expect(cdp).toBeDefined();
await cdp.send('WebAuthn.enable');

// A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys.
const response = await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal', // platform authenticator
hasResidentKey: true, // allow discoverable credentials (passkeys)
hasUserVerification: true, // device supports UV
isUserVerified: true, // simulate successful UV (PIN/biometric)
automaticPresenceSimulation: true, // auto "touch"/presence
},
});
authenticatorId = response.authenticatorId;
});

test.describe('FIDO/WebAuthn Tests', () => {
test.afterEach(async () => {
if (authenticatorId) {
await cdp?.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
authenticatorId = undefined;
}
await cdp?.send('WebAuthn.disable');
});
test('Register and authenticate with webauthn device', async ({ page }) => {
const { navigate } = asyncEvents(page);

Expand All @@ -48,6 +64,10 @@ test.describe('FIDO/WebAuthn Tests', () => {
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

// Register WebAuthn credential
const { credentials: initialCredentials } = await cdp.send('WebAuthn.getCredentials', {
authenticatorId,
Expand Down Expand Up @@ -103,6 +123,13 @@ test.describe('FIDO/WebAuthn Tests', () => {
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

await expect(cdp).toBeDefined;
await expect(authenticatorId).toBeDefined();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

// Register WebAuthn credential
const { credentials: initialCredentials } = await cdp.send('WebAuthn.getCredentials', {
authenticatorId,
Expand Down Expand Up @@ -142,3 +169,149 @@ test.describe('FIDO/WebAuthn Tests', () => {
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
});
});

test.describe('FIDO/WebAuthn Error Tests', () => {
let cdp: CDPSession | undefined;
let authenticatorId: string | undefined;

test.beforeEach(async ({ context, page }) => {
if (authenticatorId) {
await cdp?.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
authenticatorId = undefined;
}

cdp = await context.newCDPSession(page);
await expect(cdp).toBeDefined();
await cdp.send('WebAuthn.enable');

// Starts with UV succeeding so setup steps (e.g. registering a device before testing
// an auth failure) work; individual tests call WebAuthn.setUserVerified(false) right
// before the operation that should fail, which Chromium surfaces as NotAllowedError —
// simulating the user canceling the prompt.
const response = await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal', // platform authenticator
hasResidentKey: true, // allow discoverable credentials (passkeys)
hasUserVerification: true, // device supports UV
isUserVerified: true, // simulate successful UV (PIN/biometric) by default
automaticPresenceSimulation: true, // auto "touch"/presence
},
});
authenticatorId = response.authenticatorId;
});

test.afterEach(async () => {
if (authenticatorId) {
await cdp?.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
authenticatorId = undefined;
}
await cdp?.send('WebAuthn.disable');
});
test('Registration shows NotAllowedError when the WebAuthn prompt is canceled', async ({
page,
}) => {
const { navigate } = asyncEvents(page);

await navigate(
'/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
);
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();

await page.getByRole('button', { name: 'USER_LOGIN' }).click();
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click();
await page.getByRole('button', { name: 'Biometrics/Security Key' }).click();

// Make user verification fail for this authenticator so the registration attempt is
// rejected with NotAllowedError — simulating the user canceling the prompt.
await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: false });
await page.getByRole('button', { name: 'FIDO Register' }).click();

await expect(page.getByText('FIDO Registration Error - NotAllowedError')).toBeVisible();
await expect(page.getByRole('button', { name: 'FIDO Register' })).toBeVisible();
});

test('Device authentication shows NotAllowedError when the WebAuthn prompt is canceled', async ({
page,
}) => {
const { navigate } = asyncEvents(page);

await navigate(
'/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
);
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();

await page.getByRole('button', { name: 'USER_LOGIN' }).click();
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

// Register a credential so there is a device to authenticate with.
await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click();
await page.getByRole('button', { name: 'Biometrics/Security Key' }).click();
await page.getByRole('button', { name: 'FIDO Register' }).click();
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();

await page.getByRole('button', { name: 'DEVICE_AUTHENTICATION' }).click();
await page.getByRole('button', { name: 'Biometrics/Security Key' }).last().click();

// Make user verification fail for this authenticator so the assertion attempt is
// rejected with NotAllowedError — simulating the user canceling the prompt.
await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: false });
await page.getByRole('button', { name: 'FIDO Authenticate' }).click();

await expect(page.getByText('FIDO Authentication Error - NotAllowedError')).toBeVisible();
await expect(page.getByRole('button', { name: 'FIDO Authenticate' })).toBeVisible();
});

test('Usernameless authentication shows NotAllowedError when the WebAuthn prompt is canceled', async ({
page,
}) => {
const { navigate } = asyncEvents(page);

await navigate(
'/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
);
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();

await page.getByRole('button', { name: 'USER_LOGIN' }).click();
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

// Register a discoverable credential so there is a passkey to authenticate with.
await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click();
await page.getByRole('button', { name: 'Biometrics/Security Key' }).click();
await page.getByRole('button', { name: 'FIDO Register' }).click();
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();

await page.getByRole('button', { name: 'USER_NAMELESS' }).click();
await expect(page.getByText('FIDO2 Authentication')).toBeVisible();

// Make user verification fail for this authenticator so the assertion attempt is
// rejected with NotAllowedError — simulating the user canceling the prompt.
await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: false });
await page.getByRole('button', { name: 'FIDO Authenticate' }).click();

await expect(page.getByText('FIDO Usernameless Error - NotAllowedError')).toBeVisible();
await expect(page.getByRole('button', { name: 'FIDO Authenticate' })).toBeVisible();
});
});
19 changes: 5 additions & 14 deletions e2e/davinci-suites/src/password-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test.describe('ValidatedPasswordCollector — password policy (Example - Registr
try {
await deleteTestUser(page, email);
} catch (err) {
console.error(`[cleanup] Failed to delete test user ${email}:`, err);
throw new Error(`[cleanup] Failed to delete test user ${email}: ${JSON.stringify(err)}`);
}
}
});
Expand Down Expand Up @@ -146,26 +146,17 @@ test.describe('ValidatedPasswordCollector — password policy (Example - Registr
await page.locator('#userPassword').fill(password);

// Submit the form by calling submit() on the form element
await page.locator('form').evaluate((form: HTMLFormElement) => form.submit());
await page.getByRole('button', { name: 'Submit' }).click({ timeout: 10000 });

// Wait for the page to navigate to the next step
// The heading should change from "Example - Registration 1" to something else
await page.waitForFunction(
() => {
const heading = document.querySelector('h2');
return heading && !heading.textContent?.includes('Example - Registration');
},
{ timeout: 10000 },
);

// Verify we've moved to the next step
const heading = page.locator('h2').first();
await expect(heading).toBeVisible();
await expect(page.getByRole('heading', { name: 'Example - Registration' })).not.toBeVisible();

// If the flow shows a "Continue" button, click through to complete it
const continueBtn = page.getByRole('button', { name: 'Continue' });
if (await continueBtn.isVisible()) {
await continueBtn.click();
}

await expect(page.getByRole('heading', { name: 'Complete' })).toBeVisible();
});
});
Loading
Loading