Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use Playwright’s storageState for portable login reuse, and a persistent browser profile only when the browser itself must survive a restart. First identify where your application stores authentication data—cookies, localStorage, IndexedDB, origin-private data, sessionStorage, or WebAuthn credentials. Save only the stores you actually use, keep the resulting files secret, and automate MFA with a dedicated test account, virtual WebAuthn credentials, or tightly controlled OTP fixtures. Never disable production MFA or copy production credentials into tests.

Choose the persistence boundary first

“Stay logged in” can mean two different things. A Playwright storageState snapshot lets a new, isolated browser context start authenticated. A persistent profile writes the browser profile to disk so the browser can reopen with its cookies and other profile data after a restart.

Approach Survives browser restart Portable to workers or another machine Best fit Main risk
In-memory context No; it ends when the browser closes No One short-lived test or exploratory run Every run must authenticate again
storageState The snapshot can be reused after a restart Yes, when copied securely Parallel tests, CI, and repeatable suites The file can contain cookies, headers, tokens, or private WebAuthn keys
Persistent profile Yes Usually no; profile locking and machine-specific data complicate sharing Desktop-like workflows that require the same browser profile State leakage between tests and profile corruption or locking

Authentication is distributed. A site may use an HTTP cookie for the session, a bearer token in localStorage, an IndexedDB record, origin-private file-system data, or a passkey. Observe the post-login context and capture the stores your application actually uses rather than assuming cookies are sufficient.

Create a reusable Playwright authentication state

1. Log in once in a setup project

Keep authentication outside individual tests. The setup project below writes one state file for a test identity. Create the directory first and keep it out of source control.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { test as setup, expect } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page, context }) => {
  await page.goto('https://app.example.test/login');
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();

  // Include IndexedDB when this application stores its token there.
  await context.storageState({ path: authFile, indexedDB: true });
});

The indexedDB option is useful only when your application depends on IndexedDB. If the site uses only cookies and localStorage, a normal storageState({ path }) snapshot is smaller and easier to reason about. Register any passkey through the normal enrollment flow in this isolated test account before taking the snapshot.

2. Make tests consume the snapshot

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'setup',
      testMatch: /.*.setup.js/
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json'
      },
      dependencies: ['setup']
    }
  ]
});
import { test, expect } from '@playwright/test';

test('opens an authenticated page', async ({ page }) => {
  await page.goto('https://app.example.test/account');
  await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});

Use a separate state file for each identity. If parallel workers mutate the same account, generate one state per worker and assign separate test data. This prevents one test from revoking, refreshing, or changing another test’s session.

3. Protect the state file

Authentication state is equivalent to a credential. Playwright warns that a state file may contain sensitive cookies and headers that can impersonate the account. Add playwright/.auth/ to .gitignore, restrict filesystem permissions, encrypt any backup, and regenerate the file after expiration or suspected exposure. Do not print its JSON in CI logs, upload it as an unrestricted artifact, or commit it to a repository.

When a persistent profile is the right tool

Use launchPersistentContext when the browser profile itself must survive closing and reopening—for example, a desktop workflow that depends on profile preferences or browser-managed data. Give each test identity its own directory and never point automated tests at your everyday profile.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { chromium } from '@playwright/test';

const context = await chromium.launchPersistentContext('./pw-profile-test-user', {
  headless: false
});
const page = await context.newPage();
await page.goto('https://app.example.test/account');
// Run the workflow, then close the context when finished.
await context.close();

A persistent profile is not a substitute for isolation. Do not run two processes against the same profile directory, and do not reuse a profile across unrelated accounts. For CI and most parallel suites, a setup project plus storageState gives clearer boundaries and easier cleanup.

Handle sessionStorage deliberately

sessionStorage is scoped to an origin and is not persisted across page loads by Playwright. There is no direct Playwright persistence API for it. If your application genuinely stores authentication data there, serialize it after login and install an initialization script before the application’s first page load.

import { test as setup } from '@playwright/test';
import fs from 'node:fs/promises';

const authFile = 'playwright/.auth/user.json';
const sessionFile = 'playwright/.auth/session-storage.json';

setup('capture session storage', async ({ page, context }) => {
  await page.goto('https://app.example.test/login');
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL);
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.waitForURL('**/account');

  const values = await page.evaluate(() => {
    const result = {};
    for (let i = 0; i < sessionStorage.length; i++) {
      const key = sessionStorage.key(i);
      result[key] = sessionStorage.getItem(key);
    }
    return result;
  });
  await fs.writeFile(sessionFile, JSON.stringify(values), { mode: 0o600 });
  await context.storageState({ path: authFile, indexedDB: true });
});
import { test } from '@playwright/test';
import fs from 'node:fs';

const sessionValues = JSON.parse(
  fs.readFileSync('playwright/.auth/session-storage.json', 'utf8')
);

test.beforeEach(async ({ context }) => {
  await context.addInitScript(values => {
    for (const [key, value] of Object.entries(values)) {
      window.sessionStorage.setItem(key, value);
    }
  }, sessionValues);
});

