# Authentication Flow Recipes

> **When to use**: You need to test login, signup, logout, session management, or any authentication-related user flow.

---

## Recipe 1: Basic Login

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';

test('logs in with valid credentials', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Wait for navigation to complete after login
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await expect(page.getByText('Welcome, user@example.com')).toBeVisible();
});

test('shows error for invalid credentials', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('WrongPassword');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByRole('alert')).toContainText('Invalid email or password');
  // Should remain on login page
  await expect(page).toHaveURL('/login');
});

test('shows validation errors for empty fields', async ({ page }) => {
  await page.goto('/login');

  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByText('Email is required')).toBeVisible();
  await expect(page.getByText('Password is required')).toBeVisible();
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test('logs in with valid credentials', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await expect(page.getByText('Welcome, user@example.com')).toBeVisible();
});

test('shows error for invalid credentials', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('WrongPassword');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByRole('alert')).toContainText('Invalid email or password');
  await expect(page).toHaveURL('/login');
});

test('shows validation errors for empty fields', async ({ page }) => {
  await page.goto('/login');

  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page.getByText('Email is required')).toBeVisible();
  await expect(page.getByText('Password is required')).toBeVisible();
});
```

---

## Recipe 2: Login with "Remember Me"

### Complete Example

**TypeScript**

```typescript
import { test, expect, type BrowserContext } from '@playwright/test';

test('remember me persists session across browser restarts', async ({ browser }) => {
  // First session: log in with "remember me" checked
  const context1 = await browser.newContext();
  const page1 = await context1.newPage();

  await page1.goto('/login');
  await page1.getByLabel('Email').fill('user@example.com');
  await page1.getByLabel('Password').fill('SecurePass123!');
  await page1.getByLabel('Remember me').check();
  await page1.getByRole('button', { name: 'Sign in' }).click();

  await expect(page1).toHaveURL('/dashboard');

  // Save storage state (cookies + localStorage)
  const storageState = await context1.storageState();
  await context1.close();

  // Second session: restore state to simulate browser restart
  const context2 = await browser.newContext({ storageState });
  const page2 = await context2.newPage();

  await page2.goto('/dashboard');

  // Should still be logged in without re-authenticating
  await expect(page2).toHaveURL('/dashboard');
  await expect(page2.getByText('Welcome, user@example.com')).toBeVisible();

  await context2.close();
});

