passkeys e2e playwright testing webauthn virtual authenticatorPasskeys Implementation

Passkeys E2E Playwright Testing via WebAuthn Virtual Authenticator

Learn to set up E2E testing for passkeys across browsers with Playwright, Nightwatch, Selenium, and Puppeteer using the WebAuthn virtual authenticator.

Blog-Post-Author

Anders

Created: March 30, 2024

Updated: October 1, 2024


Our mission is to make the Internet a safer place , and the new login standard passkeys provides a superior solution to achieve that. Thats why we want to help you understanding passkeys and its characteristics better.

1. Introduction: Passkeys E2E Testing

2. Background: Browser Automation and E2E Testing Frameworks

    2.1 What is Browser Automation?

    2.2 What is WebDriver?

    2.3 What is Chrome DevTools Protocol (CDP)?

    2.4 Puppeteer & Playwright as CDP-based E2E Testing Frameworks

    2.5 Selenium & Nightwatch as WebDriver-based E2E Testing Frameworks

    2.6 Cypress as E2E Testing Framework with Native Scripting

3. What Makes E2E Testing of Passkeys with Playwright a Problem?

4. WebAuthn Virtual Authenticator Makes E2E Passkey Testing Possible

    4.1 What is the WebAuthn Virtual Authenticator?

    4.2 What are the WebAuthn Virtual Authenticators Benefits?

        4.2.1 Automated Testing with the Webauthn Virtual Authenticator

        4.2.2 Manual Testing and Demonstration with the WebAuthn Virtual Authenticator

    4.3 What are the Disadvantages of the WebAuthn Virtual Authenticator?

        4.3.1 Inability to Simulate Hardware-Specific Functionalities

        4.3.2 Sparse Documentation and Unaddressed Technical Problems

5. How to Set Up WebAuthn Virtual Authenticator in Playwright

6. WebAuthn Virtual Authenticator Use Cases

    6.1 How to Simulate Passkey Input Attempt in Playwright

        6.1.1 Approach 1: Automatic simulation with automaticPresenceSimulation set to true

        6.1.2 Approach 2: Manual Simulation with automaticPresenceSimulation set to false

    6.2 How to Test Passkey Creation

    6.3How to Test Passkey Verification

    6.4 How to Test Passkey Deletion

    6.5 How to Simulate Cross-Device Authentication

7. Alternatives to the WebAuthn Virtual Authenticator

    7.1 Testing with Mock Services

    7.2 Integration Testing with Real Authenticators

8. Recommendations for Developers

    8.1 Study the Landscape for E2E Testing Frameworks

    8.2 Understand the Underlying Concepts behind WebAuthn and Passkeys

9. Summary

1. Introduction: Passkeys E2E Testing

Passkeys are becoming more and more widely accepted as an authentication method, relying on Web Authentication (WebAuthn) as its underlying standard. Their rise in popularity is quite recent, which makes documentation and other resources relatively scarce. This, along with the complex nature of implementing passkeys, can make it challenging for developers to find relevant information on designing, implementing, and especially testing passkeys for their platforms and services.

This guide aims to fill that gap, focusing on aspects of the WebAuthn virtual authenticator not thoroughly covered in its official documentation. For instance, we discuss configuration options for the virtual authenticator that are not self-explanatory in the documentation, as well as workarounds for certain use cases that the virtual authenticator does not provide a convenient solution for. Otherwise, this guide is also helpful for developers who are simply looking for easy-to-follow examples for using the virtual authenticator in test code.

Our guide uses examples from Playwright to provide a simple walkthrough for effectively testing passkey implementation in your project. Playwright is an end-to-end (E2E) testing framework that uses Chrome DevTools Protocol (CDP) as the protocol for browser automation. If you are specifically looking for technical examples of testing passkeys in Playwright, you can skip ahead to Section 5. On the other hand, if you are using other E2E testing frameworks like Puppeteer or Selenium and looking to test passkeys on these frameworks, the test code implementations will be identical or very similar to the examples provided in this guide, depending on which framework you are using. In the next section we provide a background on the different E2E frameworks and how relevant this guide will be for these frameworks.

2. Background: Browser Automation and E2E Testing Frameworks

2.1. What is Browser Automation?

Browser automation, as the name suggests, is the process of automating repetitive user actions on the browser for the purposes of scraping the web for data or in our case testing web applications. WebDriver and Chrome DevTools Protocol (CDP) are two of the main browser automation protocols that are relevant for this guide, as they each provide an implementation of the WebAuthn virtual authenticator.

2.2. What is WebDriver?

WebDriver is a remote-controlled interface that can be seen as a middleman in the communication between the client and the browser. The focus of this protocol is to provide a platform- and language-neutral interface that supports all major browsers, including ones that are not Chromium-based such as Firefox and Safari. As the WebDriver interface needs to manage a connection with the client as well as with the browser, this approach sacrifices speed and stability in exchange for a wider range of browser support (i.e. higher flakiness). Notable WebDriver clients include Selenium and Nightwatch.

Passkeys Playwright WebDriver Client

Taken from jankaritech

2.3. What is Chrome DevTools Protocol (CDP)?

Chrome DevTools Protocol (CDP), on the other hand, does not have a middleman like the WebDriver interface between the client and the browser. In addition, the communication between the client and the browser happens via a socket connection, in contrast with the slower HTTP connection between the client and the WebDriver interface in the previous approach. These points make CDP much faster and less flaky than WebDriver. The downside is that this protocol is only supported for Chromium-based browsers like Chrome and Edge. Playwright and Puppeteer are example clients that use CDP to communicate with browsers.

Passkeys PlayWright CDP Client

Taken from jankaritech

2.4. Puppeteer & Playwright as CDP-based E2E Testing Frameworks

Puppeteer, akin to Playwright, is an E2E framework built directly on the CDP. This means that Puppeteer and Playwright both use the same implementation of the WebAuthn virtual authenticator and that the API communication using the WebAuthn virtual authenticator via the socket connection is also identical.

To demonstrate, we compare the test code in both Playwright and Puppeteer for calling the getCredentials method which returns a list of all credentials registered to the virtual authenticator so far. We also attach a simple event listener for the credentialAdded event which is triggered when a passkey credential is successfully registered. Do not be intimidated by the details of the implementation, as they will be explained in the later sections. These examples are simply to demonstrate how similar the implementations are between the two frameworks.

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.

Join Passkeys Community

Playwright:

const client = await page.context().newCDPSession(page); await client.send('WebAuthn.enable'); const authenticatorId = const result = await client.send('WebAuthn.addVirtualAuthenticator', { options }); ... // get all credentials registered in the virtual authenticator const result = await client.send('WebAuthn.getCredentials', { authenticatorId }); console.log(result.credentials); // add a listener to the credentialAdded event to output a log to the console whenever a passkey credential is registered client.on('WebAuthn.credentialAdded', () => { console.log('Credential Added!'); });

Puppeteer:

const client = await page.target().createCDPSession(); await client.send('WebAuthn.enable'); const authenticatorId = const result = await client.send('WebAuthn.addVirtualAuthenticator', { options }); ... // get all credentials registered in the virtual authenticator const result = await client.send('WebAuthn.getCredentials', { authenticatorId }); console.log(result.credentials); // add a listener to the credentialAdded event to output a log to the console whenever a passkey credential is registered client.on('WebAuthn.credentialAdded', () => { console.log('Credential Added!'); });

While the methods for initializing the CDP session in the beginning of the test codes were slightly different, calling methods and handling events in the CDP WebAuthn virtual authenticator API is identical. This means that if you are looking to use the WebAuthn virtual authenticator in Puppeteer, you can follow this guide line-by-line.

Slack Icon

Become part of our Passkeys Community for updates and support.

Join

2.5. Selenium & Nightwatch as WebDriver-based E2E Testing Frameworks

