Oursky

BLOG

A Practical Guide: Automating Passkey Testing with Playwright and Authgear

Joyz Ng

Passkeys (WebAuthn) offer a secure, passwordless future for web logins, but testing them presents unique challenges. This practical guide walks you through automating Passkey testing using Playwright and CDP features, and how you can quickly build an app to support Passkeys login with Authgear.

What is WebAuthn?

WebAuthn (short for Web Authentication) is a web standard that lets users log in securely without passwords.

Instead of typing a password:

  • You use your fingerprint, FaceID, or device PIN.
  • It is safer (no password to steal) and easier for users.
  • WebAuthn is the technology behind Passkeys.

In short:

  • No passwords
  • Use device security (fingerprint, face, PIN)
  • Safer and faster login

What is a Passkey?

A Passkey is a login credential based on WebAuthn. It is stored securely on your device, and you can use it to log in by just unlocking your device (no typing).

Apple, Google, Microsoft, and others support Passkeys.

Passkeys work across devices (phone → computer, etc.)

Passkey = WebAuthn credential that syncs across your devices

What is a CDP Session?

CDP = Chrome DevTools Protocol

It lets Playwright talk directly to Chrome like a developer tool would:

  • Simulate fake devices
  • Control authentication prompts
  • Do deeper testing

When testing Passkeys, we use a CDP session to create a fake Passkey device so we don’t need real hardware.

Build with Authgear: One-Click Passkey Activation

With Authgear (which provides Free Plan!), you can quickly spin up an app with passkey authentication in minutes:

  1. Create a basic app using v0, Lovable, Bolt, or other UI builders
  2. Enable passkeys with one click in your Authgear admin portal
  3. Integrate their SDK with minimal code
  4. Add their pre-built authentication components

The platform handles all complex WebAuthn protocols and cryptography behind the scenes, giving you a complete passkey authentication system to test with no specialized knowledge required.

Set Up Passkey on Authgear by One-Click

Use Playwright to Test Passkey Login (Step-by-Step)

Now that you have an app ready (thanks to Authgear), let’s start testing!

Step 1: Install Playwright (if you haven’t yet)

Follow the instructions on Playwright official site to install and setup the automated test project:

npm init playwright@latest

Step 2: Create a WebAuthn Helper Class

Create a script called webauthn-helper.ts to simulate a virtual passkey device in your Playwright tests:

import { Page, BrowserContext } from '@playwright/test';

export class WebAuthnHelper {
    private page: Page;
    private context: BrowserContext;
    private authenticatorId: string | null = null;
    private client: any;

    constructor(page: Page, context: BrowserContext) {
        this.page = page;
        this.context = context;
    }

    async setupWebAuthnEnvironment(): Promise<void> {
        this.client = await this.context.newCDPSession(this.page);

        await this.client.send('WebAuthn.enable');

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

        this.authenticatorId = result.authenticatorId;
        console.log('Authenticator ID:', this.authenticatorId);
    }

    async setUserVerified(isVerified: boolean): Promise<void> {
        if (!this.authenticatorId || !this.client) {
            throw new Error('Authenticator not initialized.');
        }

        await this.client.send('WebAuthn.setUserVerified', {
            authenticatorId: this.authenticatorId,
            isUserVerified: isVerified,
        });
    }

    async removeAuthenticator(): Promise<void> {
        if (!this.authenticatorId || !this.client) {
            return;
        }

        await this.client.send('WebAuthn.removeVirtualAuthenticator', {
            authenticatorId: this.authenticatorId,
        });

        this.authenticatorId = null;
    }
}

This helper script uses mainly 3 CDP commands:

  • WebAuthn.addVirtualAuthenticator - Creates a virtual authenticator device
  • WebAuthn.setUserVerified - Controls authentication approval state
  • WebAuthn.removeVirtualAuthenticator - Cleans up the virtual device

Setting automaticPresenceSimulation: true makes the virtual device auto-approve "touch" actions.

