Handling Two-Factor Authentication in Browser Automation

Two-factor authentication is one of the hardest problems in browser automation. Unlike CAPTCHAs, which are designed to block bots, 2FA is designed to verify identity -- and that distinction makes it fundamentally harder to work around. Your agent can fill in a username and password, but when the site sends an SMS code or prompts for an authenticator app, the automation pipeline breaks.

This guide covers practical approaches to handling 2FA in automated browser workflows, from testing environments to production agent systems operating on real accounts.

Why 2FA Breaks Automation

The core problem is that 2FA introduces an out-of-band verification step. The second factor exists specifically because it requires something the automated system does not have access to: a physical device, a phone number, or a biometric input.

Common 2FA methods you will encounter:

Each type requires a different strategy, and some have no fully automated solution at all.

Approach 1: TOTP Secret Sharing

If the account uses TOTP-based 2FA (Google Authenticator, Authy, 1Password), you can automate it completely -- but only if you have the TOTP secret. The secret is the base32-encoded string shown during 2FA setup, typically displayed as a QR code.

const { authenticator } = require('otplib');

// Store the TOTP secret securely (not hardcoded like this)
const secret = process.env.TOTP_SECRET;

// Generate the current 6-digit code
const token = authenticator.generate(secret);

// Type it into the 2FA input field
await page.type('input[name="otp"]', token);
await page.click('button[type="submit"]');

When it works: Testing environments where you control the accounts. Internal tools where you can provision service accounts with known TOTP secrets. Any scenario where you have the secret at setup time.

When it fails: Third-party accounts where you do not control the 2FA setup. Accounts that were set up manually and nobody saved the TOTP secret. Accounts using SMS, push, or hardware key 2FA.

Approach 2: Backup Codes

Most services provide one-time backup codes when 2FA is enabled. These are designed for recovery scenarios but work perfectly for automation -- each code is valid once and can be stored alongside credentials.

// Store backup codes in your secrets manager
const backupCodes = JSON.parse(process.env.BACKUP_CODES);

// Use the next available code
const code = backupCodes.shift();
await page.type('input[name="backup-code"]', code);

// Important: update your stored list so you don't reuse codes
await updateSecrets({ BACKUP_CODES: JSON.stringify(backupCodes) });

When it works: Low-frequency automation where you log in occasionally. Most services give 8-10 backup codes, enough for weeks or months of use.

When it fails: High-frequency logins that burn through codes quickly. Sites that require re-authentication to generate new backup codes (chicken-and-egg problem). Some sites do not offer backup codes at all.

Approach 3: Session Persistence

The best 2FA solve is one you never have to do. If you can preserve the authenticated session between agent runs, you only need to handle 2FA once (or whenever the session expires).

const fs = require('fs');

// Save cookies after successful login + 2FA
const cookies = await page.cookies();
fs.writeFileSync('session.json', JSON.stringify(cookies));

// On next run, restore the session
const saved = JSON.parse(fs.readFileSync('session.json'));
await page.setCookie(...saved);

This works well in practice but requires careful management. Sessions expire, cookies get invalidated, and sites sometimes force re-authentication. Your agent needs to detect when a session is no longer valid and trigger a fresh login flow.

Approach 4: Human Escalation

For SMS codes, push notifications, hardware keys, and any 2FA method that requires a physical device, there is no way to automate the process without access to that device. The only reliable approach is to have a human step in.

This is where human-in-the-loop becomes essential rather than optional. The agent detects the 2FA prompt, pauses, and a human with access to the second factor completes the verification.

Human 2FA with Pilot

Pilot lets your agent hand the browser session to a human operator who can complete any 2FA flow -- SMS, push notification, hardware key, or anything else.

const pilot = require('./pilot')('https://pilotapp.dev', {
  apiKey: 'pk_your_key'
});

// After entering username + password, check for 2FA prompt
const needs2FA = await page.$('input[name="otp"], .two-factor-prompt, [data-testid="2fa"]');

if (needs2FA) {
  const result = await pilot.rescue(page, '2FA required -- SMS code sent to account owner');
  if (!result.solved) {
    throw new Error(`2FA rescue failed: ${result.error}`);
  }
}

// Agent continues -- 2FA completed by human
// Save session cookies to avoid 2FA on next run
const cookies = await page.cookies();
await saveSession(cookies);

The rescue call blocks while a human operator connects to the browser, completes the 2FA challenge, and disconnects. The agent then saves the authenticated session so it does not need human help again until the session expires.

A Practical Strategy for Production

In a production agent system, combine these approaches in a priority chain:

  1. Try session restoration first. Load saved cookies and check if the session is still valid. If it is, skip login entirely.
  2. If session expired, log in with credentials. Enter username and password programmatically.
  3. If TOTP 2FA appears and you have the secret, generate and enter the code automatically.
  4. If any other 2FA type appears, escalate to a human via Pilot or your own HITL system.
  5. After successful login, save the session for next time.

This minimizes human intervention to only the cases where it is genuinely needed. Most runs will use the cached session. Occasional runs will need a TOTP code. And only when sessions expire on SMS/push-protected accounts will a human need to step in.

Security Considerations

Automating 2FA inherently involves handling sensitive credentials. A few things to keep in mind: