Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/src/api/class-browsercontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ This event is not emitted.

Playwright has ability to mock clock and passage of time.

## property: BrowserContext.credentials
* since: v1.61
- type: <[Credentials]>

Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept
`navigator.credentials.create()` / `navigator.credentials.get()` ceremonies.

## property: BrowserContext.debugger
* since: v1.59
- type: <[Debugger]>
Expand Down
103 changes: 103 additions & 0 deletions docs/src/api/class-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# class: Credentials
* since: v1.61

`Credentials` provides a virtual WebAuthn authenticator scoped to a [BrowserContext]. It lets tests
seed credentials, intercept `navigator.credentials.create()` / `navigator.credentials.get()` calls
in pages, and complete WebAuthn ceremonies without a real authenticator.

Implemented in userland via an injected script, so it works across Chromium, Firefox and WebKit.

**Usage**

```js
const context = await browser.newContext();
await context.credentials.install();
await context.credentials.create({ rpId: 'example.com' });
const page = await context.newPage();
await page.goto('https://example.com/login');
// Page's navigator.credentials.get() will be answered using the seeded credential.
```

## async method: Credentials.install
* since: v1.61

Installs the virtual WebAuthn authenticator into the context, overriding
`navigator.credentials.create()` and `navigator.credentials.get()` in all current
and future pages. Call this before the page first touches `navigator.credentials`.

Required: until `install()` is called, no interception is in place and the page sees
the platform's native (or absent) WebAuthn behaviour. Seeding credentials with
[`method: Credentials.create`] without `install()` populates the registry but the
page will never see those credentials.

## async method: Credentials.create
* since: v1.61
- returns: <[Object]>
* alias: VirtualCredential
- `id` <[string]> Base64url-encoded credential id.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should playwright encode it itself?

- `rpId` <[string]> Relying party id.
- `userHandle` <[string]> Base64url-encoded user handle.
- `privateKey` <[string]> Base64url-encoded PKCS#8 (DER) private key.
- `publicKey` <[string]> Base64url-encoded SPKI (DER) public key.

Seeds a virtual WebAuthn credential. With only `rpId`, generates a fresh ECDSA P-256 keypair,
credential id and user handle. To import a pre-registered credential (e.g. authenticating as an
existing test user the server already knows about), supply all four of `id`, `userHandle`,
`privateKey` and `publicKey` together. Call [`method: Credentials.install`] before navigating to a
page that uses WebAuthn.

### param: Credentials.create.options
* since: v1.61
- `options` <[Object]>
- `rpId` <[string]> Relying party id (typically the site's effective domain).
- `id` ?<[string]> Base64url-encoded credential id. Auto-generated if omitted.
- `userHandle` ?<[string]> Base64url-encoded user handle. Auto-generated if omitted.
- `privateKey` ?<[string]> Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.
- `publicKey` ?<[string]> Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.

## async method: Credentials.delete
* since: v1.61

Removes a previously seeded credential.

### param: Credentials.delete.id
* since: v1.61
- `id` <[string]>

Base64url-encoded credential id.

## async method: Credentials.get
* since: v1.61
- returns: <[Array]<[Object]>>
* alias: VirtualCredential
- `id` <[string]>
- `rpId` <[string]>
- `userHandle` <[string]>
- `privateKey` <[string]>
- `publicKey` <[string]>

Returns seeded credentials, optionally filtered by `rpId` or `id`.

### option: Credentials.get.rpId
* since: v1.61
- `rpId` <[string]>

Only return credentials for this relying party id.

### option: Credentials.get.id
* since: v1.61
- `id` <[string]>

Only return the credential with this base64url-encoded id.

## async method: Credentials.setUserVerified
* since: v1.61

Toggles whether the virtual authenticator auto-approves user-verification prompts. Useful for
simulating a user denying biometric verification.

### param: Credentials.setUserVerified.value
* since: v1.61
- `value` <[boolean]>

`true` to auto-approve user verification (default), `false` to refuse.
20 changes: 20 additions & 0 deletions examples/webauthn/register-and-login.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { chromium } from 'playwright';

(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 250 });
const context = await browser.newContext();
await context.credentials.install();
const page = await context.newPage();
await page.goto('https://webauthn.io/');
await page.locator('#input-email').fill(`pw-demo-${Date.now()}`);
await page.locator('#register-button').click();
await page.getByText(/success!.*try to authenticate/i).waitFor();