Selenium and Nightwatch are E2E testing frameworks that rely on WebDriver for browser automation. While the WebAuthn virtual authenticator implementation for WebDriver is separate from its implementation for CDP, their API specifications are similar. For almost every method in the CDP Webauthn virtual authenticator API, you can find a corresponding method in the WebDriver WebAuthn virtual authenticator API. However, one thing to note is that while it was possible to attach event listeners for when a passkey is successfully added or asserted in the CDP WebAuthn virtual authenticator API, this is not possible in the WebDriver counterpart.

Selenium:

const driver = await new Builder().forBrowser('chrome').build(); const options = new VirtualAuthenticatorOptions(); await driver.addVirtualAuthenticator(options); ... // get all credentials registered in the virtual authenticator const credentials = await driver.getCredentials();

It is evident that the syntax of setting up the virtual authenticator instance and making API calls are different from the corresponding CDP implementation. However, since the API specifications of the two WebAuthn virtual authenticators are very similar, it would be viable to follow this guide to write a corresponding implementation on a WebDriver-based E2E testing framework.

2.6. Cypress as E2E Testing Framework with Native Scripting

Cypress is an E2E testing framework that is not primarily built on WebDriver or CDP like the frameworks mentioned above. It uses native JavaScript to communicate with the browser. However, it provides low-level access to the CDP, which means it is possible to send raw CDP commands to utilize the CDPs WebAuthn virtual authenticator.

Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

Because the syntax for this low-level access is tedious and very different from the examples above, we will not go into details in this guide. However, further information on how to call CDP commands in Cypress is explained in this guide. The big picture concepts for using the CDP WebAuthn virtual authenticator presented in this guide is still relevant for those seeking to test passkeys on Cypress.

3. What Makes E2E Testing of Passkeys with Playwright a Problem?

There are many reasons why testing passkey implementation is naturally more challenging than other, simpler user actions in a web environment. The need to handle dynamic user interactions involved with biometric authentication, such as fingerprint scanning or facial recognition, adds a layer of complexity that might not be practical to address in detail while writing tests. As security is naturally a major concern in the context of authentication, it is also necessary to ensure that passkey authentication is seamlessly integrated across various browsers and devices without room for security vulnerabilities.

4. WebAuthn Virtual Authenticator Makes E2E Passkey Testing Possible

Simplifying the complexity of handling dynamic user interactions involved in passkey operations, as well as testing its integration into different browsers and devices, are made easier using the WebAuthn virtual authenticator.

4.1. What is the WebAuthn Virtual Authenticator?

The WebAuthn virtual authenticator is a software representation of the authenticator model specified in the WebAuthn standard. It emulates the behavior of a physical authenticator device, such as a hardware security key (e.g. YubiKey) or biometric scanner (e.g. used in Face ID, Touch ID or Windows Hello), but operates entirely within software (so no physical authentication or scanning of biometrics is involved).

4.2. What are the WebAuthn Virtual Authenticators Benefits?

There are two main benefits for the WebAuthn virtual authenticator.

4.2.1. Automated Testing with the Webauthn Virtual Authenticator

As WebDriver and CDP are browser automation tools, it is evident that the primary use case of the WebAuthn virtual authenticator implementation in these protocols is automated testing. Leveraging these protocols, the virtual authenticator enables simple yet comprehensive testing of passkey functionalities in controlled environments such as E2E testing frameworks (e.g. Playwright, Cypress, Nightwatch).

4.2.2. Manual Testing and Demonstration with the WebAuthn Virtual Authenticator

The CDPs WebAuthn virtual authenticator is also accessible via the Chrome browsers DevTools, and it can be used for manual testing or simply for demonstration purposes. With this feature you can simulate passkey input on a device that does not natively support passkeys. Correspondingly, it is also possible to simulate a passkey-unsupported environment on a device that supports passkeys.

Passkeys Playwright Virtual WebAuthn Authenticator