test('no remember me does not persist session', async ({ browser }) => {
  const context1 = await browser.newContext();
  const page1 = await context1.newPage();

  await page1.goto('/login');
  await page1.getByLabel('Email').fill('user@example.com');
  await page1.getByLabel('Password').fill('SecurePass123!');
  // Explicitly leave "Remember me" unchecked
  await expect(page1.getByLabel('Remember me')).not.toBeChecked();
  await page1.getByRole('button', { name: 'Sign in' }).click();

  await expect(page1).toHaveURL('/dashboard');

  // Capture only cookies (not sessionStorage) to simulate new tab/window
  const cookies = await context1.cookies();
  await context1.close();

  // New context with only persistent cookies (session cookies are excluded)
  const persistentCookies = cookies.filter(c => c.expires > 0);
  const context2 = await browser.newContext();
  await context2.addCookies(persistentCookies);
  const page2 = await context2.newPage();

  await page2.goto('/dashboard');

  // Should redirect to login since session was not persisted
  await expect(page2).toHaveURL(/\/login/);

  await context2.close();
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test('remember me persists session across browser restarts', async ({ browser }) => {
  const context1 = await browser.newContext();
  const page1 = await context1.newPage();

  await page1.goto('/login');
  await page1.getByLabel('Email').fill('user@example.com');
  await page1.getByLabel('Password').fill('SecurePass123!');
  await page1.getByLabel('Remember me').check();
  await page1.getByRole('button', { name: 'Sign in' }).click();

  await expect(page1).toHaveURL('/dashboard');

  const storageState = await context1.storageState();
  await context1.close();

  const context2 = await browser.newContext({ storageState });
  const page2 = await context2.newPage();

  await page2.goto('/dashboard');

  await expect(page2).toHaveURL('/dashboard');
  await expect(page2.getByText('Welcome, user@example.com')).toBeVisible();

  await context2.close();
});
```

---

## Recipe 3: Signup with Email Verification (Mocked)

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';

test('completes signup flow with mocked email verification', async ({ page }) => {
  // Intercept the verification email API so we can capture the token
  let verificationToken = '';
  await page.route('**/api/auth/send-verification', async (route) => {
    // Let the request go through to the server
    const response = await route.fetch();
    const body = await response.json();

    // Capture the token from the API response (test environments expose this)
    verificationToken = body.verificationToken;

    await route.fulfill({ response });
  });

  // Step 1: Fill out signup form
  await page.goto('/signup');

  await page.getByLabel('Full name').fill('Jane Tester');
  await page.getByLabel('Email').fill('jane@example.com');
  await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
  await page.getByLabel('Confirm password').fill('SecurePass123!');
  await page.getByLabel('I agree to the Terms of Service').check();
  await page.getByRole('button', { name: 'Create account' }).click();

  // Step 2: Verify "check your email" screen appears
  await expect(page.getByText('Check your email')).toBeVisible();
  await expect(page.getByText('jane@example.com')).toBeVisible();

  // Step 3: Navigate directly to verification URL using captured token
  expect(verificationToken).toBeTruthy();
  await page.goto(`/verify-email?token=${verificationToken}`);

  // Step 4: Verify account is now active
  await expect(page.getByText('Email verified successfully')).toBeVisible();
  await expect(page).toHaveURL('/login');

  // Step 5: Log in with the new account
  await page.getByLabel('Email').fill('jane@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Welcome, Jane Tester')).toBeVisible();
});

test('signup with fully mocked email API (no server dependency)', async ({ page }) => {
  const fakeToken = 'test-verification-token-12345';

  // Mock the signup API endpoint
  await page.route('**/api/auth/signup', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        message: 'Verification email sent',
        verificationToken: fakeToken,
      }),
    });
  });

  // Mock the verification endpoint
  await page.route(`**/api/auth/verify-email?token=${fakeToken}`, async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ message: 'Email verified', redirectTo: '/login' }),
    });
  });

  await page.goto('/signup');

  await page.getByLabel('Full name').fill('Jane Tester');
  await page.getByLabel('Email').fill('jane@example.com');
  await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
  await page.getByLabel('Confirm password').fill('SecurePass123!');
  await page.getByLabel('I agree to the Terms of Service').check();
  await page.getByRole('button', { name: 'Create account' }).click();

  await expect(page.getByText('Check your email')).toBeVisible();

  // Simulate clicking the verification link from the email
  await page.goto(`/verify-email?token=${fakeToken}`);

  await expect(page.getByText('Email verified successfully')).toBeVisible();
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test('completes signup flow with mocked email verification', async ({ page }) => {
  let verificationToken = '';
  await page.route('**/api/auth/send-verification', async (route) => {
    const response = await route.fetch();
    const body = await response.json();
    verificationToken = body.verificationToken;
    await route.fulfill({ response });
  });

  await page.goto('/signup');

  await page.getByLabel('Full name').fill('Jane Tester');
  await page.getByLabel('Email').fill('jane@example.com');
  await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
  await page.getByLabel('Confirm password').fill('SecurePass123!');
  await page.getByLabel('I agree to the Terms of Service').check();
  await page.getByRole('button', { name: 'Create account' }).click();

  await expect(page.getByText('Check your email')).toBeVisible();

  expect(verificationToken).toBeTruthy();
  await page.goto(`/verify-email?token=${verificationToken}`);

  await expect(page.getByText('Email verified successfully')).toBeVisible();
  await expect(page).toHaveURL('/login');
});
```

---

## Recipe 4: Password Reset Flow

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';

test('completes password reset flow', async ({ page }) => {
  let resetToken = '';

  // Intercept the password reset API to capture the reset token
  await page.route('**/api/auth/forgot-password', async (route) => {
    const response = await route.fetch();
    const body = await response.json();
    resetToken = body.resetToken;
    await route.fulfill({ response });
  });

  // Step 1: Request password reset
  await page.goto('/forgot-password');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByRole('button', { name: 'Send reset link' }).click();

  await expect(page.getByText('Reset link sent')).toBeVisible();
  await expect(page.getByText('user@example.com')).toBeVisible();

  // Step 2: Navigate to the reset page with token
  expect(resetToken).toBeTruthy();
  await page.goto(`/reset-password?token=${resetToken}`);

  // Step 3: Set new password
  await page.getByLabel('New password', { exact: true }).fill('NewSecurePass456!');
  await page.getByLabel('Confirm new password').fill('NewSecurePass456!');
  await page.getByRole('button', { name: 'Reset password' }).click();

  await expect(page.getByText('Password reset successfully')).toBeVisible();

  // Step 4: Log in with new password
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('NewSecurePass456!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL('/dashboard');
});

test('password reset with expired token shows error', async ({ page }) => {
  await page.goto('/reset-password?token=expired-token-123');

  await page.getByLabel('New password', { exact: true }).fill('NewSecurePass456!');
  await page.getByLabel('Confirm new password').fill('NewSecurePass456!');
  await page.getByRole('button', { name: 'Reset password' }).click();

  await expect(page.getByRole('alert')).toContainText(
    /token has expired|link is no longer valid/i
  );
  await expect(page.getByRole('link', { name: 'Request a new reset link' })).toBeVisible();
});

test('password reset enforces password strength requirements', async ({ page }) => {
  await page.goto('/reset-password?token=valid-token');

  // Try a weak password
  await page.getByLabel('New password', { exact: true }).fill('123');
  await page.getByLabel('Confirm new password').fill('123');
  await page.getByRole('button', { name: 'Reset password' }).click();

  await expect(page.getByText(/at least 8 characters/i)).toBeVisible();
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test('completes password reset flow', async ({ page }) => {
  let resetToken = '';

  await page.route('**/api/auth/forgot-password', async (route) => {
    const response = await route.fetch();
    const body = await response.json();
    resetToken = body.resetToken;
    await route.fulfill({ response });
  });

  await page.goto('/forgot-password');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByRole('button', { name: 'Send reset link' }).click();

  await expect(page.getByText('Reset link sent')).toBeVisible();

  expect(resetToken).toBeTruthy();
  await page.goto(`/reset-password?token=${resetToken}`);

  await page.getByLabel('New password', { exact: true }).fill('NewSecurePass456!');
  await page.getByLabel('Confirm new password').fill('NewSecurePass456!');
  await page.getByRole('button', { name: 'Reset password' }).click();

  await expect(page.getByText('Password reset successfully')).toBeVisible();

  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('NewSecurePass456!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL('/dashboard');
});
```

---

## Recipe 5: OAuth Login (Mocked Callback)

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';

test('logs in via mocked Google OAuth callback', async ({ page }) => {
  // Intercept the OAuth redirect to Google and simulate the callback
  await page.route('**/accounts.google.com/**', async (route) => {
    // Instead of actually going to Google, redirect back to our callback URL
    // with a mock authorization code
    const callbackUrl = new URL('/auth/callback/google', 'http://localhost:3000');
    callbackUrl.searchParams.set('code', 'mock-auth-code-12345');
    callbackUrl.searchParams.set('state', route.request().url().match(/state=([^&]+)/)?.[1] || '');

    await route.fulfill({
      status: 302,
      headers: { location: callbackUrl.toString() },
    });
  });

  // Mock the token exchange on our backend
  await page.route('**/api/auth/callback/google**', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        user: {
          id: '1',
          email: 'googleuser@gmail.com',
          name: 'Google User',
          avatar: 'https://example.com/avatar.jpg',
        },
        token: 'mock-jwt-token',
      }),
    });
  });

  await page.goto('/login');

  // Click the OAuth button
  await page.getByRole('button', { name: /Sign in with Google/i }).click();

  // After the mocked OAuth flow, we should land on the dashboard
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Google User')).toBeVisible();
});

test('handles OAuth login failure gracefully', async ({ page }) => {
  // Simulate OAuth provider returning an error
  await page.route('**/accounts.google.com/**', async (route) => {
    const callbackUrl = new URL('/auth/callback/google', 'http://localhost:3000');
    callbackUrl.searchParams.set('error', 'access_denied');
    callbackUrl.searchParams.set('error_description', 'User denied access');

    await route.fulfill({
      status: 302,
      headers: { location: callbackUrl.toString() },
    });
  });

  await page.goto('/login');
  await page.getByRole('button', { name: /Sign in with Google/i }).click();

  await expect(page.getByRole('alert')).toContainText(/authentication failed|access denied/i);
  await expect(page).toHaveURL(/\/login/);
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test('logs in via mocked Google OAuth callback', async ({ page }) => {
  await page.route('**/accounts.google.com/**', async (route) => {
    const callbackUrl = new URL('/auth/callback/google', 'http://localhost:3000');
    callbackUrl.searchParams.set('code', 'mock-auth-code-12345');
    callbackUrl.searchParams.set('state', route.request().url().match(/state=([^&]+)/)?.[1] || '');

    await route.fulfill({
      status: 302,
      headers: { location: callbackUrl.toString() },
    });
  });

  await page.route('**/api/auth/callback/google**', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        user: { id: '1', email: 'googleuser@gmail.com', name: 'Google User' },
        token: 'mock-jwt-token',
      }),
    });
  });

  await page.goto('/login');
  await page.getByRole('button', { name: /Sign in with Google/i }).click();

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Google User')).toBeVisible();
});
```

---

## Recipe 6: Role-Based Access Testing

### Complete Example

**TypeScript**

```typescript
import { test, expect, type Page } from '@playwright/test';
import path from 'path';

// Store auth state for each role in separate files
const authDir = path.resolve(__dirname, '../.auth');

// Setup: create auth state files for each role (run in globalSetup or auth.setup)
// This pattern uses Playwright's storage state for efficient role switching
const roles = ['admin', 'editor', 'viewer'] as const;
type Role = typeof roles[number];

function authFile(role: Role): string {
  return path.join(authDir, `${role}.json`);
}

// Auth setup project — runs before all tests
test.describe('auth setup', () => {
  const credentials: Record<Role, { email: string; password: string }> = {
    admin: { email: 'admin@example.com', password: 'AdminPass123!' },
    editor: { email: 'editor@example.com', password: 'EditorPass123!' },
    viewer: { email: 'viewer@example.com', password: 'ViewerPass123!' },
  };

  for (const role of roles) {
    test(`authenticate as ${role}`, async ({ page }) => {
      await page.goto('/login');
      await page.getByLabel('Email').fill(credentials[role].email);
      await page.getByLabel('Password').fill(credentials[role].password);
      await page.getByRole('button', { name: 'Sign in' }).click();
      await expect(page).toHaveURL('/dashboard');

      await page.context().storageState({ path: authFile(role) });
    });
  }
});

// Admin tests
test.describe('admin access', () => {
  test.use({ storageState: authFile('admin') });

  test('admin can access user management', async ({ page }) => {
    await page.goto('/admin/users');
    await expect(page).toHaveURL('/admin/users');
    await expect(page.getByRole('heading', { name: 'User Management' })).toBeVisible();
    await expect(page.getByRole('button', { name: 'Add user' })).toBeVisible();
    await expect(page.getByRole('button', { name: 'Delete' }).first()).toBeVisible();
  });

  test('admin can access system settings', async ({ page }) => {
    await page.goto('/admin/settings');
    await expect(page).toHaveURL('/admin/settings');
    await expect(page.getByRole('heading', { name: 'System Settings' })).toBeVisible();
  });
});

// Editor tests
test.describe('editor access', () => {
  test.use({ storageState: authFile('editor') });

  test('editor can create and edit content', async ({ page }) => {
    await page.goto('/content');
    await expect(page.getByRole('button', { name: 'New post' })).toBeVisible();
    await expect(page.getByRole('button', { name: 'Edit' }).first()).toBeVisible();
  });

  test('editor cannot access admin area', async ({ page }) => {
    await page.goto('/admin/users');
    // Should be redirected or shown forbidden
    await expect(page).toHaveURL(/\/(403|dashboard|login)/);
  });
});

// Viewer tests
test.describe('viewer access', () => {
  test.use({ storageState: authFile('viewer') });

  test('viewer can read content', async ({ page }) => {
    await page.goto('/content');
    await expect(page.getByRole('article').first()).toBeVisible();
  });

  test('viewer cannot create content', async ({ page }) => {
    await page.goto('/content');
    await expect(page.getByRole('button', { name: 'New post' })).not.toBeVisible();
  });

  test('viewer cannot access admin area', async ({ page }) => {
    await page.goto('/admin/users');
    await expect(page).toHaveURL(/\/(403|dashboard|login)/);
  });
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');
const path = require('path');

const authDir = path.resolve(__dirname, '../.auth');

function authFile(role) {
  return path.join(authDir, `${role}.json`);
}

// Admin tests
test.describe('admin access', () => {
  test.use({ storageState: authFile('admin') });

  test('admin can access user management', async ({ page }) => {
    await page.goto('/admin/users');
    await expect(page).toHaveURL('/admin/users');
    await expect(page.getByRole('heading', { name: 'User Management' })).toBeVisible();
    await expect(page.getByRole('button', { name: 'Add user' })).toBeVisible();
  });

  test('admin can access system settings', async ({ page }) => {
    await page.goto('/admin/settings');
    await expect(page).toHaveURL('/admin/settings');
  });
});

// Viewer tests
test.describe('viewer access', () => {
  test.use({ storageState: authFile('viewer') });

  test('viewer cannot create content', async ({ page }) => {
    await page.goto('/content');
    await expect(page.getByRole('button', { name: 'New post' })).not.toBeVisible();
  });

  test('viewer cannot access admin area', async ({ page }) => {
    await page.goto('/admin/users');
    await expect(page).toHaveURL(/\/(403|dashboard|login)/);
  });
});
```

---

## Recipe 7: Session Timeout Handling

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';

test('redirects to login after session timeout', async ({ page, context }) => {
  // Log in first
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page).toHaveURL('/dashboard');

  // Simulate session expiration by clearing the session cookie
  const cookies = await context.cookies();
  const sessionCookie = cookies.find(
    (c) => c.name === 'session' || c.name === 'sid' || c.name === 'connect.sid'
  );

  if (sessionCookie) {
    await context.clearCookies({ name: sessionCookie.name });
  }

  // Try to navigate to a protected page
  await page.goto('/settings');

  // Should redirect to login with a return URL
  await expect(page).toHaveURL(/\/login/);
  await expect(page.getByText(/session.*expired|please.*log in again/i)).toBeVisible();
});

test('shows session expiry warning before timeout', async ({ page }) => {
  // Mock the session check endpoint to return "expiring soon"
  await page.route('**/api/auth/session', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        valid: true,
        expiresIn: 120, // 2 minutes remaining
      }),
    });
  });

  await page.goto('/dashboard');

  // The app should show a warning when session is about to expire
  await expect(page.getByText(/session.*expir/i)).toBeVisible({ timeout: 10000 });
  await expect(page.getByRole('button', { name: /extend|stay logged in/i })).toBeVisible();
});

