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
109 changes: 82 additions & 27 deletions docs/src/api/class-credentials.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,77 @@
# 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.
`Credentials` is a virtual WebAuthn authenticator scoped to a [BrowserContext]. It lets tests
register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()`
ceremonies in the page, without a real authenticator or hardware security key.

Implemented in userland via an injected script, so it works across Chromium, Firefox and WebKit.
There are two common ways to use it:

**Usage**
- **Seed a known credential.** The passkey already exists — for example, your backend provisioned
it for a test user. Import it with [`method: Credentials.create`] so the app under test can sign
in right away. See the first example below.
- **Capture a credential, then reuse it.** Let the app register a passkey once in a setup test,
read it back with [`method: Credentials.get`], and seed it into later tests — the same way
[`method: BrowserContext.storageState`] reuses signed-in state. See the second example below.

**Usage: seed a known credential**

```js
const context = await browser.newContext();

// A passkey your backend already provisioned for a test user.
await context.credentials.create({
rpId: 'example.com',
id: knownCredentialId, // base64url
userHandle: knownUserHandle, // base64url
privateKey: knownPrivateKey, // base64url PKCS#8 (DER)
publicKey: knownPublicKey, // base64url SPKI (DER)
});
await context.credentials.install();

const page = await context.newPage();
await page.goto('https://example.com/login');
// The page's navigator.credentials.get() is answered with the seeded passkey.
```

**Usage: capture a passkey, then reuse it**

```js
// setup test: let the app register a passkey, then save it.
const context = await browser.newContext();
await context.credentials.install();

const page = await context.newPage();
await page.goto('https://example.com/register');
await page.getByRole('button', { name: 'Create a passkey' }).click();

// Read back the passkey the page registered — it includes the private key.
const [credential] = await context.credentials.get({ rpId: 'example.com' });
fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));
```

```js
// later test: seed the captured passkey so the app starts already enrolled.
const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
const context = await browser.newContext();
await context.credentials.create(credential);
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.
// navigator.credentials.get() resolves the captured passkey — already signed in.
```

**Defaults**

- The authenticator presents itself as a **platform** authenticator (`authenticatorAttachment` is
`'platform'`), and `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` resolves
to `true` in the page.
- Seeded credentials are **discoverable** (resident), so both username-then-passkey and
usernameless passkey flows resolve them.
- Fresh keys are ECDSA P-256 (COSE algorithm `-7`). An omitted `id` or `userHandle` is
filled with 16 random bytes.

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

Expand All @@ -27,7 +81,7 @@ 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
[`method: Credentials.create`] without `install()` populates the authenticator, but the
page will never see those credentials.

## async method: Credentials.create
Expand All @@ -40,11 +94,17 @@ page will never see those credentials.
- `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.
Seeds a virtual WebAuthn credential and returns it.

With only `rpId`, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The
seeded credential is discoverable (resident), so the page can resolve it from both
username-then-passkey and usernameless passkey flows. The returned object carries the `privateKey` and `publicKey`, so
it can be persisted to disk and re-seeded in a later test.

To **import a known credential**, 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
Expand All @@ -58,7 +118,9 @@ page that uses WebAuthn.
## async method: Credentials.delete
* since: v1.61

Removes a previously seeded credential.
Removes a credential from the authenticator by its id. Works for any credential currently held —
both those seeded with [`method: Credentials.create`] and those the page registered itself by
calling `navigator.credentials.create()`.

### param: Credentials.delete.id
* since: v1.61
Expand All @@ -76,7 +138,12 @@ Base64url-encoded credential id.
- `privateKey` <[string]>
- `publicKey` <[string]>

Returns seeded credentials, optionally filtered by `rpId` or `id`.
Returns every credential currently held by the authenticator, optionally filtered by `rpId` or
`id`. This includes both credentials seeded with [`method: Credentials.create`] and credentials
the page registered itself by calling `navigator.credentials.create()`.

Each returned credential includes its `privateKey` and `publicKey`, so a passkey the app just
registered can be saved and re-seeded into a later test with [`method: Credentials.create`] — see the second example in the class overview.

### option: Credentials.get.rpId
* since: v1.61
Expand All @@ -89,15 +156,3 @@ Only return credentials for this relying party id.
- `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.
69 changes: 69 additions & 0 deletions docs/src/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,75 @@ export const test = baseTest.extend<{}, { workerStorageState: string }>({
});
```

### Passkeys (WebAuthn)
* langs: js

**When to use**
- Your app signs users in with passkeys (WebAuthn), and you want tests to start already enrolled.

**Details**

