Skip to Content

CONQUERING MFA: HOW PLAYWRIGHT’S BUILT-IN STORAGE STATE REVOLUTIONIZES MULTI-FACTOR AUTHENTICATION TESTING

December 30, 2025
Mohammed Akbar Ali

Breaking the MFA Testing Barrier with Zero External Dependencies

Multi-Factor Authentication has become the security standard, but it’s also become the testing bottleneck. While security teams celebrate MFA’s protection, QA engineers face a daily nightmare of manual code entry, session timeouts, and broken test suites. Playwright’s built-in storage state feature changes this entirely, transforming MFA from a testing obstacle into a one-time setup that just works.

The MFA Testing Problem Nobody Talks About

The Hidden Cost of “Secure” Applications

Manual Intervention: Tests pause for human MFA code entry 

Time Multiplication: MFA flows repeated across hundreds of tests 

Flaky Test Syndrome: MFA timeouts cause random failures 

Productivity Drain: More time managing authentication than writing tests 

Coverage Gaps: Complex MFA flows get skipped

Why Traditional Solutions Fall Short

Most teams resort to workarounds that create more problems:

Disabled MFA in test environments: Reduces security coverage

Shared test accounts: Security risks and concurrent conflicts

Complex mock systems: Expensive to maintain, miss real issues

Manual test execution: Defeats automation purpose

Playwright’s Game-Changing Approach

Storage State: The MFA Silver Bullet

Playwright’s `storageState` feature captures the complete browser authentication context—cookies, localStorage, sessionStorage, and IndexedDB—in a single JSON file. This isn’t just cookie management; it’s comprehensive session cloning that preserves the entire authenticated state.

“`typescript

// One-time authentication setup

await page.context().storageState({ path: ‘auth.json’ });

// Infinite authentication reuse

const context = await browser.newContext({ storageState: ‘auth.json’ });

“`

What Makes This Revolutionary

Zero External Dependencies: No external libraries or plugins required 

Complete State Capture: Entire authenticated session preserved 

Cross-Test Persistence: Authentication works across test files and suites 

Real MFA Testing: Tests against actual MFA implementations locally

Important Note: MFA codes should never be stored in files or environment variables. They must be entered manually through UI interaction or command line prompts during the initial authentication setup.

How It Works: From MFA Hell to Testing Heaven

The Traditional MFA Testing Flow

“`typescript

// The nightmare scenario – repeated for every test

test(‘user dashboard’, async ({ page }) => {

  await page.goto(‘/login’);

  await page.fill(‘#username’, ‘user@example.com’);

  await page.fill(‘#password’, ‘password123’);

  await page.click(‘#login’);

  // Manual intervention required EVERY time

  await page.waitForSelector(‘#mfa-prompt’);

  const mfaCode = await promptUserForMFACode(); // Breaks automation

  await page.fill(‘#mfa-code’, mfaCode);

  await page.click(‘#verify’);

  // Finally ready to test – but authentication consumed 80% of test time

  await expect(page.getByText(‘Dashboard’)).toBeVisible();

});

“`

The Playwright Storage State Revolution

“`typescript

// global-setup.ts – Run once, authenticate forever

import { test as setup } from ‘@playwright/test’;

import { createInterface } from ‘readline’;

const promptMFACode = (): Promise<string> => {

  const rl = createInterface({

    input: process.stdin,

    output: process.stdout

  });

  return new Promise((resolve) => {

    rl.question(‘Enter MFA code: ‘, (code) => {

      rl.close();

      resolve(code);

    });

  });

};

setup(‘authenticate’, async ({ page }) => {

  await page.goto(‘/login’);

  await page.fill(‘#username’, process.env.TEST_USERNAME);

  await page.fill(‘#password’, process.env.TEST_PASSWORD);

  await page.click(‘#login’);

  // Handle MFA – manual entry required once per test suite

  await page.waitForSelector(‘#mfa-prompt’);

  // Prompt user for MFA code via command line

  const mfaCode = await promptMFACode();

  await page.fill(‘#mfa-code’, mfaCode);

  await page.click(‘#verify’);

  // Wait for successful authentication

  await page.waitForURL(‘/dashboard’);

  // Capture complete authenticated state

  await page.context().storageState({

    path: ‘playwright/.auth/user.json’

  });

});

“`

“`typescript

// Every subsequent test – Zero MFA overhead

test(‘user dashboard’, async ({ browser }) => {

  const context = await browser.newContext({

    storageState: ‘playwright/.auth/user.json’

  });

  const page = await context.newPage();

  // Skip directly to testing – already authenticated

  await page.goto(‘/dashboard’);

  await expect(page.getByText(‘Dashboard’)).toBeVisible();

  // Test runs instantly – no authentication delay

});

“`

### Session Validation and Recovery

