Playwright · Authentication and state reuse

Test login once, reuse state safely

Build fast authentication suites by separating sign-in setup from role-specific tests, while still testing logout, lockout and expiry as first-class behavior.

1 · Global setup

Create state through one observable login

A setup project signs in before dependent browser projects begin. Wait for the visible identity result, then save storageState. The state includes the secure session cookie even though JavaScript cannot read that HttpOnly value. Keep generated state files ignored because even synthetic session tokens grant temporary access to the current sandbox.

Global setup and storageState
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('authenticate manager', async ({ page }) => {
  await page.goto('/practice/apps/auth/?delay=0');
  const login = page.locator('#login-form');
  await login.getByLabel('Username').fill('manager_user');
  await login.getByLabel('Password').fill('Practice@123');
  await page.getByRole('button', { name: 'Sign in to Practice' }).click();
  await expect(page.getByTestId('signed-in-user')).toContainText('manager_user');
  await page.context().storageState({ path: 'playwright/.auth/manager.json' });
});

2 · Per-role projects

Model permissions in Playwright configuration

Separate state files make the actor explicit. A Manager project can assert reports and ticket updates, while a Read-only project expects mutation controls to be disabled and protected API mutations to return 403. The same unauthenticated request should return 401, proving the application distinguishes identity from authorization.

Role-specific projects
projects: [
  { name: 'setup', testMatch: /auth[.]setup[.]ts/ },
  {
    name: 'manager',
    use: { storageState: 'playwright/.auth/manager.json' },
    dependencies: ['setup'],
  },
  {
    name: 'readonly',
    use: { storageState: 'playwright/.auth/readonly.json' },
    dependencies: ['setup'],
  },
]

3 · API login

Skip repeated UI login without skipping behavior

API login is useful when authentication is a prerequisite rather than the subject of a test. Create the isolated Lab session, include its CSRF token in the same-origin login mutation, and reuse the cookie jar when navigating the UI. Keep one focused UI login test so selectors, validation and visible error behavior remain covered.

API login then UI
const session = await request.get('/practice/api/v1/lab/session');
const csrf = (await session.json()).data.csrfToken;
const login = await request.post('/practice/api/v1/auth/login', {
  headers: { 'X-Lab-CSRF': csrf },
  data: { username: 'manager_user', password: 'Practice@123', idleSeconds: 300 },
});
expect(login.status()).toBe(200);
await page.goto('/practice/apps/auth/');
await expect(page.getByTestId('signed-in-user')).toContainText('manager_user');

4 · Logout and expiry

Assert server invalidation, not only navigation

Logout revokes the server-side session and clears its cookie. Prove it by requesting the identity endpoint after logout and expecting 401. Idle expiry is configurable from five seconds to thirty minutes, and the dedicated expiry control advances the fixture deterministically so the suite does not sleep. Bearer-token tests can request an already expired access token and assert TOKEN_EXPIRED immediately.

Deterministic session expiry
await page.getByRole('button', { name: 'Expire current session' }).click();
await page.getByRole('button', { name: 'Inspect session' }).click();
await expect(page.locator('#session-result')).toContainText('AUTH_SESSION_EXPIRED');

5 · Complete exercise

Cover identity, permissions and storage boundaries

  1. Sign in each public role and save a distinct storage state.
  2. Assert the Admin-only control is hidden for Customer and Manager.
  3. Assert the Read-only mutation control is disabled and the API returns 403.
  4. Call a protected endpoint without credentials and assert 401.
  5. Issue, refresh and expire bearer tokens; verify the access token lives in localStorage.
  6. Use the public demo API key once, then prove an invalid key returns 401.
  7. Log out and verify the previously saved session cannot reach the identity endpoint.