[`property: BrowserContext.credentials`] is a virtual WebAuthn authenticator. Unlike cookie or local storage state, a passkey is seeded **imperatively** with [`method: Credentials.create`] and [`method: Credentials.install`], so it lives in a [`context` fixture override](./test-fixtures.md#overriding-fixtures) rather than in the `storageState` config option.

If your backend already provisioned a passkey for the test user, seed it directly — no setup project required:

```js title="playwright/fixtures.ts"
import { test as baseTest } from '@playwright/test';
export * from '@playwright/test';

export const test = baseTest.extend({
context: async ({ context }, use) => {
// A passkey your backend provisioned for the test user.
await context.credentials.create({
rpId: 'example.com',
id: process.env.PASSKEY_ID,
userHandle: process.env.PASSKEY_USER_HANDLE,
privateKey: process.env.PASSKEY_PRIVATE_KEY,
publicKey: process.env.PASSKEY_PUBLIC_KEY,
});
await context.credentials.install();
await use(context);
},
});
```

Otherwise, let the app register a passkey once in a [setup project](#basic-shared-account-in-all-tests), capture it with [`method: Credentials.get`], and save it to disk:

```js title="tests/passkey.setup.ts"
import { test as setup } from '@playwright/test';
import fs from 'fs';

setup('enroll passkey', async ({ context, page }) => {
await context.credentials.install();
await page.goto('https://example.com/register');
// The app calls navigator.credentials.create() to register the passkey.
await page.getByRole('button', { name: 'Create a passkey' }).click();

// Read back the registered passkey, including its private key, and save it.
const [credential] = await context.credentials.get({ rpId: 'example.com' });
fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));
});
```

Then seed the captured passkey into every test's context:

```js title="playwright/fixtures.ts"
import { test as baseTest } from '@playwright/test';
import fs from 'fs';
export * from '@playwright/test';

export const test = baseTest.extend({
context: async ({ context }, use) => {
const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
await context.credentials.create(credential);
await context.credentials.install();
await use(context);
},
});
```

Declare the `setup` project as a [dependency](./test-projects.md#dependencies) of your testing projects, just like in the [basic flow](#basic-shared-account-in-all-tests). The saved `passkey.json` contains a private key, so keep it under `playwright/.auth` and out of source control (see [Core concepts](#core-concepts)).

### Multiple signed in roles
* langs: js

Expand Down
24 changes: 0 additions & 24 deletions docs/src/intro-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,12 @@ Playwright recommends using the official [Playwright Pytest plugin](./test-runne

Get started by installing Playwright and running the example test to see it in action.

<Tabs
groupId="package-managers"
defaultValue="pypi"
values={[
{label: 'PyPI', value: 'pypi'},
{label: 'Anaconda', value: 'anaconda'}
]
}>
<TabItem value="pypi">

Install the [Pytest plugin](https://pypi.org/project/pytest-playwright/):

```bash
pip install pytest-playwright
```

</TabItem>
<TabItem value="anaconda">

Install the [Pytest plugin](https://anaconda.org/Microsoft/pytest-playwright):

```bash
conda config --add channels conda-forge
conda config --add channels microsoft
conda install pytest-playwright
```

</TabItem>
</Tabs>

Install the required browsers:

```bash
Expand Down
13 changes: 0 additions & 13 deletions docs/src/library-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ title: "Getting started - Library"

## Installation

### Pip

[<img src="https://badge.fury.io/py/playwright.svg" alt="PyPI version" width="132" height="20" />](https://pypi.python.org/pypi/playwright/)

```bash
Expand All @@ -15,17 +13,6 @@ pip install playwright
playwright install
```

### Conda

[<img src="https://img.shields.io/conda/v/microsoft/playwright" alt="Anaconda version" width="160" height="20" />](https://anaconda.org/Microsoft/playwright)

```bash
conda config --add channels conda-forge
conda config --add channels microsoft
conda install playwright
playwright install
```

These commands download the Playwright package and install browser binaries for Chromium, Firefox and WebKit. To modify this behavior see [installation parameters](./browsers.md#install-browsers).

## Usage
Expand Down
41 changes: 0 additions & 41 deletions examples/webauthn/user-verification.mjs

This file was deleted.

12 changes: 7 additions & 5 deletions packages/isomorphic/manualPromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ export class LongStandingScope {
}

export function signalToPromise(signal: AbortSignal): { promise: Promise<void>, dispose: () => void } {
if (signal.aborted)
return { promise: Promise.resolve(), dispose: () => {} };
let dispose: (() => void) | undefined;
const promise = new Promise<void>(resolve => {
if (signal.aborted)
resolve();
else
signal.addEventListener('abort', () => resolve(), { once: true });
const onAbort = () => resolve();
signal.addEventListener('abort', onAbort, { once: true });
dispose = () => signal.removeEventListener('abort', onAbort);
});
return { promise, dispose: () => {} };
return { promise, dispose: dispose! };
}

function cloneError(error: Error, frames: string[]) {
Expand Down
9 changes: 4 additions & 5 deletions packages/isomorphic/protocolMetainfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,10 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
['BrowserContext.clockRunFor', { title: 'Run clock "{ticksNumber|ticksString}"', }],
['BrowserContext.clockSetFixedTime', { title: 'Set fixed time "{timeNumber|timeString}"', }],
['BrowserContext.clockSetSystemTime', { title: 'Set system time "{timeNumber|timeString}"', }],
['BrowserContext.credentialsInstall', { title: 'Install virtual WebAuthn authenticator', }],
['BrowserContext.credentialsCreate', { title: 'Create virtual credential for "{rpId}"', }],
['BrowserContext.credentialsGet', { title: 'Get virtual credentials', }],
['BrowserContext.credentialsDelete', { title: 'Delete virtual credential', }],
['BrowserContext.credentialsSetUserVerified', { title: 'Set virtual authenticator user verified', }],
['BrowserContext.credentialsInstall', { title: 'Install virtual WebAuthn authenticator', group: 'configuration', }],
['BrowserContext.credentialsCreate', { title: 'Create virtual credential for "{rpId}"', group: 'configuration', }],
['BrowserContext.credentialsGet', { title: 'Get virtual credentials', group: 'configuration', }],
['BrowserContext.credentialsDelete', { title: 'Delete virtual credential', group: 'configuration', }],
['BrowserType.launch', { title: 'Launch browser', }],
['BrowserType.launchPersistentContext', { title: 'Launch persistent context', }],
['BrowserType.connectOverCDP', { title: 'Connect over CDP', }],
Expand Down
Loading
Loading