ScreenshotNeo

BlogGuides

Web Authentication for Browser Automation: A Practical Guide

Choose the right authentication strategy for browser tests: exercise login, reuse saved state, or design OAuth security for a browser app.

By the ScreenshotNeo team4 October 202610 min read

For browser automation, first decide which of three jobs you need to do: test the login interface, start application tests in an already authenticated state, or choose a secure OAuth architecture for an application that runs in a browser. Use a real login flow when the login experience itself is under test; reuse saved browser state when tests only need to start signed in; and treat OAuth design as a separate security decision. Playwright supports saving and reusing authentication state, but that state is sensitive and shared accounts can cause conflicts when parallel tests modify the same server-side data. Playwright authentication guidance

Choose the authentication approach

Your goal Recommended approach Key consideration
Verify login, logout, validation, or recovery behavior Exercise the relevant UI flow in a dedicated test Keep login coverage focused; do not make every unrelated test repeat it.
Test signed-in application features Authenticate in a setup project, save storage state, and load it into isolated test contexts Use a shared account only when tests do not interfere with its server-side data.
Run parallel tests that modify overlapping data Provision separate accounts or isolated data per worker Independent browser contexts do not isolate server-side account state.
Secure an application’s OAuth flow Follow browser-app security guidance; assess Authorization Code with PKCE and a Backend-for-Frontend (BFF) This is application architecture, not a browser-test session setup.

These choices are not interchangeable. A saved test session says how automation starts authenticated; it does not establish that the application’s OAuth design is secure. RFC 10017, dated August 2026, recommends Authorization Code with PKCE, rejects the Implicit flow, and asks implementers to consider a BFF. Browser code cannot securely keep a client secret. RFC 10017: OAuth 2.0 for Browser-Based Applications

Reuse authenticated state with Playwright

The following JavaScript example uses Playwright Test. It authenticates once in a setup project, writes state to playwright/.auth/user.json, then loads that state into tests that need a signed-in user. It assumes the application exposes a test login page with email and password fields and a submit button; adapt the locators and success condition to your app. Use a dedicated test account and credentials supplied through the environment.

1. Keep credentials and state out of source control

Add these entries to .gitignore:

playwright/.auth/
.env

Create the auth directory before the setup test runs. The saved state file can contain cookies and headers that impersonate the account. Playwright explicitly warns that it may contain sensitive cookies and headers. Do not commit it, publish it in broadly accessible CI artifacts, or copy it to shared locations without access controls. Playwright: introduction to authentication

2. Configure a setup project and dependent tests

In playwright.config.ts:

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

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
      testIgnore: /auth\.setup\.ts/,
    },
  ],
});

Playwright recommends a setup project when tests can safely share one account’s state. Tests still use isolated, non-persistent browser contexts; the saved state seeds those contexts. Playwright: shared account in all tests

3. Authenticate and save storage state

Create tests/auth.setup.ts:

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

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

setup('authenticate', async ({ page }) => {
  const email = process.env.E2E_USERNAME;
  const password = process.env.E2E_PASSWORD;
  if (!email || !password) {
    throw new Error('Set E2E_USERNAME and E2E_PASSWORD before running the setup.');
  }

  await fs.mkdir('playwright/.auth', { recursive: true });
  await page.goto(process.env.E2E_LOGIN_URL ?? 'http://127.0.0.1:3000/login');
  await page.getByLabel('Email').fill(email);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Replace this with an app-specific assertion that proves login completed.
  await expect(page.getByTestId('account-menu')).toBeVisible();
  await page.context().storageState({ path: authFile });
});

The assertion matters: saving state immediately after clicking submit can persist an unauthenticated or partially authenticated session if the redirect or server exchange has not finished. Prefer an observable, app-specific signed-in condition over a fixed sleep.

4. Use the saved state in application tests

Create or adapt a normal test, for example tests/dashboard.spec.ts:

import { test, expect } from '@playwright/test';

