> ## Documentation Index
> Fetch the complete documentation index at: https://docs.authsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# E2E testing with passkeys

> Automate end-to-end browser tests that enroll and authenticate with passkeys using Playwright's virtual authenticator.

This guide shows how to write browser-based end-to-end tests that cover the full passkey lifecycle: enrolling a passkey, then using it to complete a challenge.

The test drives your app exactly like a user would, letting it redirect into your identity provider and on to Authsignal. This exercises your policy and user-flow configuration alongside the passkey ceremony.

## The challenge with passkey E2E tests

Passkeys are backed by hardware-bound or password-manager-synced credentials. A regular Playwright or Selenium test can't unlock Touch ID, summon a security key, or open Apple Passwords.

Chromium ships with the [WebAuthn DevTools Protocol domain](https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/), which lets you install a **virtual authenticator**. Once installed, every `navigator.credentials.create` and `navigator.credentials.get` call resolves against that virtual authenticator instead of real platform hardware. Playwright exposes this via [CDP sessions](https://playwright.dev/docs/api/class-cdpsession).

## Prerequisites

* An Authsignal tenant. We recommend a **dedicated tenant for testing** so test traffic doesn't pollute production analytics.
* The passkey authenticator enabled for the tenant.
* An app integrated with either the pre-built UI, the Web SDK, or an identity provider integration.
* Playwright installed in your project.

## Setting up the virtual authenticator

The helper below enables the WebAuthn CDP domain and adds a platform-style virtual authenticator that supports resident keys and user verification. This is the closest analogue to a real platform authenticator like Touch ID.

```ts utils/page-helpers.ts theme={null}
import { Page } from "@playwright/test";

export async function setupWebAuthn(page: Page) {
  const client = await page.context().newCDPSession(page);

  await client.send("WebAuthn.enable", { enableUI: true });

  const result = await client.send("WebAuthn.addVirtualAuthenticator", {
    options: {
      protocol: "ctap2",
      transport: "internal",
      hasResidentKey: true,
      hasUserVerification: true,
      isUserVerified: true,
      automaticPresenceSimulation: true,
    },
  });

  return { client, authenticatorId: result.authenticatorId };
}
```

Key options:

* `transport: "internal"` makes the credential behave like a platform authenticator. Use `"usb"` or `"nfc"` if you want to simulate a roaming security key.
* `hasResidentKey: true` enables discoverable credentials so [passkey autofill](/authentication-methods/passkey/web-sdk#using-autofill) works.
* `isUserVerified: true` and `automaticPresenceSimulation: true` let the test bypass biometric/touch prompts.

## Provision a test user

Each test needs a known user in your identity provider. Stand this up as a fixture so it runs once per test suite:

* **Azure AD B2C**: create users via the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/api/user-post-users).
* **Auth0**: use the [Management API `POST /api/v2/users`](https://auth0.com/docs/api/management/v2/users/post-users).
* **Cognito**: use [`AdminCreateUser`](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html) with a known password.
* **Keycloak**: use the [Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_users).
* Other or custom identity providers will have their own admin APIs to programmatically create users.

## Drive the journey end-to-end

Navigate to your app's normal login URL and complete the user login via primary factor first. If your application is passwordless, this will just be providing the username.

A realistic test covers two passes: an **enrollment pass** that creates the passkey, and a **re-authentication pass** that signs the same user back in using the credential just enrolled. The virtual authenticator persists for the life of the `BrowserContext`, so both passes share the same credential.

Depending on your integration, this may either redirect to Authsignal's pre-built UI or your custom UI which supports passkey enrollment and authentication.

```ts tests/passkey.spec.ts theme={null}
import { test, expect } from "@playwright/test";
import { setupWebAuthn } from "./utils/page-helpers";
import { provisionTestUser } from "./utils/idp-helpers";

test("user can enroll a passkey then sign back in with it", async ({ page }) => {
  // 1. Set up webAuthn and provision a test user
  const { client } = await setupWebAuthn(page);
  const testUser = await provisionTestUser();

  // 2. Navigate to your application's login page
  await page.goto("https://yourapp.com/login");

  // 3. Complete the login form and click the sign-in button
  await page.getByRole("textbox", { name: "Email Address" }).fill(testUser.email);
  await page.getByRole("textbox", { name: "Password" }).fill(testUser.password);
  await page.getByRole("button", { name: "Sign in" }).click();

  // 4. Wait for the virtual authenticator to create the passkey
  const credentialAdded = new Promise<void>((resolve) => {
    client.once("WebAuthn.credentialAdded", () => resolve());
  });

  // Depending on your integration and action configuration, you may need to customize this step to initiate the passkey creation prompt.
  // This example assumes the use of the Authsignal pre-built UI with an action configuration that has multiple authentication methods enabled.
  await page.getByText("Passkey", { exact: true }).click();

  await credentialAdded;

  // 5. The pre-built UI redirects back to your app and assert that the user is authenticated.
  await page.waitForURL(/yourapp\.com\/dashboard/);
  await expect(page.getByText(`Welcome, ${testUser.email}`)).toBeVisible();

  // 6. Sign out from your application to start a fresh journey on the next request
  await page.goto("https://yourapp.com/logout");

  // 7. Sign back in to begin the re-authentication process.
  await page.goto("https://yourapp.com/login");
  await page.getByLabel("Email Address").fill(testUser.email);
  await page.getByLabel("Password").fill(testUser.password);

  // 8. After clicking sign in, the user will be redirected to the Authsignal pre-built UI where they passkey prompt will be displayed.
  const credentialAsserted = new Promise<void>((resolve) => {
    client.once("WebAuthn.credentialAsserted", () => resolve());
  });
  await page.getByRole("button", { name: "Sign in" }).click();

  // Present the virtual authenticator credential to the browser
  await credentialAsserted;

  // 9. Wait for the pre-built UI to redirect back to your application and assert that the user is authenticated.
  await page.waitForURL(/yourapp\.com\/dashboard/);
  await expect(page.getByText(`Welcome, ${testUser.email}`)).toBeVisible();
});
```

The `WebAuthn.credentialAdded` and `WebAuthn.credentialAsserted` events fire when the virtual authenticator actually completes a ceremony, so they're a reliable signal that the WebAuthn call resolved successfully before you make further DOM assertions.

## Other test frameworks

This guide is written for Playwright, but the same virtual-authenticator approach works in any framework that speaks either the Chrome DevTools Protocol or the [W3C WebAuthn Automation extension](https://www.w3.org/TR/webauthn-2/#sctn-automation):

* **Selenium 4**: first-class support via the W3C WebDriver extension. See [Virtual Authenticator](https://www.selenium.dev/documentation/webdriver/interactions/virtual_authenticator/) in the Selenium docs. Works across Chrome, Edge, and Firefox.
* **WebdriverIO**: exposes the same WebDriver extension. See [`addVirtualAuthenticator`](https://webdriver.io/docs/api/webdriver#addvirtualauthenticator).
* **Puppeteer**: uses raw CDP, the same protocol Playwright uses under the hood. Open a [`CDPSession`](https://pptr.dev/api/puppeteer.cdpsession) and call the [`WebAuthn` domain](https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/) directly.

## Next steps

* Review [passkey best practices on web](/authentication-methods/passkey/best-practices-web) before deciding which actions to cover with E2E tests.
