Web Authentication for Browser Automation: Cookies, Sessions, and Login Flows
Learn how to log in with Playwright, reuse storage state safely, handle sessionStorage, and isolate authenticated browser tests.
For Playwright browser automation, establish authentication by logging in through the real UI, wait for a stable authenticated condition, then save the browser context’s storage state and load it into later test contexts. This avoids repeating login in every test while preserving realistic browser authentication. Treat the saved state as a credential: it can contain cookies and headers that let someone impersonate the account.
This guide covers Playwright with TypeScript, plus Python and Node.js examples, isolated test sessions, sessionStorage, HTTP authentication, common failures, and safe state handling. For the APIs referenced here, see the official Playwright authentication guide and BrowserContext API.
1. Choose an authentication approach
| Approach | Use it when | Trade-off |
|---|---|---|
| Log in through the UI in a setup step | The login flow itself is under test, or the app needs a real browser interaction to establish state. | It exercises redirects and UI behavior, but takes time and can be affected by login changes. |
| Save and reuse storage state | Most tests need an authenticated user, and login itself is covered elsewhere. | Tests start faster, but saved credentials expire and must be protected. |
| Authenticate each test independently | Tests modify account data or need separate identities. | Better isolation; repeated login or setup adds cost and runtime. |
Playwright contexts are independent browser sessions. A separate context per test helps prevent cookies and local browser state from leaking between tests. Whether accounts can be shared depends on server-side behavior: tests that change shared data should use different accounts to avoid races. Some applications also bind authentication to a browser or device, so validate state reuse in each browser engine your suite supports.
2. Log in and save state with Playwright TypeScript
Install Playwright and its browsers in the project using the commands in the official installation guide. The following setup project logs in through the page, waits until the app is visibly authenticated, and stores cookies and browser storage in an ignored directory.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: { baseURL: 'https://app.example.com' },
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'chromium',
use: { browserName: 'chromium', storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
});
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import fs from 'node:fs';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_USER!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait for an authenticated condition after redirects and cookie-setting responses.
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page).toHaveURL(/dashboard/);
fs.mkdirSync('playwright/.auth', { recursive: true });
await page.context().storageState({ path: authFile, indexedDB: true });
});
Use selectors and the final URL that match your application. Requiring both an authenticated UI element and an expected route can make the completion condition clearer; if your app legitimately returns elsewhere, assert its stable signed-in indicator instead. The storageState indexedDB option is available in current Playwright versions; check the API reference for version-specific details.
Add playwright/.auth/ to .gitignore. Do not print the state file or commit it, even to a private repository. Use environment secrets or your CI secret store for credentials, and restrict access to generated state files and CI artifacts.
3. Reuse the saved state in tests
// tests/account.spec.ts
import { test, expect } from '@playwright/test';
test('opens the authenticated account page', async ({ page }) => {
await page.goto('/account');
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
The project configuration loads the state into the test context. The setup project runs first when required by the project dependency. For tests that must verify login, sign-out, or unauthenticated access, define a project or test configuration without the saved state. Playwright documents this pattern as “Avoid authentication in some tests.”
Refresh state when sessions expire. A setup step can detect a failed login or missing authenticated indicator and fail clearly, instead of letting every downstream test fail with confusing authorization errors.
4. Python and Node.js alternatives
These examples show the same UI-login-then-save workflow using the Playwright language APIs. Replace the sample domain, selectors, and expected signed-in condition with the application’s actual behavior.
Python
import os
from pathlib import Path
from playwright.sync_api import sync_playwright, expect
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
page = context.new_page()
page.goto('https://app.example.com/login')
page.get_by_label('Email').fill(os.environ['E2E_USER'])
page.get_by_label('Password').fill(os.environ['E2E_PASSWORD'])
page.get_by_role('button', name='Sign in').click()
expect(page.get_by_role('navigation')).to_be_visible()
expect(page).to_have_url(__import__('re').compile('dashboard'))
Path('playwright/.auth').mkdir(parents=True, exist_ok=True)
context.storage_state(path='playwright/.auth/user.json', indexed_db=True)
browser.close()
# A later run can load the saved state:
# browser.new_context(storage_state='playwright/.auth/user.json')
Node.js
import { chromium, expect } from '@playwright/test';
import fs from 'node:fs';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill(process.env.E2E_USER);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page).toHaveURL(/dashboard/);
fs.mkdirSync('playwright/.auth', { recursive: true });
await context.storageState({ path: 'playwright/.auth/user.json', indexedDB: true });
await browser.close();
// A later run can load it:
// const context = await browser.newContext({ storageState: 'playwright/.auth/user.json' });
Install the Python package or Node.js Playwright package according to the official setup documentation. Keep the language-specific APIs and package versions consistent with your project; option availability can vary by release.
5. Cookies, storage state, and sessionStorage
Cookies are sent according to their domain, path, security, and expiry rules. Local storage and IndexedDB are origin-scoped browser stores and are not automatically sent as request headers; application code may read them and attach credentials to API calls. Playwright storage state can represent cookies, local storage, IndexedDB, and passkey-related state. It does not include sessionStorage by default.
If the application genuinely relies on sessionStorage, persist it explicitly. This example serializes the current page’s sessionStorage and restores it before application scripts run. Restrict the injected origin to the application origin, and do not store secrets in a file that may be logged or committed.
// Save sessionStorage after authentication
const sessionStorageData = await page.evaluate(() => JSON.stringify(sessionStorage));
fs.writeFileSync('playwright/.auth/session.json', sessionStorageData);
// Restore it on a later page before app scripts run
const sessionData = JSON.parse(fs.readFileSync('playwright/.auth/session.json', 'utf8'));
await page.addInitScript(data => {
if (location.origin === 'https://app.example.com') {
for (const [key, value] of Object.entries(data)) sessionStorage.setItem(key, String(value));
}
}, sessionData);
await page.goto('https://app.example.com/dashboard');
This is an application-specific workaround: sessionStorage is scoped to a tab session and origin, so restoring it across navigations, popups, or multiple origins may need additional handling. Prefer the application’s supported authentication flow where possible.
6. HTTP authentication and direct cookie management
For sites protected by HTTP Basic or similar browser-level authentication, configure credentials on the browser context and scope them to the intended origin. Do not confuse HTTP authentication with an application login form.
const context = await browser.newContext({
httpCredentials: {
username: process.env.HTTP_USER!,
password: process.env.HTTP_PASSWORD!,
origin: 'https://staging.example.com',
},
});
When a test needs explicit cookie setup or inspection, the BrowserContext API provides cookie management. Prefer loading a state file generated by a real login when the app has complex redirects, CSRF checks, device binding, or several related cookies. Manually constructing cookies can omit attributes or server-side assumptions and lead to fragile tests.
7. Keep tests isolated and credentials safe
- Keep authentication files in a dedicated ignored directory and exclude them from source control and published CI artifacts.
- Restrict file and artifact access; avoid dumping cookies, authorization headers, passwords, or storage state in logs.
- Use separate accounts when parallel tests mutate account data or otherwise affect shared server-side state.
- Use a shared account only when tests can safely share its server state, and account for concurrent runs.
- Regenerate saved state when it expires or is revoked; do not assume a saved cookie stays valid indefinitely.
- Run login-flow tests separately from ordinary authenticated tests so that both the login behavior and the faster reuse path are covered.
For OAuth architecture and security recommendations, consult the IETF’s RFC 10017, OAuth 2.0 for Browser-Based Applications. It is a Best Current Practice published in August 2026. Do not infer token-storage rules from a summary; follow the RFC’s detailed threat model and the requirements of your application.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Tests redirect to the login page despite loading state | The state expired, was saved before the redirect chain finished, or belongs to another origin. | Wait for a final authenticated URL or UI marker before saving. Regenerate state and verify the app origin and storage file path. |
| Login click succeeds but state is empty | The click was treated as completion before asynchronous navigation or cookie-setting responses finished. | Wait for the stable signed-in condition after the redirect, then call storageState. |
| sessionStorage values are missing after restoring state | Playwright’s standard storage-state file does not persist sessionStorage. | Use an explicit origin-scoped save/restore script only if the application depends on it. |
| Parallel tests log one another out or overwrite data | They share an account whose server-side state is mutable. | Allocate a separate account per worker or test where isolation is required. |
| State works in Chromium but not another engine | The application’s authentication may depend on browser-specific behavior, or state may be bound to the original context. | Run setup for each required engine and verify its own saved state against an authenticated condition. |
| HTTP auth credentials are ignored or sent unexpectedly | Credentials are configured at the wrong layer or not scoped to the intended origin. | Use context httpCredentials for HTTP authentication and set its origin where possible; use the app’s UI flow for form-based login. |
| CI works once, then fails on a later run | State was short-lived, revoked, or tied to an expired session. | Run the setup project on each CI run or refresh state through a controlled secret-backed process. Avoid retaining it as a broadly accessible artifact. |
9. Runtime, reliability, and cost considerations
Reusing state removes repeated login steps from tests, but it adds a lifecycle: setup must succeed, state must remain protected, and expired sessions must be refreshed. A dedicated setup project makes that dependency visible. UI login is more representative when login behavior matters; state reuse is practical for tests focused on post-login features.
Parallelism is safe only when both browser context state and server-side account data are isolated enough for the tests. Independent contexts isolate browser cookies and storage, but they do not create separate server accounts. For reliability, assert a meaningful authenticated condition after setup and keep a smaller set of explicit login tests that exercise the full flow.
Or skip the browser setup
If your task is to capture a page rather than test its authenticated behavior, ScreenshotNeo provides a one-call website screenshot API. It does not replace authenticated test setup for pages that require your own login cookies. A basic capture is:
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}`);
See the ScreenshotNeo API documentation for request options. Cookie banners are accepted and removed before the shot, along with known newsletter popups and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses report the page verdict and billing status. Its MCP server lets AI agents call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Should every test log in through the UI?
No. Cover the login flow in dedicated tests, then reuse saved state for tests whose purpose is the authenticated feature.
Can multiple browser contexts use the same state file?
Yes, each can load the state to start with equivalent browser authentication. They remain separate browser contexts, but may still interfere through shared server-side account data.
Does storage state contain passwords?
It is intended to preserve authenticated browser state, such as cookies and storage, rather than the original password. Those artifacts can still grant account access and must be protected like credentials.