test('extends session when user clicks extend', async ({ page }) => {
  let sessionExtended = false;

  await page.route('**/api/auth/session', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ valid: true, expiresIn: 120 }),
    });
  });

  await page.route('**/api/auth/refresh', async (route) => {
    sessionExtended = true;
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ valid: true, expiresIn: 3600 }),
    });
  });

  await page.goto('/dashboard');

  await expect(page.getByRole('button', { name: /extend|stay logged in/i })).toBeVisible({
    timeout: 10000,
  });
  await page.getByRole('button', { name: /extend|stay logged in/i }).click();

  expect(sessionExtended).toBe(true);
  await expect(page.getByText(/session.*expir/i)).not.toBeVisible();
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test('redirects to login after session timeout', async ({ page, context }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page).toHaveURL('/dashboard');

  const cookies = await context.cookies();
  const sessionCookie = cookies.find(
    (c) => c.name === 'session' || c.name === 'sid' || c.name === 'connect.sid'
  );

  if (sessionCookie) {
    await context.clearCookies({ name: sessionCookie.name });
  }

  await page.goto('/settings');

  await expect(page).toHaveURL(/\/login/);
  await expect(page.getByText(/session.*expired|please.*log in again/i)).toBeVisible();
});
```

---

## Recipe 8: Logout

### Complete Example

**TypeScript**

```typescript
import { test, expect } from '@playwright/test';

