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
15 changes: 13 additions & 2 deletions docs/src/api/class-browsercontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -1549,7 +1549,7 @@ Whether to emulate network being offline for the browser context.
- `name` <[string]>
- `value` <[string]>

Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB snapshot and virtual WebAuthn credentials.

## async method: BrowserContext.storageState
* since: v1.8
Expand All @@ -1566,10 +1566,21 @@ Returns storage state for this browser context, contains current cookies, local
Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot.
If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this.

### option: BrowserContext.storageState.credentials
* since: v1.61
- `credentials` ?<boolean>

Set to `true` to include the context's virtual WebAuthn [`property: BrowserContext.credentials`] (passkeys) in the storage
state snapshot. The captured credentials carry their private keys, so they can be re-seeded into a later context via the
[`option: Browser.newContext.storageState`] option or [`method: BrowserContext.setStorageState`].
Note that restoring the storage state that contains credentials will automatically install the virtual WebAuthn authenticator (see [`method: Credentials.install`]), and prevent all real authenticators from working in this context.

## async method: BrowserContext.setStorageState
* since: v1.59

Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state.
Clears the existing cookies, local storage, IndexedDB entries and virtual WebAuthn credentials, and sets the new storage
state. When the storage state contains credentials, the virtual WebAuthn authenticator is installed (equivalent to
[`method: Credentials.install`]), preventing all real authenticators from working in this context.

**Usage**

Expand Down
15 changes: 10 additions & 5 deletions docs/src/api/class-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()`
ceremonies in the page, without a real authenticator or hardware security key.

There are two common ways to use it:
There are three common ways to use it:

- **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
Comment thread
dgozman marked this conversation as resolved.
[`method: BrowserContext.storageState`] reuses signed-in state. See the second example below.
read it back with [`method: Credentials.get`], and seed it into later tests. See the second example below.
- **Save credentials in the storage state, restore later.** Let the app register a passkey in a
setup test and save it as part of the storage state by setting [`option: BrowserContext.storageState.credentials`]. See [authentication guide](../auth.md) for examples.

**Usage: seed a known credential**

Expand Down Expand Up @@ -103,10 +104,10 @@ await page.GotoAsync("https://example.com/login");
// The page's navigator.credentials.get() is answered with the seeded passkey.
```

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

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

Expand Down Expand Up @@ -264,6 +265,10 @@ await page.GotoAsync("https://example.com/login");
// navigator.credentials.get() resolves the captured passkey — already signed in.
```

**Usage: save credentials in the storage state, restore later**

See [authentication guide](../auth.md) for examples of using saving and resotring the storage state.

**Defaults**

- The authenticator presents itself as a **platform** authenticator (`authenticatorAttachment` is
Expand Down
74 changes: 3 additions & 71 deletions docs/src/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,9 @@ existing authentication state instead.
Playwright provides a way to reuse the signed-in state in the tests. That way you can log
in only once and then skip the log in step for all of the tests.

Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) or in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage), in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), or as passkeys ([WebAuthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) credentials). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.

Cookies, local storage and IndexedDB state can be used across different browsers. They depend on your application's authentication model which may require some combination of cookies, local storage or IndexedDB.
Cookies, local storage, IndexedDB and virtual WebAuthn credentials (passkeys) can be used across different browsers. They depend on your application's authentication model which may require some combination of cookies, local storage, IndexedDB or passkeys.

The following code snippet retrieves state from an authenticated context and creates a new context with that state.

Expand Down Expand Up @@ -397,74 +397,6 @@ export const test = baseTest.extend<{}, { workerStorageState: string }>({
});
```

### Passkeys (WebAuthn)
Comment thread
dgozman marked this conversation as resolved.
* 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('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.rpId, 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 Expand Up @@ -656,7 +588,7 @@ test('admin and user', async ({ adminPage, userPage }) => {

### Session storage

Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage), [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) and passkey ([WebAuthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.

```js
// Get session storage and store as env variable
Expand Down
32 changes: 26 additions & 6 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9635,7 +9635,10 @@ export interface BrowserContext {
setOffline(offline: boolean): Promise<void>;

/**
* Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state.
* Clears the existing cookies, local storage, IndexedDB entries and virtual WebAuthn credentials, and sets the new
* storage state. When the storage state contains credentials, the virtual WebAuthn authenticator is installed
* (equivalent to [credentials.install()](https://playwright.dev/docs/api/class-credentials#credentials-install)),
* preventing all real authenticators from working in this context.
*
* **Usage**
*
Expand Down Expand Up @@ -9700,11 +9703,24 @@ export interface BrowserContext {
}): Promise<void>;

/**
* Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB
* snapshot.
* Returns storage state for this browser context, contains current cookies, local storage snapshot, IndexedDB
* snapshot and virtual WebAuthn credentials.
* @param options
*/
storageState(options?: {
/**
* Set to `true` to include the context's virtual WebAuthn
* [browserContext.credentials](https://playwright.dev/docs/api/class-browsercontext#browser-context-credentials)
* (passkeys) in the storage state snapshot. The captured credentials carry their private keys, so they can be
* re-seeded into a later context via the
* [`storageState`](https://playwright.dev/docs/api/class-browser#browser-new-context-option-storage-state) option or
* [browserContext.setStorageState(storageState)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-storage-state).
* Note that restoring the storage state that contains credentials will automatically install the virtual WebAuthn
* authenticator (see [credentials.install()](https://playwright.dev/docs/api/class-credentials#credentials-install)),
* and prevent all real authenticators from working in this context.
*/
credentials?: boolean;

/**
* Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage
* state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication,
Expand Down Expand Up @@ -18888,7 +18904,7 @@ export interface Coverage {
* `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies in the page, without a real
* authenticator or hardware security key.
*
* There are two common ways to use it:
* There are three common ways to use it:
*
* **Usage: seed a known credential**
*
Expand All @@ -18909,10 +18925,10 @@ export interface Coverage {
* // The page's navigator.credentials.get() is answered with the seeded passkey.
* ```
*
* **Usage: capture a passkey, then reuse it**
* **Usage: capture a credential, then reuse it**
*
* ```js
* // setup test: let the app register a passkey, then save it.
* // setup test: let the app register a passkey, then save the storage state with it.
* const context = await browser.newContext();
* await context.credentials.install();
*
Expand All @@ -18937,6 +18953,10 @@ export interface Coverage {
* // navigator.credentials.get() resolves the captured passkey — already signed in.
* ```
*
* **Usage: save credentials in the storage state, restore later**
*
* See [authentication guide](https://playwright.dev/docs/auth) for examples of using saving and resotring the storage state.
*
* **Defaults**
*/
export interface Credentials {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
});
}

async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
async storageState(options: { path?: string, indexedDB?: boolean, credentials?: boolean } = {}): Promise<StorageState> {
const state = await this._channel.storageState({ indexedDB: options.indexedDB, credentials: options.credentials });
if (options.path) {
await mkdirIfNeeded(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
Expand Down
9 changes: 9 additions & 0 deletions packages/playwright-core/src/client/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ export type BrowserNewContextParams = {
storageState?: {
cookies?: SetNetworkCookie[],
origins?: SetOriginStorage[],
credentials?: VirtualCredential[],
},
};
export type BrowserNewContextOptions = {
Expand Down Expand Up @@ -1047,6 +1048,7 @@ export type BrowserNewContextOptions = {
storageState?: {
cookies?: SetNetworkCookie[],
origins?: SetOriginStorage[],
credentials?: VirtualCredential[],
},
};
export type BrowserNewContextResult = {
Expand Down Expand Up @@ -1124,6 +1126,7 @@ export type BrowserNewContextForReuseParams = {
storageState?: {
cookies?: SetNetworkCookie[],
origins?: SetOriginStorage[],
credentials?: VirtualCredential[],
},
};
export type BrowserNewContextForReuseOptions = {
Expand Down Expand Up @@ -1198,6 +1201,7 @@ export type BrowserNewContextForReuseOptions = {
storageState?: {
cookies?: SetNetworkCookie[],
origins?: SetOriginStorage[],
credentials?: VirtualCredential[],
},
};
export type BrowserNewContextForReuseResult = {
Expand Down Expand Up @@ -1594,24 +1598,29 @@ export type BrowserContextSetOfflineOptions = {
export type BrowserContextSetOfflineResult = void;
export type BrowserContextStorageStateParams = {
indexedDB?: boolean,
credentials?: boolean,
};
export type BrowserContextStorageStateOptions = {
indexedDB?: boolean,
credentials?: boolean,
};
export type BrowserContextStorageStateResult = {
cookies: NetworkCookie[],
origins: OriginStorage[],
credentials?: VirtualCredential[],
};
export type BrowserContextSetStorageStateParams = {
storageState?: {
cookies?: SetNetworkCookie[],
origins?: SetOriginStorage[],
credentials?: VirtualCredential[],
},
};
export type BrowserContextSetStorageStateOptions = {
storageState?: {
cookies?: SetNetworkCookie[],
origins?: SetOriginStorage[],
credentials?: VirtualCredential[],
},
};
export type BrowserContextSetStorageStateResult = void;
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright-core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export type StorageState = {
};
export type SetStorageState = {
cookies?: channels.SetNetworkCookie[],
origins?: (Omit<channels.SetOriginStorage, 'indexedDB'> & { indexedDB?: unknown[] })[]
origins?: (Omit<channels.SetOriginStorage, 'indexedDB'> & { indexedDB?: unknown[] })[],
credentials?: unknown[],
};

export type LifecycleEvent = channels.LifecycleEvent;
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ scheme.BrowserNewContextParams = tObject({
storageState: tOptional(tObject({
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
origins: tOptional(tArray(tType('SetOriginStorage'))),
credentials: tOptional(tArray(tType('VirtualCredential'))),
})),
});
scheme.BrowserNewContextResult = tObject({
Expand Down Expand Up @@ -585,6 +586,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
storageState: tOptional(tObject({
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
origins: tOptional(tArray(tType('SetOriginStorage'))),
credentials: tOptional(tArray(tType('VirtualCredential'))),
})),
});
scheme.BrowserNewContextForReuseResult = tObject({
Expand Down Expand Up @@ -845,15 +847,18 @@ scheme.BrowserContextSetOfflineParams = tObject({
scheme.BrowserContextSetOfflineResult = tOptional(tObject({}));
scheme.BrowserContextStorageStateParams = tObject({
indexedDB: tOptional(tBoolean),
credentials: tOptional(tBoolean),
});
scheme.BrowserContextStorageStateResult = tObject({
cookies: tArray(tType('NetworkCookie')),
origins: tArray(tType('OriginStorage')),
credentials: tOptional(tArray(tType('VirtualCredential'))),
});
scheme.BrowserContextSetStorageStateParams = tObject({
storageState: tOptional(tObject({
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
origins: tOptional(tArray(tType('SetOriginStorage'))),
credentials: tOptional(tArray(tType('VirtualCredential'))),
})),
});
scheme.BrowserContextSetStorageStateResult = tOptional(tObject({}));
Expand Down
Loading
Loading