-
Notifications
You must be signed in to change notification settings - Fork 3
test(davinci-client): virtual authenticator e2e tests #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@forgerock/davinci-client': patch | ||
| --- | ||
|
|
||
| Improve FIDO module error handling when no options |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /* | ||
| * Copyright (c) 2025 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 { fido } from '@forgerock/davinci-client'; | ||
| import type { | ||
| FidoRegistrationCollector, | ||
| FidoAuthenticationCollector, | ||
| Updater, | ||
| } from '@forgerock/davinci-client/types'; | ||
|
|
||
| export default function fidoComponent( | ||
| formEl: HTMLFormElement, | ||
| collector: FidoRegistrationCollector | FidoAuthenticationCollector, | ||
| updater: Updater<FidoRegistrationCollector | FidoAuthenticationCollector>, | ||
| submitForm: () => Promise<void>, | ||
| ) { | ||
| const fidoApi = fido(); | ||
| if (collector.type === 'FidoRegistrationCollector') { | ||
| const button = document.createElement('button'); | ||
| button.type = 'button'; | ||
| button.value = collector.output.key; | ||
| button.innerHTML = 'FIDO Register'; | ||
| formEl.appendChild(button); | ||
|
|
||
| button.onclick = async () => { | ||
| const credentialOptions = collector.output.config.publicKeyCredentialCreationOptions; | ||
| const response = await fidoApi.register(credentialOptions); | ||
| console.log('fido.register response:', response); | ||
| if ('error' in response) { | ||
| console.error(response); | ||
| } else { | ||
| const error = updater(response); | ||
| if (error && 'error' in error) { | ||
| console.error(error.error.message); | ||
| } else { | ||
| await submitForm(); | ||
| } | ||
| } | ||
| }; | ||
| } else if (collector.type === 'FidoAuthenticationCollector') { | ||
| const button = document.createElement('button'); | ||
| button.type = 'button'; | ||
| button.value = collector.output.key; | ||
| button.innerHTML = 'FIDO Authenticate'; | ||
| formEl.appendChild(button); | ||
|
|
||
| button.onclick = async () => { | ||
| const credentialOptions = collector.output.config.publicKeyCredentialRequestOptions; | ||
| const response = await fidoApi.authenticate(credentialOptions); | ||
| console.log('fido.authenticate response:', response); | ||
| if ('error' in response) { | ||
| console.error(response); | ||
| } else { | ||
| const error = updater(response); | ||
| if (error && 'error' in error) { | ||
| console.error(error.error.message); | ||
| } else { | ||
| await submitForm(); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we know if we are going to have the same issue with DaVinci as we did with AM? The too many credentials issue, that is.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Responded to this via slack, but for posterity
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| 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.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.afterEach(async () => { | ||
| await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }); | ||
| await cdp.send('WebAuthn.disable'); | ||
| }); | ||
|
|
||
| test.describe('FIDO/WebAuthn Tests', () => { | ||
| test('Register and authenticate with webauthn device', async ({ page }) => { | ||
| const { navigate } = asyncEvents(page); | ||
|
|
||
| await navigate( | ||
| '/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c', | ||
| ); | ||
| await expect(page).toHaveURL( | ||
| 'http://localhost:5829/?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(); | ||
|
|
||
| // Register WebAuthn credential | ||
| const { credentials: initialCredentials } = await cdp.send('WebAuthn.getCredentials', { | ||
| authenticatorId, | ||
| }); | ||
| await expect(initialCredentials).toHaveLength(0); | ||
|
|
||
| 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(); | ||
|
|
||
| const { credentials: recordedCredentials } = await cdp.send('WebAuthn.getCredentials', { | ||
| authenticatorId, | ||
| }); | ||
| await expect(recordedCredentials).toHaveLength(1); | ||
|
|
||
| await page.getByRole('button', { name: 'Continue' }).click(); | ||
|
|
||
| // Verify we're back at home page if successful | ||
| await expect(page.getByText('FIDO2 Test Form')).toBeVisible(); | ||
|
|
||
| // Authenticate with the registered WebAuthn credential | ||
| const initialSignCount = recordedCredentials[0].signCount; | ||
|
|
||
| await page.getByRole('button', { name: 'DEVICE_AUTHENTICATION' }).click(); | ||
| await page.getByRole('button', { name: 'Biometrics/Security Key' }).last().click(); | ||
| await page.getByRole('button', { name: 'FIDO Authenticate' }).click(); | ||
|
|
||
| const credentialsAfterAuth = await cdp.send('WebAuthn.getCredentials', { | ||
| authenticatorId, | ||
| }); | ||
| await expect(credentialsAfterAuth.credentials).toHaveLength(1); | ||
|
|
||
| // Signature counter should have incremented after successful authentication/assertion | ||
| await expect(credentialsAfterAuth.credentials[0].signCount).toBeGreaterThan(initialSignCount); | ||
|
|
||
| // Verify we're back at home page if successful | ||
| await expect(page.getByText('FIDO2 Test Form')).toBeVisible(); | ||
| }); | ||
|
|
||
| // Note: This test is currently not working due to a DaVinci issue where the authentication options | ||
| // are not included in the response. | ||
| test.skip('Register and authenticate with usernameless', async ({ page }) => { | ||
| const { navigate } = asyncEvents(page); | ||
|
|
||
| await navigate( | ||
| '/?clientId=20dd0ed0-bb9b-4c8f-9a60-9ebeb4b348e0&acr_values=98f2c058aae71ec09eb268db6810ff3c', | ||
| ); | ||
| await expect(page).toHaveURL( | ||
| 'http://localhost:5829/?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(); | ||
|
|
||
| // Register WebAuthn credential | ||
| const { credentials: initialCredentials } = await cdp.send('WebAuthn.getCredentials', { | ||
| authenticatorId, | ||
| }); | ||
| await expect(initialCredentials).toHaveLength(0); | ||
|
|
||
| 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(); | ||
|
|
||
| const { credentials: recordedCredentials } = await cdp.send('WebAuthn.getCredentials', { | ||
| authenticatorId, | ||
| }); | ||
| await expect(recordedCredentials).toHaveLength(1); | ||
|
|
||
| await page.getByRole('button', { name: 'Continue' }).click(); | ||
|
|
||
| // Verify we're back at home page if successful | ||
| await expect(page.getByText('FIDO2 Test Form')).toBeVisible(); | ||
|
|
||
| // Authenticate with the registered WebAuthn credential | ||
| const initialSignCount = recordedCredentials[0].signCount; | ||
|
|
||
| await page.getByRole('button', { name: 'USER_NAMELESS' }).click(); | ||
| await expect(page.getByText('FIDO2 Authentication')).toBeVisible(); | ||
| await page.getByRole('button', { name: 'FIDO Authenticate' }).click(); | ||
|
|
||
| const credentialsAfterAuth = await cdp.send('WebAuthn.getCredentials', { | ||
| authenticatorId, | ||
| }); | ||
| await expect(credentialsAfterAuth.credentials).toHaveLength(1); | ||
|
|
||
| // Signature counter should have incremented after successful authentication/assertion | ||
| await expect(credentialsAfterAuth.credentials[0].signCount).toBeGreaterThan(initialSignCount); | ||
|
|
||
| // Verify we're back at home page if successful | ||
| await expect(page.getByText('FIDO2 Test Form')).toBeVisible(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,14 @@ export function fido(): FidoClient { | |
| register: async function register( | ||
| options: FidoRegistrationOptions, | ||
| ): Promise<FidoRegistrationInputValue | GenericError> { | ||
| if (!options) { | ||
| return { | ||
| error: 'registration_error', | ||
| message: 'FIDO registration failed: No options available', | ||
| type: 'fido_error', | ||
| } as GenericError; | ||
| } | ||
|
|
||
|
Comment on lines
+58
to
+65
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! It's good we caught this during our testing phase. |
||
| const createCredentialµ = Micro.sync(() => transformRegistrationOptions(options)).pipe( | ||
| Micro.flatMap((publicKeyCredentialCreationOptions) => | ||
| Micro.tryPromise({ | ||
|
|
@@ -108,6 +116,14 @@ export function fido(): FidoClient { | |
| authenticate: async function authenticate( | ||
| options: FidoAuthenticationOptions, | ||
| ): Promise<FidoAuthenticationInputValue | GenericError> { | ||
| if (!options) { | ||
| return { | ||
| error: 'authentication_error', | ||
| message: 'FIDO authentication failed: No options available', | ||
| type: 'fido_error', | ||
| } as GenericError; | ||
| } | ||
|
|
||
| const getAssertionµ = Micro.sync(() => transformAuthenticationOptions(options)).pipe( | ||
| Micro.flatMap((publicKeyCredentialRequestOptions) => | ||
| Micro.tryPromise({ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for finding these ❤️