// Use a logged-in state
test.use({ storageState: '.auth/user.json' });

test('logs out and redirects to login page', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page.getByText('Welcome')).toBeVisible();

  // Open user menu and click logout
  await page.getByRole('button', { name: /user menu|account|profile/i }).click();
  await page.getByRole('menuitem', { name: 'Log out' }).click();

  // Should redirect to login
  await expect(page).toHaveURL('/login');

  // Verify we can no longer access protected pages
  await page.goto('/dashboard');
  await expect(page).toHaveURL(/\/login/);
});

test('logout clears all session data', async ({ page, context }) => {
  await page.goto('/dashboard');

  await page.getByRole('button', { name: /user menu|account|profile/i }).click();
  await page.getByRole('menuitem', { name: 'Log out' }).click();

  await expect(page).toHaveURL('/login');

  // Verify cookies are cleared
  const cookies = await context.cookies();
  const sessionCookies = cookies.filter(
    (c) => c.name === 'session' || c.name === 'sid' || c.name === 'token'
  );
  expect(sessionCookies).toHaveLength(0);

  // Verify localStorage is cleared
  const token = await page.evaluate(() => localStorage.getItem('authToken'));
  expect(token).toBeNull();
});

test('logout from all devices', async ({ page }) => {
  let logoutAllCalled = false;

  await page.route('**/api/auth/logout-all', async (route) => {
    logoutAllCalled = true;
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ message: 'Logged out from all devices' }),
    });
  });

  await page.goto('/settings/security');

  await page.getByRole('button', { name: 'Log out of all devices' }).click();

  // Confirm the action in the dialog
  await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();

  expect(logoutAllCalled).toBe(true);
  await expect(page).toHaveURL(/\/login/);
});
```

**JavaScript**

```javascript
const { test, expect } = require('@playwright/test');

