Skip to content

Commit faa2963

Browse files
committed
test(davinci-client): virtual authenticator e2e tests
1 parent b6d3630 commit faa2963

3 files changed

Lines changed: 167 additions & 4 deletions

File tree

e2e/davinci-app/components/fido.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import type {
8+
FidoRegistrationCollector,
9+
FidoAuthenticationCollector,
10+
Updater,
11+
FidoClient,
12+
} from '@forgerock/davinci-client/types';
13+
14+
export default function fidoComponent(
15+
formEl: HTMLFormElement,
16+
collector: FidoRegistrationCollector | FidoAuthenticationCollector,
17+
updater: Updater<FidoRegistrationCollector | FidoAuthenticationCollector>,
18+
fidoApi: FidoClient,
19+
submitForm: () => Promise<void>,
20+
) {
21+
if (collector.type === 'FidoRegistrationCollector') {
22+
const button = document.createElement('button');
23+
button.type = 'button';
24+
button.value = collector.output.key;
25+
button.innerHTML = 'FIDO Register';
26+
formEl.appendChild(button);
27+
28+
button.onclick = async () => {
29+
const credentialOptions = collector.output.config.publicKeyCredentialCreationOptions;
30+
const response = await fidoApi.register(credentialOptions);
31+
console.log('fido.register response:', response);
32+
if ('error' in response) {
33+
console.error(response);
34+
} else {
35+
const error = updater(response);
36+
if (error && 'error' in error) {
37+
console.error(error.error.message);
38+
} else {
39+
await submitForm();
40+
}
41+
}
42+
};
43+
} else if (collector.type === 'FidoAuthenticationCollector') {
44+
const button = document.createElement('button');
45+
button.type = 'button';
46+
button.value = collector.output.key;
47+
button.innerHTML = 'FIDO Authenticate';
48+
formEl.appendChild(button);
49+
50+
button.onclick = async () => {
51+
const credentialOptions = collector.output.config.publicKeyCredentialRequestOptions;
52+
const response = await fidoApi.authenticate(credentialOptions);
53+
console.log('fido.authenticate response:', response);
54+
if ('error' in response) {
55+
console.error(response);
56+
} else {
57+
const error = updater(response);
58+
if (error && 'error' in error) {
59+
console.error(error.error.message);
60+
} else {
61+
await submitForm();
62+
}
63+
}
64+
};
65+
}
66+
}

e2e/davinci-app/main.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import './style.css';
88

99
import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk';
10-
import { davinci } from '@forgerock/davinci-client';
10+
import { davinci, fido } from '@forgerock/davinci-client';
1111
import type {
1212
CustomLogger,
1313
DaVinciConfig,
@@ -31,6 +31,7 @@ import singleValueComponent from './components/single-value.js';
3131
import multiValueComponent from './components/multi-value.js';
3232
import labelComponent from './components/label.js';
3333
import objectValueComponent from './components/object-value.js';
34+
import fidoComponent from './components/fido.js';
3435

3536
const loggerFn = {
3637
error: () => {
@@ -81,13 +82,14 @@ const urlParams = new URLSearchParams(window.location.search);
8182

8283
(async () => {
8384
const davinciClient: DavinciClient = await davinci({ config, logger, requestMiddleware });
84-
const protectAPI = protect({ envId: '02fb4743-189a-4bc7-9d6c-a919edfe6447' });
85+
const protectApi = protect({ envId: '02fb4743-189a-4bc7-9d6c-a919edfe6447' });
86+
const fidoApi = fido();
8587
const continueToken = urlParams.get('continueToken');
8688
const formEl = document.getElementById('form') as HTMLFormElement;
8789
let resumed: InternalErrorResponse | NodeStates | undefined;
8890

8991
// Initialize Protect
90-
const error = await protectAPI.start();
92+
const error = await protectApi.start();
9193
if (error?.error) {
9294
console.error('Error starting Protect:', error.error);
9395
}
@@ -251,6 +253,17 @@ const urlParams = new URLSearchParams(window.location.search);
251253
);
252254
} else if (collector.type === 'IdpCollector') {
253255
socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp());
256+
} else if (
257+
collector.type === 'FidoRegistrationCollector' ||
258+
collector.type === 'FidoAuthenticationCollector'
259+
) {
260+
fidoComponent(
261+
formEl, // You can ignore this; it's just for rendering
262+
collector, // This is the plain object of the collector
263+
davinciClient.update(collector), // Returns an update function for this collector
264+
fidoApi, // FIDO module for interacting with WebAuthn API
265+
submitForm,
266+
);
254267
} else if (collector.type === 'FlowCollector') {
255268
flowLinkComponent(
256269
formEl, // You can ignore this; it's just for rendering
@@ -278,7 +291,7 @@ const urlParams = new URLSearchParams(window.location.search);
278291
}
279292

280293
async function updateProtectCollector(protectCollector: ProtectCollector) {
281-
const data = await protectAPI.getData();
294+
const data = await protectApi.getData();
282295
if (typeof data !== 'string' && 'error' in data) {
283296
console.error(`Failed to retrieve data from PingOne Protect: ${data.error}`);
284297
return;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { test, expect, CDPSession } from '@playwright/test';
2+
import { asyncEvents } from './utils/async-events.js';
3+
import { password, username } from './utils/demo-user.js';
4+
5+
test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available
6+
7+
let cdp: CDPSession | undefined;
8+
let authenticatorId: string | undefined;
9+
10+
test.beforeEach(async ({ context, page }) => {
11+
cdp = await context.newCDPSession(page);
12+
await cdp.send('WebAuthn.enable');
13+
14+
// A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys.
15+
const response = await cdp.send('WebAuthn.addVirtualAuthenticator', {
16+
options: {
17+
protocol: 'ctap2',
18+
transport: 'internal', // platform authenticator
19+
hasResidentKey: true, // allow discoverable credentials (passkeys)
20+
hasUserVerification: true, // device supports UV
21+
isUserVerified: true, // simulate successful UV (PIN/biometric)
22+
automaticPresenceSimulation: true, // auto "touch"/presence
23+
},
24+
});
25+
authenticatorId = response.authenticatorId;
26+
});
27+
28+
test.afterEach(async () => {
29+
await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
30+
await cdp.send('WebAuthn.disable');
31+
});
32+
33+
test('Register and authenticate with webauthn device', async ({ page }) => {
34+
const { navigate } = asyncEvents(page);
35+
36+
await navigate('https://aj-test.pi.scrd.run:5829/?acr_values=ccff5c09002042bd86104da45cd7102e');
37+
await expect(page).toHaveURL(
38+
'https://aj-test.pi.scrd.run:5829/?acr_values=ccff5c09002042bd86104da45cd7102e',
39+
);
40+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
41+
42+
await page.getByRole('button', { name: 'USER_LOGIN' }).click();
43+
await page.getByLabel('Username').fill(username);
44+
await page.getByLabel('Password').fill(password);
45+
await page.getByRole('button', { name: 'Sign On' }).click();
46+
47+
// Register WebAuthn credential
48+
const { credentials: intialCredentials } = await cdp.send('WebAuthn.getCredentials', {
49+
authenticatorId,
50+
});
51+
await expect(intialCredentials).toHaveLength(0);
52+
53+
await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click();
54+
await page.getByRole('button', { name: 'Biometrics/Security Key' }).click();
55+
await page.getByRole('button', { name: 'FIDO Register' }).click();
56+
57+
const { credentials: recordedCredentials } = await cdp.send('WebAuthn.getCredentials', {
58+
authenticatorId,
59+
});
60+
await expect(recordedCredentials).toHaveLength(1);
61+
62+
await page.getByRole('button', { name: 'Continue' }).click();
63+
64+
// Verify we're back at home page if successful
65+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
66+
67+
// Authenticate with the registered WebAuthn credential
68+
const initialSignCount = recordedCredentials[0].signCount;
69+
70+
await page.getByRole('button', { name: 'DEVICE_AUTHENTICATION' }).click();
71+
await page.getByRole('button', { name: 'Biometrics/Security Key' }).last().click();
72+
await page.getByRole('button', { name: 'FIDO Authenticate' }).click();
73+
74+
const credentialsAfterAuth = await cdp.send('WebAuthn.getCredentials', {
75+
authenticatorId,
76+
});
77+
await expect(credentialsAfterAuth.credentials).toHaveLength(1);
78+
79+
// Signature counter should have incremented after successful authentication/assertion
80+
await expect(credentialsAfterAuth.credentials[0].signCount).toBeGreaterThan(initialSignCount);
81+
82+
// Verify we're back at home page if successful
83+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
84+
});

0 commit comments

Comments
 (0)