const seeded = await context.credentials.get();
console.log(` ✓ registered: id=${seeded[0].id.substring(0, 12)}… rpId=${seeded[0].rpId}`);

await page.locator('#login-button').click();
await page.getByText(/you're logged in/i).waitFor();
console.log(` ✓ authenticated`);
await browser.close();
})();
39 changes: 39 additions & 0 deletions examples/webauthn/seed-credential.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Demonstrates `context.credentials.create()` — seeding a pre-existing
// credential into a fresh context.
//
// In a real test suite the keypair would live in a fixture file (saved once
// after registering a stable test user). Here we synthesise it by registering
// in context A, then importing into context B to authenticate without any
// further UI interaction.

import { chromium } from 'playwright';

(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 250 });
const username = `pw-demo-${Date.now()}`;

// Context A: register so webauthn.io stores the credential server-side.
const contextA = await browser.newContext();
await contextA.credentials.install();
const pageA = await contextA.newPage();
await pageA.goto('https://webauthn.io/');
await pageA.locator('#input-email').fill(username);
await pageA.locator('#register-button').click();
await pageA.getByText(/success!.*try to authenticate/i).waitFor();
const [registered] = await contextA.credentials.get();
await contextA.close();
console.log(` ✓ registered ${username}: id=${registered.id.substring(0, 12)}…`);

// Context B: seed the same credential. webauthn.io's home page issues a
// discoverable `navigator.credentials.get()` on load — the seeded credential
// satisfies it and the site logs us in with no clicks needed.
const contextB = await browser.newContext();
await contextB.credentials.install();
await contextB.credentials.create(registered);
const pageB = await contextB.newPage();
await pageB.goto('https://webauthn.io/');
await pageB.getByText(/you're logged in/i).waitFor();
console.log(` ✓ authenticated with seeded credential (no UI)`);

await browser.close();
})();
41 changes: 41 additions & 0 deletions examples/webauthn/user-verification.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Demonstrates `context.credentials.setUserVerified()` — simulating a user
// refusing biometric verification (e.g. a wrong fingerprint).
//
// We configure webauthn.io to require user verification, then flip the
// authenticator's UV flag off and try to log in. The relying party sees
// UV=0 in the assertion flags and rejects the login. Flipping UV back on
// makes the next attempt succeed.

import { chromium } from 'playwright';

(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 250 });
const context = await browser.newContext();
await context.credentials.install();
const page = await context.newPage();
await page.goto('https://webauthn.io/');

// Force the relying party to require user verification at both ends.
await page.locator('button:has-text("Advanced Settings")').click();
await page.locator('#optRegUserVerification').selectOption('required');
await page.locator('#optAuthUserVerification').selectOption('required');

await page.locator('#input-email').fill(`pw-demo-${Date.now()}`);
await page.locator('#register-button').click();
await page.getByText(/success!.*try to authenticate/i).waitFor();
console.log(` ✓ registered (UV=required)`);

// Simulate failed biometric — UV bit will be 0 in the next assertion.
await context.credentials.setUserVerified(false);
await page.locator('#login-button').click();
await page.getByText(/authentication failed/i).waitFor();
console.log(` ✓ login rejected: server got UV=0 but required UV=1`);

// Recovery: biometric succeeds, UV=1 in the assertion.
await context.credentials.setUserVerified(true);
await page.locator('#login-button').click();
await page.getByText(/you're logged in/i).waitFor();
console.log(` ✓ login succeeded after UV restored`);

await browser.close();
})();
Loading
Loading