Important: Storage states may expire and need periodic refresh. Implement validation mechanisms to detect expired sessions.

“`typescript

// Intelligent session validation with refresh capability

export class AuthManager {

  static async getAuthenticatedContext(browser: Browser): Promise<BrowserContext> {

    const context = await browser.newContext({

      storageState: ‘auth/user.json’

    });

    const page = await context.newPage();

    await page.goto(‘/dashboard’);

    // Validate authentication is still valid

    try {

      await expect(page.getByTestId(‘user-menu’)).toBeVisible({ timeout: 5000 });

      return context;

    } catch {

      // Session expired – require fresh authentication

      console.log(‘Session expired, please re-authenticate…’);

      await this.refreshAuthentication(browser);

      return this.getAuthenticatedContext(browser);

    }

  }

  private static async refreshAuthentication(browser: Browser): Promise<void> {

    // Trigger fresh MFA authentication

    console.log(‘Storage state expired. Manual re-authentication required.’);

    throw new Error(‘Please run setup again with fresh MFA authentication’);

  }

}

“`

Basic Configuration

Playwright Configuration with MFA Support

“`typescript

// playwright.config.ts

export default defineConfig({

  projects: [

    // Setup project for authentication

    {

      name: ‘setup’,

      testMatch: /global\.setup\.ts/,

    },

    // Main test project with authentication dependency

    {

      name: ‘chromium’,

      use: {

        …devices[‘Desktop Chrome’],

        storageState: ‘playwright/.auth/user.json’,

      },

      dependencies: [‘setup’],

    },

  ],

});

“`

Note: Each browser engine requires its own storage state file. Chrome, Firefox, Safari, and Edge cannot share authentication states.

Real-World Impact

Before: MFA codes required for every test execution, constant workflow interruption 

After: Zero manual intervention once authenticated, uninterrupted test execution

Important: Storage state files cannot be committed to repositories – local development only.

Benefits for Local Development

Zero manual intervention during local test execution 

Uninterrupted workflow without repeated MFA challenges 

Complete test coverage locally without skipping MFA scenarios 

Focus on test logic instead of authentication mechanics 

Faster feedback cycles for test development and debugging

CI/CD Limitation: Storage state files cannot be committed to repositories due to security concerns – this approach is for local development only.

Implementation Best Practices

Security Considerations

Never commit storage state files: Exclude from version control – contain sensitive data 

Local development only: Not suitable for CI/CD pipelines 

Never store MFA codes: Always require manual entry 

Regular refresh: Schedule periodic re-authentication for expired states 

Browser-specific states: Each browser engine needs separate storage files

Important Limitations

Critical Limitation: Storage state files contain sensitive authentication data and cannot be safely stored in version control or CI/CD environments. This approach is designed for local development workflows only.

Getting Started

Prerequisites

• Node.js 16+ and Playwright installed 

• Basic Playwright knowledge 

• MFA-protected application for testing

Quick Implementation

1. Setup authentication once in global-setup.ts

2. Configure Playwright to use storage state

3. Run tests without repeated MFA prompts

Key Points

Never store MFA codes – always enter manually 

Local development only – cannot be used in CI/CD 

Browser-specific states – each browser needs separate files 

Regular refresh needed – storage states expire 

Exclude from version control – contains sensitive data

Conclusion: MFA Automation for Local Development

Playwright’s storage state transforms Multi-Factor Authentication from a local development testing roadblock into a one-time setup that enables seamless developer workflows. By capturing complete browser authentication context in a simple JSON file, development teams can eliminate MFA friction during local testing while maintaining security standards.

The built-in nature of this solution—requiring zero external dependencies or complex integrations—makes it accessible to development teams of all sizes for local testing scenarios. As security requirements continue to evolve, Playwright’s approach provides a practical solution for local development while acknowledging the security constraints that prevent its use in CI/CD environments.

Key Takeaway: This approach is designed for local development workflows where developers need uninterrupted access to MFA-protected applications during testing. For CI/CD scenarios, alternative authentication strategies must be employed.

About Implementation Support

This MFA automation approach using Playwright’s built-in storage state is immediately available to any development team using Playwright for local testing. The techniques described require no special licensing, external services, or complex setup procedures—just Playwright and proper understanding of the security limitations.

Important Scope: This solution is designed for local development environments to eliminate MFA friction during test development and debugging. Teams must implement separate strategies for CI/CD pipelines where storage state files cannot be safely utilized.

Tags: MFA Automation, Playwright

About the author

Senior Manager | India
Akbar is a Senior Automation Architect at Sogeti, driving innovation through open-source automation frameworks and GenAI-led test strategies. He has led multiple PoCs, crafted scalable automation assets, and aligned testing solutions with business goals.

Leave a Reply

Your email address will not be published. Required fields are marked *

Slide to submit