Inject only the keys the application requires. Copying every entry can restore stale wizard steps, feature flags, or one-time workflow data and make failures appear random.

Automate MFA without weakening it

WebAuthn and passkeys

For a WebAuthn factor, create a dedicated test account and enroll a virtual credential through the same registration flow a real user follows. Keep that credential inside the isolated test environment, then capture it in the controlled browser state if your Playwright version and browser support WebAuthn credential snapshots. Browser-context credential snapshots contain private keys; restoring one installs a virtual authenticator in that context, and real authenticators do not work there.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Microsoft Edge DevTools provides a software virtual authenticator for registration and debugging without a physical key. Use it only for test identities. Your test should still exercise challenge creation, origin checks, user verification requirements, credential registration, sign-in, and credential removal. Do not export a production passkey or turn off MFA merely to make automation easier.

TOTP and other one-time codes

Store the test account’s OTP seed in a secret manager available only to the test worker. Generate a code at run time and submit it through the normal MFA form; never hard-code the seed in a fixture or commit it with the test. Test the security properties, not just the happy path:

  • Short expiration time and rejection after expiry.
  • Single use: a code accepted once must fail on replay.
  • Strict attempt limits, rate limiting, and account or IP lockout.
  • Invalidation after successful verification.
  • Consistent enforcement on web, API, federated-login, and account-recovery paths.
  • No OTP values in application, test, proxy, or CI logs, and no long-term plaintext storage.

FIDO2/WebAuthn is preferable when phishing resistance is part of the threat model because the credential is bound to the legitimate origin. OTP remains useful for testing fallback and recovery paths, but it does not provide the same origin binding.

Reliability and parallel-run practices

  • Refresh deliberately: regenerate state when sessions expire instead of retrying a stale file indefinitely.
  • Use deterministic identities: one account per worker when tests change server-side state.
  • Wait on application signals: prefer a visible post-login element or URL over a fixed sleep.
  • Capture required stores only: unnecessary IndexedDB or sessionStorage data increases size and leakage risk.
  • Keep MFA enrollment separate: enroll virtual credentials once in a controlled setup, then test sign-in and revocation independently.
  • Clean up: remove temporary profiles and expired state files after a run.

A saved state avoids repeated login latency, but it does not make a session permanent. Server-side expiry, revocation, password changes, device policies, and MFA resets can invalidate it at any time.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting common failures

The test is redirected to login

Inspect the post-login context. The application may keep its token in IndexedDB, sessionStorage, or origin-private data rather than cookies or localStorage. Capture the missing store, confirm the state file is loaded by the intended project, and verify that the test origin exactly matches the origin used during login.

State works locally but not in CI

Check filesystem permissions, working-directory differences, environment variables, clock skew, and whether the server binds sessions to IP, device, or user agent. Generate state inside the CI job instead of copying a long-lived file between environments.

Parallel tests log one another out

They are sharing an account or profile. Allocate a state file and server-side identity per worker, or serialize the conflicting tests. Never share one persistent-profile directory between processes.

SessionStorage values disappear

They were injected too late or under the wrong origin. Register addInitScript before navigation and restrict the script to the exact origin that reads the values.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

WebAuthn restoration fails

Confirm that the credential was created for the same test relying party and origin, that the browser context supports the virtual credential snapshot, and that no physical authenticator is expected in a context restored with virtual credentials.

OTP tests pass even when security is broken

Add explicit cases for expiry, replay, excessive attempts, lockout, rate limits, and recovery endpoints. Verify that logs and traces redact the code and seed.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If the goal is to obtain a clean image or PDF of a page rather than drive an interactive login, ScreenshotNeo accepts custom cookies, headers, user agents, and Authorization values in one API request. It removes cookie-consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for authentication and options. A basic request is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. Create a free ScreenshotNeo account when you want the capture handled without maintaining a browser profile.

Best Value
Sale
The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws
  • Comes with secure packaging
  • It can be a gift item
  • Easy to read text

FAQ

Can I reuse one state file for every test account?

No. State is identity-specific. Keep a separate snapshot for each account, and separate worker snapshots when tests can interfere with one another.

Does a persistent profile replace MFA enrollment?

No. It preserves profile data; it does not make an expired, revoked, or unenrolled factor valid. Enrollment and recovery still need dedicated test flows.

Should I choose OTP or WebAuthn for new tests?

Use WebAuthn when you need origin-bound, phishing-resistant authentication. Add OTP tests when fallback, recovery, or legacy compatibility is part of the product requirements.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I reuse one state file for every test account?

No. State is identity-specific. Keep a separate snapshot for each account, and separate worker snapshots when tests can interfere with one another.

Does a persistent profile replace MFA enrollment?

No. It preserves profile data; it does not make an expired, revoked, or unenrolled factor valid. Enrollment and recovery still need dedicated test flows.

Should I choose OTP or WebAuthn for new tests?

Use WebAuthn when you need origin-bound, phishing-resistant authentication. Add OTP tests when fallback, recovery, or legacy compatibility is part of the product requirements.

Quick Recap

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.