The screenshot above shows an example of using the virtual authenticator in Chrome for manual testing or demonstration purposes. You can see that different configuration options for the virtual authenticator are possible, and the addition and deletion of credentials can also be tracked. Refer to this guide from Google for more information on using the virtual authenticator in your browser, including the configuration options and recommended values for each.

4.3. What are the Disadvantages of the WebAuthn Virtual Authenticator?

While the WebAuthn virtual authenticator is an elegant solution for testing passkey implementations, there are a few disadvantages worth noting.

4.3.1. Inability to Simulate Hardware-Specific Functionalities

Being a purely software-based solution, the WebAuthn virtual authenticator cannot replicate the unique hardware characteristics and security features of physical authenticators. The distinction between using various platform authenticators (which are built into a device, such as biometric scanner on a smartphone) and various cross-platform authenticators (which are external devices, like hardware security keys) cannot be simulated using the WebAuthn virtual authenticator. While the black-box simplification of the complexities involved with various types of platform and cross-platform authenticators is one of the advantages of using the WebAuthn virtual authenticator, if you seek to simulate and test the nuances of the different types of authenticators, other solutions should be explored.

4.3.2. Sparse Documentation and Unaddressed Technical Problems

Given the relatively recent adoption of WebAuthn and the novelty of passkey technology, the ecosystem surrounding virtual authenticators is still maturing. This results in a scarcity of comprehensive documentation and unresolved technical challenges, particularly in the context of integrating virtual authenticators with automated testing frameworks. This guide aims to address this issue by providing comprehensive insights into testing passkeys in an automated testing environment, while also focusing on addressing the inconveniences still present in using these tools and presenting workarounds for these problems.

5. How to Set Up WebAuthn Virtual Authenticator in Playwright

After a successful installation of Playwright and itsdependencies, you can get started right away writing your first test bycreating a file with a name that ends in either .spec.ts or .test.ts with thefollowing content:

import { test, expect } from '@playwright/test'; test('my first test', async ({ page }) => { await page.goto('https://passkeys.eu'); // start simulating user actions });

To use the WebAuthn virtual authenticator in Playwright, it is sufficient to simply initiate a CDP session and attach a virtual authenticator in the beginning of a test case, as follows:

test('signup with passkey', async ({ page }) => { // Initialize a CDP session for the current page const client = await page.context().newCDPSession(page); // Enable WebAuthn environment in this session await client.send('WebAuthn.enable'); // Attach a virtual authenticator with specific options const result = await client.send('WebAuthn.addVirtualAuthenticator', { options: { protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: false, }, }); const authenticatorId = result.authenticatorId; // Further test steps to simulate user interactions and assertions ... });

Options for configuring the WebAuthn virtual authenticator:

  • protocol: This option specifies the protocol the virtual authenticator speaks. Possible values are "ctap2" and "u2f"
  • transport: This option specifies the type of authenticator the virtual authenticator simulates. Possible values are "usb", "nfc", "ble", and "internal". If set to "internal" it simulates a platform authenticator, while other values simulate cross-platform authenticators.
  • hasResidentKey: Setting it to true supports Resident Key (i.e. client-side discoverable credential).
  • hasUserVerification: Setting it to true supports User Verification. Setting this as true is recommended as it allows simulation of successful and failed passkey input.
  • isUserVerified: Setting it to true emulates a successful authentication scenario, whereas false mimics an authentication failure, such as when a user cancels the passkey input. Note that this setting is effective only when hasUserVerification is set to true.
  • automaticPresenceSimulation: When set to true, passkey input occurs automatically and immediately upon any authentication prompt. Conversely, setting it to false requires manual initiation of the passkey authentication simulation in the test code. Opting for manual simulation (false) is recommended for two reasons:
    • Increasing test code readability: Automatic simulation can obscure the understanding of test flow, as authentication attempts are simulated without explicit triggers in the test code.
    • Avoiding unintended behavior: Automatic simulation means that passkey input will be triggered even if the tester is not aware that the passkey was prompted. This is especially a problem for Conditional UI which would be easier for the tester to overlook.

6. WebAuthn Virtual Authenticator Use Cases

In this section, we explore the usage of the WebAuthn virtual authenticator methods and events in the context of both common and fringe use-cases.

6.1. How to Simulate Passkey Input Attempt in Playwright

This might be the most important yet confusing task when using the WebAuthn virtual authenticator in a test code, as there is no explicit built-in method to trigger a passkey input. The solution lies in the WebAuthn virtual authenticator configuration options namely, isUserVerified and automaticPresenceSimulation. With these options we can simulate this user interaction via two different approaches described below.

6.1.1. Approach 1: Automatic simulation with automaticPresenceSimulation set to true

Case 1: Simulating a successful passkey input

test('signup with passkey', async ({ page }) => { ... await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); await page.getByRole(‘button’, { name: ‘Login’ }).click(); // successful passkey input is automatically simulated (isUserVerified=true) await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Welcome!’); ... });

Simulating a successful passkey input usually requires no additional lines in the test code. The final line (await expect...) waits for the page to change (triggered by the implicit successful passkey input).

Case 2: Simulating a cancelled passkey input (that does not trigger changes in the UI)

Testing a failed or cancelled passkey input is more complicated as it may not lead to any observable changes in the UI. In other words, waiting for the page to change as in the previous example is not adequate for ensuring that the passkey input has completed processing. Checking that the page has not changed after the implicit passkey input is meaningless, as the check will almost certainly happen before the passkey input has completed processing. While the virtual authenticator provides a way to wait for a successful passkey input to be processed by listening to event emission (as will be discussed in approach 2), there is currently no built-in way to detect a failed or cancelled passkey input. A workaround would be to simply add a hard timeout to wait for the passkey operation to complete before checking that the UI indeed stayed the same.

test('signup with passkey', async ({ page }) => { // Simulate a set of user actions to trigger a passkey prompt ... await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); // Simulate passkey input when prompted in the test await inputPasskey(async () => { await page.waitForTimeout(300); await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); }); // Further test steps ...

In either case, the readability of the test code is limited by the implicitness of the passkey operation. As mentioned earlier, it would also be easy to overlook when Conditional UI might be prompted, in which case the passkey operation would automatically complete without the testers knowledge.

6.1.2. Approach 2: Manual Simulation with automaticPresenceSimulation set to false

Manually triggering a passkey input by switching the value of automaticPresenceSimulation option resolves the problems encountered in the previous approach, namely in terms of test-code readability.

Case 1: Simulating a successful passkey input

Following code snippets simulate a successful passkey input:

async simulateSuccessfulPasskeyInput(operationTrigger: () => Promise<void>) { // initialize event listeners to wait for a successful passkey input event const operationCompleted = new Promise<void>(resolve => { client.on('WebAuthn.credentialAdded', () => resolve()); client.on('WebAuthn.credentialAsserted', () => resolve()); }); // set isUserVerified option to true // (so that subsequent passkey operations will be successful) await client.send('WebAuthn.setUserVerified', { authenticatorId: authenticatorId, isUserVerified: true, }); // set automaticPresenceSimulation option to true // (so that the virtual authenticator will respond to the next passkey prompt) await client.send(‘WebAuthn.setAutomaticPresenceSimulation’, { authenticatorId: authenticatorId, enabled: true, }); // perform a user action that triggers passkey prompt await operationTrigger(); // wait to receive the event that the passkey was successfully registered or verified await operationCompleted; // set automaticPresenceSimulation option back to false await client.send(‘WebAuthn.setAutomaticPresenceSimulation’, { authenticatorId, enabled: false, }); }
test('signup with passkey', async ({ page }) => { ... // Simulate passkey input with a promise that triggers a passkey prompt as the argument await simulateSuccessfulPasskeyInput(() => page.getByRole('button', { name: 'Create account with passkeys' }).click() ); ... });

The helper function might be quite intimidating when you look at it for the first time. It helps to understand that all the technical complexities of simulating a passkey operation are abstracted into the helper function. This means that when it is used inside the test code, it makes the code simple and clear, as can be seen in the second code snippet above.

Compared to the implicit approach in Section 6.1.1, this explicit approach increases the readability of the code as well. This would especially be helpful for when Conditional UI is prompted, as this explicit approach prevents unintentional, implicit completion of passkey operation without the developers awareness.

Now lets understand each part of the helper function.

First, we define the operationCompleted promise which waits for either WebAuthn.credentialAdded event or WebAuthn.credentialAsserted event, which is, as name suggests, emitted when a passkey credential is registered or verified, respectively. This promise will be used later.

Next, the isUserVerified option is set to true, so that the subsequent passkey operation by the WebAuthn virtual authenticator will be successful. The automaticPresenceSimulation is also set to true, so that the WebAuthn virtual authenticator will respond to the next passkey prompt from the webpage.

The awaiting of the operationTrigger promise is necessary to avoid a race condition. The race condition happens when the webpage prompts the passkey before the automaticPresenceSimulation is set to true. To prevent this, the user action which triggers the passkey prompt must be performed after the automaticPresenceSimulation is set to true. In the example above, the user clicks the button named Create account with passkeys to trigger the passkey prompt.

After the user action is completed, we must wait for the successful passkey operation to complete. This is done by awaiting the promise we defined in the beginning of the helper function. The completion of the successful passkey operation is marked by the emission of WebAuthn.credentialAdded or WebAuthn.credentialAsserted event. In the example above, as the user is registering a passkey, WebAuthn.credentialAdded event would be emitted.

Finally, the automaticPresenceSimulation option is set back to false, to prevent unintentional passkey operations from happening later in the test code.

Case 2: Simulating a cancelled passkey input

For a cancelled passkey input, we must make a slight modification to the implementation for the previous case. In the case of a successful passkey input, there are events, namely WebAuthn.credentialAdded and WebAuthn.credentialAsserted, which are emitted upon the completion of the operation. However, the WebAuthn virtual authenticator does not provide any event for a cancelled or failed passkey input. Thus, we must use an alternative way to check for the completion of a cancelled or failed passkey operation.

Following code snippets are simulate a failed passkey input:

async simulateFailedPasskeyInput(operationTrigger: () => Promise<void>, postOperationCheck: () => Promise<void>) { // set isUserVerified option to false // (so that subsequent passkey operations will fail) await client.send('WebAuthn.setUserVerified', { authenticatorId: authenticatorId, isUserVerified: false, }); // set automaticPresenceSimulation option to true // (so that the virtual authenticator will respond to the next passkey prompt) await client.send(‘WebAuthn.setAutomaticPresenceSimulation’, { authenticatorId: authenticatorId, enabled: true, }); // perform a user action that triggers passkey prompt await operationTrigger(); // wait for an expected UI change that indicates the passkey operation has completed await postOperationCheck(); // set automaticPresenceSimulation option back to false await client.send(‘WebAuthn.setAutomaticPresenceSimulation’, { authenticatorId, enabled: false, }); }
test('signup with passkey', async ({ page }) => { // Simulate a set of user actions to trigger a passkey prompt ... await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); // Simulate passkey input when prompted in the test await inputPasskey(async () => { await page.waitForTimeout(300); await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); }); // Further test steps ... });

In the helper function, the event listeners are replaced with a promise parameter postOperationCheck which waits for an expected UI change to happen before automaticPresenceSimulation can be set back to false.

In the test code, the only difference is that the helper function must be called with an additional promise which checks for the intended UI change. In the example above, we check that the web application has successfully navigated to a page in which the header has the text Something went wrong....

As was discussed in Section 6.1.1, cancelling a passkey input may not lead to any observable change to the UI. Like the example provided in that section, we must add a hard wait before checking that the UI has indeed remained the same in such cases:

test('signup with passkey', async ({ page }) => { ... await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); // Simulate passkey input with a promise that triggers a passkey prompt as the argument await simulateFailedPasskeyInput( () => page.getByRole('button', { name: 'Create account with passkeys' }).click(), async () => { await page.waitForTimeout(300); await expect(page.getByRole(‘heading’, { level: 1 })).toHaveText(‘Please log in’); } ); ... });

6.2. How to Test Passkey Creation

The convenience of using a WebAuthn virtual authenticator is enhanced by its ability to behave like a real authenticator in the event of passkey creation or deletion by the web application. A test simply needs to perform user actions to simulate the creation or deletion of a passkey on the web application, and the WebAuthn virtual authenticator automatically modifies its saved credential information without any additional work from the side of the test code.

Here is the example of test code which checks that the web application registers a new passkey to the authenticator properly:

test('signup with passkey', async ({ page }) => { ... // Confirm there are currently no registered credentials const result1 = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); expect(result1.credentials).toHaveLength(0); // Perform user actions to simulate creation of a passkey credential (e.g. user registration with passkey input) ... // Confirm the passkey was successfully registered const result2 = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); expect(result2.credentials).toHaveLength(1); ... });

Combining this code snippet with the code snippets from Section 6.1, we can test the signup flow on our demo webpage. The following video is a visualization of the test in Playwrights UI mode:

6.3. How to Test Passkey Verification

Verifying a passkey credential with the WebAuthn virtual authenticator works similarly to creating a passkey, in that the virtual authenticator automatically keeps track of the number of verifications performed using a particular credential.

test('login with passkey', async ({ page }) => { ... // Confirm there is only one credential, and save its signCount const result1 = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); expect(result1.credentials).toHaveLength(1); const signCount1 = result1.credentials[0].signCount; // Perform user actions to simulate verification of a passkey credential (e.g. login with passkey input) ... // Confirm the credential's new signCount is greater than the previous signCount const result2 = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); expect(result2.credentials).toHaveLength(1); expect(result2.credentials[0].signCount).toBeGreaterThan(signCount1); ... });

The following video demonstrates a test for the login flow on our demo webpage:

6.4. How to Test Passkey Deletion

Deleting a passkey from a web application, on the other hand, should not modify any information within the WebAuthn virtual authenticator. The web application should only be able to delete credentials saved in its own server. Only the user themselves should be able to consciously and manually delete a passkey credential from the WebAuthn virtual authenticator.

test('delete a registered passkey credential', async ({ page }) => { ... // Confirm there is currently one registered credential const result1 = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); expect(result1.credentials).toHaveLength(1); // Perform user actions to simulate deletion of a passkey credential ... // Deleting a passkey credential from a website should not remove the credential from the authenticator const result2 = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); expect(result2.credentials).toHaveLength(1); ... });

The following video demonstrates a test for deletion of a passkey credential on our demo webpage:

6.5. How to Simulate Cross- Device Authentication

The most intuitive way to simulate a cross-device authentication from a second device (which does not yet have a registered passkey) is to simply add a new instance of the WebAuthn virtual authenticator via the CDP command, as such:

test('signup with passkey', async ({ page }) => { ... // add a virtual authenticator for the first device const authenticatorId1 = await client.send('WebAuthn.addVirtualAuthenticator', { options }); // perform test actions of the first device ... // add a virtual authenticator for the second device const authenticatorId2 = await client.send('WebAuthn.addVirtualAuthenticator', { options }); // perform test actions of the second device .. });

To avoid the complexity of managing the IDs of multiple virtual authenticators, it is also possible to simulate a new device by simply deleting the credentials from a single authenticator, and adding it back when needed:

test('signup with passkey', async ({ page }) => { ... const result = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); const credential = result.credentials[0]; // assuming only one registered passkey const credentialId = credential.credentialId; await client.send(‘WebAuthn.removeCredential’, { authenticatorId, credentialId }); // Perform test actions of the second device which doesn’t have a registered passkey ... // Call if it’s necessary to simulate the first device which has a registered passkey await client.send(‘WebAuthn.addCredential’, { credential }); // Perform test actions of the first device ... });

This approach can especially simplify the implementation in the case where a new device needs to be simulated, but the old device does not need to be used any longer. In this case, you simply need to clear the credentials from the virtual authenticator and discard its credentials altogether:

test('signup with passkey', async ({ page }) => { ... const result = await client.send(‘WebAuthn.getCredentials’, { authenticatorId }); const credential = result.credentials[0]; // assuming only one registered passkey const credentialId = credential.credentialId; await client.send(‘WebAuthn.clearCredentials’, { authenticatorId }); // Perform test actions of the second device which doesn’t have a registered passkey ... });

7. Alternatives to the WebAuthn Virtual Authenticator

Exploring alternatives to the WebAuthn virtual authenticator can offer flexibility in how passkey / WebAuthn authentication processes are tested within projects.

7.1. Testing with Mock Services

Developing mock services or endpoints can effectively simulate authentication behavior, simplifying tests by abstracting the intricacies of the actual authentication mechanism. This approach is particularly beneficial when external authentication services are in use, enabling the focus to remain on the integration and functionality of system components without delving into authentication specifics.

7.2. Integration Testing with Real Authenticators

For a thorough examination of authentication functionalities, employing real authenticators for integration testing provides a detailed insight into the interaction with hardware security keys (e.g. YubiKeys) or biometric devices (e.g. used in Face ID, Touch ID or Windows Hello). Although typically conducted manually due to the complex nature of integrating real-world devices into automated tests, it's feasible to develop custom automation scripts. These scripts can bridge real authenticators with end-to-end testing frameworks, offering a closer approximation to real user scenarios and enhancing the reliability of the authentication process in live environments.

8. Recommendations for Developers

After demonstrating the different options and showcasing specific code snippets for E2E testing passkeys / WebAuthn with Playwright, we additionally want to provide some more general recommendations for developers new to the topic.

8.1. Study the Landscape for E2E Testing Frameworks

Before diving into testing passkeys or any other authentication mechanisms, it's essential to assess the available E2E testing frameworks and choose the most appropriate option according to your project's requirements. Consider the tradeoff between the speed and stability offered by CDP-based frameworks like Playwright and Puppeteer, and the cross-browser compatibility provided by WebDriver-based frameworks like Selenium and Nightwatch. While CDP-based frameworks offer faster and more stable browser automation, they are limited to Chromium-based browsers. In contrast, WebDriver-based frameworks provide broader cross-browser compatibility, including support for non-Chromium browsers like Firefox and Safari, albeit with potentially slower and less stable performance. Understanding these tradeoffs will help you make an informed decision and select the framework that best suits your project's needs.

8.2. Understand the Underlying Concepts behind WebAuthn and Passkeys

While the WebAuthn virtual authenticator simplifies the process of testing passkey implementations, it's crucial for developers to have a solid understanding of the underlying concepts behind the WebAuthn standard and passkeys. Familiarize yourself with the different configurations available for the WebAuthn virtual authenticator, such as protocol, transport, hasResidentKey, hasUserVerification, and isUserVerified. Understanding these configurations will enable you to fine-tune the virtual authenticator to simulate various authentication scenarios accurately. Additionally, delve into the intricacies of passkey authentication, including its integration with different browsers and devices, as well as potential security considerations. This foundational knowledge will empower you to design comprehensive and effective testing strategies for passkey authentication in your web applications.

9. Summary

This guide delved into using the CDP WebAuthn virtual authenticator with Playwright, highlighting advanced concepts and addressing issues not covered in official documentation. We also explored alternatives to CDP within Playwright and other E2E testing frameworks. Despite varying implementations, the standardized WebAuthn virtual authenticator specifications ensure the relevance of this guide across different web automation protocols and end-to- end testing frameworks. To learn more in depth about different concepts regarding passkeys, refer to our glossary of relevant terminologies that might help you finetune the WebAuthn virtual authenticator based on your needs.

Share this article


LinkedInTwitterFacebook

Table of Contents

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free