test.use({ storageState: '.auth/user.json' });

test('logs out and redirects to login page', async ({ page }) => {
  await page.goto('/dashboard');

  await page.getByRole('button', { name: /user menu|account|profile/i }).click();
  await page.getByRole('menuitem', { name: 'Log out' }).click();

  await expect(page).toHaveURL('/login');

  await page.goto('/dashboard');
  await expect(page).toHaveURL(/\/login/);
});

test('logout clears all session data', async ({ page, context }) => {
  await page.goto('/dashboard');

  await page.getByRole('button', { name: /user menu|account|profile/i }).click();
  await page.getByRole('menuitem', { name: 'Log out' }).click();

  await expect(page).toHaveURL('/login');

  const cookies = await context.cookies();
  const sessionCookies = cookies.filter(
    (c) => c.name === 'session' || c.name === 'sid' || c.name === 'token'
  );
  expect(sessionCookies).toHaveLength(0);
});
```

---

## Variations

### Login via API for Speed

Skip the UI for login when testing non-auth features. Use `request` context to get tokens faster.

**TypeScript**

```typescript
import { test as setup } from '@playwright/test';

setup('authenticate via API', async ({ request }) => {
  const response = await request.post('/api/auth/login', {
    data: {
      email: 'user@example.com',
      password: 'SecurePass123!',
    },
  });

  expect(response.ok()).toBeTruthy();

  // Save the auth state
  await request.storageState({ path: '.auth/user.json' });
});
```

### Multi-Factor Authentication

```typescript
test('logs in with MFA (TOTP)', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('mfa-user@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Sign in' }).click();

  // MFA challenge screen appears
  await expect(page.getByText('Enter verification code')).toBeVisible();

  // In test environments, use a predictable TOTP code or mock the verification
  await page.route('**/api/auth/verify-mfa', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ success: true, token: 'jwt-token' }),
    });
  });

  await page.getByLabel('Verification code').fill('123456');
  await page.getByRole('button', { name: 'Verify' }).click();

  await expect(page).toHaveURL('/dashboard');
});
```

### Passkeys / WebAuthn (Playwright 1.61+)

Playwright 1.61 ships a virtual WebAuthn authenticator scoped to the browser context: `context.credentials`. Tests can register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies without a hardware security key. Call `credentials.install()` before any page in the context starts a WebAuthn ceremony.

**Pattern A: let the app register a passkey, then sign in with it**

```typescript
test('registers a passkey and signs in with it', async ({ browser }) => {
  const context = await browser.newContext();
  await context.credentials.install(); // override navigator.credentials in this context

  const page = await context.newPage();

  // Register: the app's "Create a passkey" flow now talks to the virtual authenticator
  await page.goto('/settings/security');
  await page.getByRole('button', { name: 'Create a passkey' }).click();
  await expect(page.getByText('Passkey added')).toBeVisible();

  // Sign out, then sign back in with the passkey
  await page.goto('/logout');
  await page.goto('/login');
  await page.getByRole('button', { name: 'Sign in with a passkey' }).click();
  // navigator.credentials.get() is answered with the registered passkey
  await expect(page).toHaveURL('/dashboard');

  await context.close();
});
```

**Pattern B: capture once, seed everywhere (the storageState pattern for passkeys)**

```typescript
// setup project: register a passkey once and save it (it includes the private key)
const [credential] = await context.credentials.get({ rpId: 'example.com' });
fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));