You can extend this with additional CDP / WebAuthn commands like:

  • WebAuthn.getCredentials - List stored credentials
  • WebAuthn.clearCredentials - Remove all stored credentials
  • WebAuthn.addCredential - Add a specific credential
  • Set automaticPresenceSimulation: false combined with WebAuthn.setUserVerified(false) to test rejection flows

Step 3: Write a Test to Sign Up and Log In with a Passkey

Then you can write your Playwright test script and uses the WebAuthnHelper Class to simulate the Passkey Login.

Below is a sample test script user-login-passkey.spec.ts:

import { test, expect } from '@playwright/test';
import { generateTestEmail, generateTestPassword } from '../utils/generate-data';
import { WebAuthnHelper } from '../utils/webauthn-helper';

test('User: Login with email and passkey', async ({ page, context }) => {
  // Generate unique test data
  const testEmail = generateTestEmail();
  const testPassword = generateTestPassword();

  // Create and setup the virtual passkey device
  const webAuthnHelper = new WebAuthnHelper(page, context);
  await webAuthnHelper.setupWebAuthnEnvironment();

  // Navigate to application and start login
  await page.goto('https://your-authgear-app.com');
  await page.waitForLoadState('networkidle');
  await page.getByTestId('login-button').click();
  await expect(page.getByRole('heading', { name: /Sign up or Log in to/i })).toBeVisible();

  // Sign up with new email
  await page.getByRole('textbox', { name: 'Email address' }).fill(testEmail);
  await page.getByRole('button', { name: 'Continue' }).click();
  await page.waitForLoadState('networkidle');

  // Create password for the new account
  await page.getByRole('textbox', { name: 'New Password' }).fill(testPassword);
  await page.getByRole('textbox', { name: 'Re-enter Password' }).fill(testPassword);
  await page.getByRole('button', { name: 'Continue' }).click();
  await page.waitForLoadState('networkidle');

  // Set up passkey for the account
  await expect(page.getByRole('heading', { name: /Simplfy your sign-in/i })).toBeVisible();
  await page.getByRole('button', { name: 'Continue' }).click();
  await webAuthnHelper.setUserVerified(true);  // Auto-approve the passkey prompt
  await page.waitForLoadState('networkidle');

  // Log out to test the login flow
  await page.getByTestId('logout-button').click();
  await page.waitForLoadState('networkidle');

  // Start login process with email
  await page.getByTestId('login-button').click();
  await page.waitForLoadState('networkidle');
  await page.getByRole('textbox', { name: 'Email address' }).fill(testEmail);
  await page.getByRole('button', { name: 'Continue' }).click();

  // Switch to passkey authentication instead of password
  await page.getByRole('button', { name: 'Use passkey'}).click();
  await page.waitForTimeout(5000);  // Wait for passkey authentication process
  await page.waitForLoadState('networkidle');

  // Verify successful login with the same user email
  await expect(page.getByTestId('user-email')).toContainText(testEmail);

  // Clean up by removing the virtual authenticator
  await webAuthnHelper.removeAuthenticator();
});

This test demonstrates a complete user journey:

  1. Creates a new user with random credentials
  2. Sets up a passkey for the account
  3. Logs out and then logs back in using the passkey instead of password
  4. Verifies the user identity is preserved between sessions
  5. Uses proper assertions to validate each step of the process
  6. Cleans up test resources afterwards

Quick Recap

Here’s the gist:

  • Passkeys (WebAuthn): Secure, password-free logins
  • Playwright + CDP: Automate testing by simulating passkey interactions
  • Authgear: Easily add passkey support to your app

Combining these technologies you can get:

  • Stronger Security: No more password risks
  • Better UX: Faster, easier logins for users
  • Faster Development: Easily implement passkeys with quick tools
  • Reliable Testing: Safeguard your login flows work with automation

Get started with passkeys and automated testing to secure your app, improve user experience, and simplify your workflows 🚀

Share

Discuss what we could do for you.