Skip to content

Commit dc6b18f

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

4 files changed

Lines changed: 228 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;

e2e/davinci-suites/playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const config: PlaywrightTestConfig = {
4141
cwd: workspaceRoot,
4242
},
4343
].filter(Boolean),
44+
testIgnore: '**/fido.test.ts',
4445
};
4546

4647
export default config;
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { test, expect, CDPSession } from '@playwright/test';
2+
import { asyncEvents } from './utils/async-events.js';
3+
4+
const username = 'JSFidoUser@user.com';
5+
const password = 'FakePassword#123';
6+
let cdp: CDPSession | undefined;
7+
let authenticatorId: string | undefined;
8+
9+
test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available
10+
11+
test.beforeEach(async ({ context, page }) => {
12+
cdp = await context.newCDPSession(page);
13+
await cdp.send('WebAuthn.enable');
14+
15+
// A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys.
16+
const response = await cdp.send('WebAuthn.addVirtualAuthenticator', {
17+
options: {
18+
protocol: 'ctap2',
19+
transport: 'internal', // platform authenticator
20+
hasResidentKey: true, // allow discoverable credentials (passkeys)
21+
hasUserVerification: true, // device supports UV
22+
isUserVerified: true, // simulate successful UV (PIN/biometric)
23+
automaticPresenceSimulation: true, // auto "touch"/presence
24+
},
25+
});
26+
authenticatorId = response.authenticatorId;
27+
});
28+
29+
test.afterEach(async () => {
30+
await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
31+
await cdp.send('WebAuthn.disable');
32+
});
33+
34+
test.describe('FIDO/WebAuthn Tests', () => {
35+
test('Register and authenticate with webauthn device', async ({ page }) => {
36+
const { navigate } = asyncEvents(page);
37+
38+
await navigate(
39+
'/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
40+
);
41+
await expect(page).toHaveURL(
42+
'http://localhost:5829/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
43+
);
44+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
45+
46+
await page.getByRole('button', { name: 'USER_LOGIN' }).click();
47+
await page.getByLabel('Username').fill(username);
48+
await page.getByLabel('Password').fill(password);
49+
await page.getByRole('button', { name: 'Sign On' }).click();
50+
51+
// Register WebAuthn credential
52+
const { credentials: intialCredentials } = await cdp.send('WebAuthn.getCredentials', {
53+
authenticatorId,
54+
});
55+
await expect(intialCredentials).toHaveLength(0);
56+
57+
await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click();
58+
await page.getByRole('button', { name: 'Biometrics/Security Key' }).click();
59+
await page.getByRole('button', { name: 'FIDO Register' }).click();
60+
61+
const { credentials: recordedCredentials } = await cdp.send('WebAuthn.getCredentials', {
62+
authenticatorId,
63+
});
64+
await expect(recordedCredentials).toHaveLength(1);
65+
66+
await page.getByRole('button', { name: 'Continue' }).click();
67+
68+
// Verify we're back at home page if successful
69+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
70+
71+
// Authenticate with the registered WebAuthn credential
72+
const initialSignCount = recordedCredentials[0].signCount;
73+
74+
await page.getByRole('button', { name: 'DEVICE_AUTHENTICATION' }).click();
75+
await page.getByRole('button', { name: 'Biometrics/Security Key' }).last().click();
76+
await page.getByRole('button', { name: 'FIDO Authenticate' }).click();
77+
78+
const credentialsAfterAuth = await cdp.send('WebAuthn.getCredentials', {
79+
authenticatorId,
80+
});
81+
await expect(credentialsAfterAuth.credentials).toHaveLength(1);
82+
83+
// Signature counter should have incremented after successful authentication/assertion
84+
await expect(credentialsAfterAuth.credentials[0].signCount).toBeGreaterThan(initialSignCount);
85+
86+
// Verify we're back at home page if successful
87+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
88+
});
89+
90+
test('Register and authenticate with usernameless', async ({ page }) => {
91+
const { navigate } = asyncEvents(page);
92+
93+
await navigate(
94+
'/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
95+
);
96+
await expect(page).toHaveURL(
97+
'http://localhost:5829/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c',
98+
);
99+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
100+
101+
await page.getByRole('button', { name: 'USER_LOGIN' }).click();
102+
await page.getByLabel('Username').fill(username);
103+
await page.getByLabel('Password').fill(password);
104+
await page.getByRole('button', { name: 'Sign On' }).click();
105+
106+
// Register WebAuthn credential
107+
const { credentials: intialCredentials } = await cdp.send('WebAuthn.getCredentials', {
108+
authenticatorId,
109+
});
110+
await expect(intialCredentials).toHaveLength(0);
111+
112+
await page.getByRole('button', { name: 'DEVICE_REGISTRATION' }).click();
113+
await page.getByRole('button', { name: 'Biometrics/Security Key' }).click();
114+
await page.getByRole('button', { name: 'FIDO Register' }).click();
115+
116+
const { credentials: recordedCredentials } = await cdp.send('WebAuthn.getCredentials', {
117+
authenticatorId,
118+
});
119+
await expect(recordedCredentials).toHaveLength(1);
120+
121+
await page.getByRole('button', { name: 'Continue' }).click();
122+
123+
// Verify we're back at home page if successful
124+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
125+
126+
// Authenticate with the registered WebAuthn credential
127+
const initialSignCount = recordedCredentials[0].signCount;
128+
129+
await page.getByRole('button', { name: 'USER_NAMELESS' }).click();
130+
await expect(page.getByText('FIDO2 Authentication')).toBeVisible();
131+
await page.getByRole('button', { name: 'FIDO Authenticate' }).click();
132+
133+
const credentialsAfterAuth = await cdp.send('WebAuthn.getCredentials', {
134+
authenticatorId,
135+
});
136+
await expect(credentialsAfterAuth.credentials).toHaveLength(1);
137+
138+
// Signature counter should have incremented after successful authentication/assertion
139+
await expect(credentialsAfterAuth.credentials[0].signCount).toBeGreaterThan(initialSignCount);
140+
141+
// Verify we're back at home page if successful
142+
await expect(page.getByText('FIDO2 Test Form')).toBeVisible();
143+
});
144+
});

0 commit comments

Comments
 (0)