// later tests: seed the captured passkey so the user is already enrolled
const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));
const context = await browser.newContext();
await context.credentials.create(credential.rpId, credential);
await context.credentials.install();
// navigator.credentials.get() now resolves the seeded passkey — no registration UI needed
```

You can also seed a credential your backend already provisioned for a test user by passing `id`, `userHandle`, `privateKey` (base64url PKCS#8), and `publicKey` (base64url SPKI) to `credentials.create(rpId, …)`, and remove one with `credentials.delete(id)`.

**Tips:**
- Treat saved passkey JSON like a saved `storageState` — it contains a private key, so keep it out of version control (`.gitignore` the `.auth/` directory).
- Combine with a setup project (see Recipe 1) so passkey enrollment happens once per run, not per test.
- Test the fallback path too: what happens when the user cancels the ceremony or has no passkey.

### Testing Auth with Different Browser Contexts

```typescript
test('two users interact simultaneously', async ({ browser }) => {
  const adminContext = await browser.newContext({
    storageState: '.auth/admin.json',
  });
  const userContext = await browser.newContext({
    storageState: '.auth/viewer.json',
  });

  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();

  // Admin bans a user
  await adminPage.goto('/admin/users');
  await adminPage.getByRole('row', { name: 'viewer@example.com' })
    .getByRole('button', { name: 'Ban' }).click();
  await adminPage.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();

  // User tries to navigate and should be blocked
  await userPage.goto('/dashboard');
  await expect(userPage.getByText(/account.*banned|access.*revoked/i)).toBeVisible();

  await adminContext.close();
  await userContext.close();
});
```

---

## Tips

1. **Use `storageState` for reusable auth**. Run login once in a setup project, save the state to a JSON file, and reuse it across all tests. This dramatically speeds up your test suite since you only log in once per role.

2. **Never hard-code credentials in test files**. Use environment variables or a `.env` file loaded via `dotenv`. In `playwright.config.ts`, set `process.env` values or use the `env` option in `use`.

3. **Prefer API-based login for non-auth tests**. Only test the login UI in your auth-specific tests. For everything else, authenticate via the API or by restoring `storageState`. This makes tests faster and less brittle.

4. **Test the negative paths too**. Invalid credentials, expired tokens, locked accounts, rate limiting. These are the scenarios users actually encounter in production and are frequently undertested.

5. **Set a shorter session timeout in your test environment**. If your app has a 30-minute session timeout, configure it to 30 seconds in the test environment so you can test timeout behavior without slow tests.

---

## Related

- [Playwright Authentication Docs](https://playwright.dev/docs/auth)
- `patterns/page-objects.md` -- Encapsulate login flows in page objects
- `foundations/assertions.md` -- Assertion patterns for auth states
- `recipes/crud-testing.md` -- Test CRUD operations once authenticated