test('signed-in user can open the dashboard', async ({ page }) => {
  await page.goto('http://127.0.0.1:3000/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Run the suite with the project’s installed Playwright Test command, such as npx playwright test. Ensure the application is available at the configured URL and that the setup project can reach its login page.

When shared state is unsafe

Saved state is a good fit when tests read data or make changes that do not compete. It is unsafe to assume that separate browser contexts make a shared account safe for concurrent writes. If tests change overlapping server-side records, one test can affect another through the account even though each has its own browser context.

  • Give each parallel worker or test a distinct test account when it changes shared account data.
  • Alternatively, partition test data so workers operate on independent records, if the application and test design support that.
  • Use a shared account for read-only or non-conflicting tests, and keep tests that mutate common state serialized or isolated.
  • Do not reuse production user credentials or production accounts for automation.

Playwright’s authentication guide specifically recommends separate accounts for tests that run in parallel and modify shared server-side state. Playwright: one account per parallel worker

Identify which browser state your app uses

Before saving or restoring a session, determine what actually represents authentication in the application. Playwright storage state supports cookies and local storage, and can include IndexedDB when requested. Authentication may also involve WebAuthn/passkeys, while session storage has a separate lifecycle and is not automatically restored by the ordinary storage-state file.

Mechanism What to check Automation implication
Cookies Which domain and path set the session cookie? Is it session-only or persistent? Saved cookies are included in storage state; domain, expiry, and secure-cookie behavior can affect reuse.
Local storage Does the app store a token or session marker for its origin? Storage state can preserve local storage for origins represented in the saved state.
IndexedDB Does the app or identity layer persist relevant authentication material there? Check the Playwright version and storage-state API options for IndexedDB capture.
Session storage Does the app rely on tab-scoped, origin-specific state? It needs explicit save and restore handling; it is not covered by standard storage-state reuse.
WebAuthn/passkeys Does sign-in require a passkey ceremony or authenticator state? Model and test the required WebAuthn behavior deliberately; a cookie/local-storage snapshot alone may not represent it.

Keep the distinction between “the browser is signed in” and “the login method works” clear. A test using pre-authenticated state validates post-login behavior. It does not validate the login UI, identity-provider availability, or the full authentication ceremony. Playwright authentication guide

Exercise the login UI deliberately

Keep a smaller set of tests for the login experience itself: valid credentials, invalid credentials, required-field validation, logout, and any app-owned recovery flow that is in scope. These tests should assert user-visible outcomes and avoid depending on unrelated application state. For third-party sign-in, behavior and automation rules vary by provider and can change. The reviewed sources do not establish durable automation support for any particular provider, so do not make a provider-specific promise based on a generic browser setup guide.

Use a separate test identity and environment. If the sign-in process involves redirects or additional verification, follow the identity provider’s current rules and your organization’s test-account policies. Keep post-login application tests independent by using the saved-state setup for those tests.

OAuth security is an application design decision

If you are building a browser-based application, decide how OAuth tokens are issued, stored, and used separately from how browser tests authenticate. RFC 10017 recommends Authorization Code with PKCE for browser applications, rejects the Implicit grant, and advises considering a BFF so tokens can remain outside browser JavaScript. A browser application cannot protect a client secret because its code and configuration are available to the user’s browser. Review the RFC and your application’s threat model before selecting an architecture. RFC 10017

Do not treat a test-state file as an OAuth architecture pattern. It is a convenient test artifact containing credentials or credential-like material, and needs strict handling. Conversely, choosing a BFF does not remove the need to test your application’s sign-in and session behavior.

Run authenticated automation reliably

  • Make setup deterministic: wait for an app-specific logged-in signal before writing state; avoid arbitrary delays where a clear condition is available.
  • Control test data: create known fixtures and clean them up so the account’s state does not drift across runs.
  • Refresh expired state intentionally: rerun setup when sessions expire or credentials rotate; do not hide expiration by weakening assertions.
  • Keep browser contexts isolated: reuse the serialized authentication material while letting each test get its own context.
  • Limit artifact exposure: avoid uploading auth files with traces or reports; if artifacts are necessary, restrict access and retention.
  • Separate responsibilities: keep a focused login test suite and use authenticated-state setup for the broader post-login suite.

Performance depends on the login flow and application, but authenticating in setup avoids repeating that flow in every test. It also centralizes failures: if setup fails, the dependent tests cannot begin with valid state. A setup failure should be visible and diagnosable rather than silently replaced with stale state.

Troubleshooting

Symptom Likely cause Fix
Tests redirect to the login page The auth setup did not finish, saved the wrong state, or the session expired. Assert the signed-in condition before saving; inspect the configured auth file path and rerun setup.
Setup cannot find a field or button The example locators do not match the application’s accessible labels or roles. Use the app’s real labels and roles, or a stable test identifier; do not copy the placeholder login page assumptions unchanged.
Works locally but fails in CI Missing credentials, unreachable base URL, different callback configuration, or protected auth state unavailable to the job. Set CI secrets and URLs explicitly, confirm the app is ready before setup, and ensure the job can read the generated state without exposing it in logs or artifacts.
Parallel tests intermittently fail or see unexpected data Workers share one server-side account and mutate overlapping state. Use separate accounts or partition data; serialize conflicting tests if isolation is unavailable.
Cookies appear absent or ineffective Cookie scope, domain, expiry, or secure-context assumptions differ from the test URL. Verify the application origin and cookie attributes, then authenticate against the same expected host and scheme.
Login is restored but the app still considers the user signed out Authentication uses IndexedDB, session storage, passkeys, or another mechanism not represented by the captured state. Identify the actual mechanism; enable supported IndexedDB capture where applicable and implement explicit session-storage handling if needed.
A state file is missing on a later run It is ignored by Git as intended, and the setup project was skipped or not run. Run the setup dependency in that job and generate state at runtime; do not commit the secret state file to fix the missing-file symptom.
External sign-in behaves differently or is blocked The provider’s current policy, verification, or security controls may not support the automation path. Do not assume provider-specific support. Use an approved test strategy and consult the provider’s current documentation and rules.

Or skip the browser setup

If the task is to capture a page, rather than verify an authenticated workflow, ScreenshotNeo is a website screenshot API and MCP server. Its one-call API returns an image or PDF; the example below captures a public page as WebP. See the ScreenshotNeo API documentation for request options. This does not replace testing an authenticated application flow.

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}`);
  • Cookie banners, popups, and chat widgets are removed before the shot.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers report the page verdict and billing status.
  • An MCP server lets AI agents use 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.

Sign up free for 1,000 screenshots a month, no card required.

FAQ

Should every end-to-end test log in through the UI?

No. Reserve UI login tests for login behavior; use saved authenticated state for tests whose purpose is the signed-in application.

Does Playwright storage state automatically include session storage?

No. Session storage needs explicit save and restore handling and is tied to an origin and browsing session lifecycle.

Can I safely reuse one account with multiple workers?

Only when their server-side changes do not conflict. For overlapping writes, give workers separate accounts or isolate their data.

Does saved state verify that my OAuth design is secure?

No. It is a test setup mechanism. Use the browser-app OAuth guidance in RFC 10017 for architecture decisions.