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:
- Create a basic app using v0, Lovable, Bolt, or other UI builders
- Enable passkeys with one click in your Authgear admin portal
- Integrate their SDK with minimal code
- 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.

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 deviceWebAuthn.setUserVerified
- Controls authentication approval stateWebAuthn.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 credentialsWebAuthn.clearCredentials
- Remove all stored credentialsWebAuthn.addCredential
- Add a specific credential- Set
automaticPresenceSimulation: false
combined withWebAuthn.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:
- Creates a new user with random credentials
- Sets up a passkey for the account
- Logs out and then logs back in using the passkey instead of password
- Verifies the user identity is preserved between sessions
- Uses proper assertions to validate each step of the process
- 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 🚀