GuideBrowser Automation

Automating 2FA with Playwright, Stagehand & Browserbase

Step-by-step recipes to solve out-of-band Email OTP, Magic-Link, Carrier SMS verification, and KMS-vaulted TOTP authenticator hurdles in headless browser automation—including how to pair Sentry Relay with 1Password and Browserbase Contexts for 100% unattended execution.

Pattern 1: Playwright Email or SMS OTP Verification

The relay.wait() helper holds the connection open until the exact code arrives. Because Sentry Relay combines an Upstash Redis 15s lookback ZSET with Pub/Sub push notifications in under 200ms, your automation never suffers from "arrived-before-wait" race conditions.

import { test, expect } from '@playwright/test';
import { SentryRelay } from '@sentryrelay/sdk';

const relay = new SentryRelay({
  apiKey: process.env.SENTRY_RELAY_API_KEY!,
});

test('Complete customer login with SMS OTP', async ({ page }) => {
  // 1. Fill in mobile number associated with Identity DID
  await page.goto('https://banking.example.com/login');
  await page.fill('input#phone', '+12035550142');
  await page.click('button#send-code');

  // 2. Await SMS OTP (blocks up to 45 seconds, returns in <200ms of arrival)
  const auth = await relay.wait('banking-bot', {
    channel: 'sms',
    timeout: 45,
  });

  console.log(`Received OTP: ${auth.event?.otpCode} (latency: ${auth.waitDurationMs}ms)`);

  // 3. Fill code into inputs
  await page.fill('input#verification-code', auth.event!.otpCode!);
  await page.click('button#submit-verification');

  // 4. Assert logged-in state
  await expect(page.locator('h1')).toContainText('Account Overview');
});

Pattern 2: Stagehand & Browser Use Autonomous Agents

When using Stagehand or Browser Use agents, query Sentry Relay when the agent detects a 2FA or magic-link challenge:

import { Stagehand } from '@browserbasehq/stagehand';
import { SentryRelay } from '@sentryrelay/sdk';

async function runAutonomousWorkflow() {
  const stagehand = new Stagehand({ env: 'BROWSERBASE' });
  await stagehand.init();

  const relay = new SentryRelay({
    apiKey: process.env.SENTRY_RELAY_API_KEY!,
  });

  const page = stagehand.page;
  await page.goto('https://portal.saas-vendor.com/login');

  // Use natural language actions to log in
  await page.act({ action: 'Type "agent-prod@in.sentryrelay.com" into email field and click Continue' });

  // Wait for 2FA screen to render
  await page.waitForSelector('text=Enter the 6-digit code');

  // Fetch the latest OTP from Sentry Relay
  const auth = await relay.wait('agent-prod', {
    channel: 'email',
    timeout: 30,
  });

  // Supply the code to the AI agent
  await page.act({ action: `Enter ${auth.event?.otpCode} into verification input and submit` });

  await stagehand.close();
}

Pattern 3: Cloud KMS-Vaulted TOTP Passcodes (≤4s Rollover Protection)

For services that enforce Google Authenticator or RFC 6238 TOTP (e.g., GitHub, AWS IAM, Okta), generate live 6-digit passcodes on-demand. If a 30-second window has <4s remaining, Sentry Relay automatically waits out the rollover so slow LLM browser steps never type an expired code:

import { SentryRelay } from '@sentryrelay/sdk';

const relay = new SentryRelay({ apiKey: process.env.SENTRY_RELAY_API_KEY! });

// Auto-waits <4s if cycle is about to expire, guaranteeing a fresh 26-30s code
const token = await relay.totp.get('github-runner', 'github_auth');

console.log(`Generated TOTP: ${token.code} (valid for ${token.remainingSeconds}s)`);

await page.fill('input#app_totp', token.code);

Pattern 4: Co-Deploying Sentry Relay with 1Password & Browserbase

Many production startups use 1Password Service Accounts (@1password/sdk) or Browserbase Contexts to store static usernames and passwords, while relying on Sentry Relay when the portal triggers an out-of-band Email OTP, Magic Link, or Carrier SMS shortcode that 1Password cannot receive—achieving 100% unattended execution without human mobile approval prompts:

typescript
import { createClient } from '@1password/sdk';
import { SentryRelay } from '@sentryrelay/sdk';

const op = await createClient({
  auth: process.env.OP_SERVICE_ACCOUNT_TOKEN!,
  integrationName: 'unattended-worker',
  integrationVersion: '1.0.0',
});
const relay = new SentryRelay({ apiKey: process.env.SENTRY_RELAY_API_KEY! });

// 1. Pull static password from 1Password Service Account Vault
const password = await op.secrets.resolve('op://Production-Bots/Stripe-Portal/password');
await page.fill('input[name="email"]', 'ops@company.com'); // Auto-forwards 2FA to @in.sentryrelay.com
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]');

// 2. Intercept out-of-band SMS or Email 2FA challenge via Sentry Relay (<200ms)
const auth = await relay.wait('stripe-prod-bot', { sender: 'stripe.com', timeout: 30 });
if (auth.matched && auth.event?.otpCode) {
  await page.fill('input[name="code"]', auth.event.otpCode);
  await page.click('button[type="submit"]');
}

Production Best Practices

  • Enable burnAfterRead: true in Workspace Settings: Ensures that every OTP is atomically deleted from Redis inside the Lua claim script the moment your worker claims it, eliminating stale replays.
  • Set realistic timeout windows: Carrier SMS delivery averages 1.2–3.5 seconds; email takes 1.0–4.0 seconds. A timeout of 30–45s accommodates upstream sender retries.
  • Zero plaintext keys in Git: Store SENTRY_RELAY_API_KEY as an encrypted GitHub Actions / Cloud Run secret.