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:
- TOTP (Time-based One-Time Password) -- apps like Google Authenticator or Authy generate codes from a shared secret.
- SMS codes -- a numeric code sent via text message to a registered phone number.
- Email codes -- similar to SMS but delivered to an email inbox.
- Push notifications -- approve/deny prompts on a mobile device (Duo, Microsoft Authenticator).
- Hardware keys -- FIDO2/WebAuthn with YubiKey or similar physical tokens.
- Passkeys -- device-bound credentials using platform authenticators.
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.
// 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.
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).
// 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.
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:
- Try session restoration first. Load saved cookies and check if the session is still valid. If it is, skip login entirely.
- If session expired, log in with credentials. Enter username and password programmatically.
- If TOTP 2FA appears and you have the secret, generate and enter the code automatically.
- If any other 2FA type appears, escalate to a human via Pilot or your own HITL system.
- 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:
- Store TOTP secrets and backup codes in a proper secrets manager (AWS Secrets Manager, Vault, etc.), never in code or environment files committed to version control.
- When using human-in-the-loop, ensure the human operator only sees the browser session, not the stored credentials. Pilot's approach of connecting via CDP means the operator interacts with the live browser without needing access to the password vault.
- Rotate credentials and backup codes on a schedule. Monitor for failed login attempts that might indicate a compromised session.
- Log every automated and human-assisted login for audit purposes.