Skip to content
Open
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
14 changes: 12 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,20 @@ 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`].

## 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`]).

**Usage**

Expand Down
21 changes: 10 additions & 11 deletions docs/src/api/class-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ There are two 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
- **Capture a passkey, then reuse it.** Let the app register a passkey once in a setup test and save
it as part of the [storage state](../auth.md) by passing `credentials: true` to
[`method: BrowserContext.storageState`]. When that state is supplied to a later context — via the
[`option: Browser.newContext.storageState`] option or [`method: BrowserContext.setStorageState`] —
the passkey is re-seeded and the authenticator installed automatically, the same way
[`method: BrowserContext.storageState`] reuses signed-in state. See the second example below.

**Usage: seed a known credential**
Expand All @@ -37,25 +40,21 @@ await page.goto('https://example.com/login');
**Usage: capture a passkey, 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();

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));
// The saved state includes the registered passkey, along with its private key.
await context.storageState({ path: 'playwright/.auth/state.json', credentials: true });
```

```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();
// later test: the captured passkey is re-seeded and the authenticator installed automatically.
const context = await browser.newContext({ storageState: 'playwright/.auth/state.json' });

const page = await context.newPage();
await page.goto('https://example.com/login');
Expand Down
75 changes: 3 additions & 72 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,75 +397,6 @@ 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 Expand Up @@ -657,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: 20 additions & 12 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9571,7 +9571,9 @@ 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)).
*
* **Usage**
*
Expand Down Expand Up @@ -9636,11 +9638,21 @@ 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).
*/
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 @@ -18857,25 +18869,21 @@ export interface Coverage {
* **Usage: capture a passkey, 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();
*
* 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));
* // The saved state includes the registered passkey, along with its private key.
* await context.storageState({ path: 'playwright/.auth/state.json', credentials: true });
* ```
*
* ```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();
* // later test: the captured passkey is re-seeded and the authenticator installed automatically.
* const context = await browser.newContext({ storageState: 'playwright/.auth/state.json' });
*
* const page = await context.newPage();
* await page.goto('https://example.com/login');
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 @@ -457,8 +457,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
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
15 changes: 13 additions & 2 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
// Note: we only need to reset properties from the "paramsThatAllowContextReuse" list.
// All other properties force a new context.
await this.clock.uninstall(progress);
await this.credentials.dispose(progress);
await progress.race(this.setUserAgent(this._options.userAgent));
await progress.race(this.doUpdateDefaultEmulatedMedia());
await progress.race(this.doUpdateDefaultViewport());
Expand Down Expand Up @@ -610,11 +609,13 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
this._origins.add(origin);
}

async storageState(progress: Progress, indexedDB = false): Promise<channels.BrowserContextStorageStateResult> {
async storageState(progress: Progress, indexedDB = false, credentials = false): Promise<channels.BrowserContextStorageStateResult> {
const result: channels.BrowserContextStorageStateResult = {
cookies: await this.cookies(progress),
origins: []
};
if (credentials)
result.credentials = await progress.race(this.credentials.get());
const originsToSave = new Set(this._origins);

const collectScript = `(() => {
Expand Down Expand Up @@ -671,11 +672,21 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
if (mode !== 'initial') {
await progress.race(this.clearCache());
await progress.race(this.doClearCookies());
if (state?.credentials?.length)
this.credentials.clear();
else
await this.credentials.dispose(progress);
}

if (state?.cookies)
await progress.race(this.addCookies(state.cookies));

if (state?.credentials?.length) {
for (const credential of state.credentials)
await progress.race(this.credentials.create(credential));
await this.credentials.install(progress);
}

const newOrigins = new Map(state?.origins?.map(p => [p.origin, p]) || []);
const allOrigins = new Set([...this._origins, ...newOrigins.keys()]);
if (allOrigins.size) {
Expand Down
Loading
Loading