Agentic browsers are crossing from demos into production workflows: research copilots logging into journals, sales agents filing updates in CRMs, and QA bots performing end-to-end checks. The bottleneck isn’t DOM clicking; it’s authentication. Modern login stacks, federation via FedCM, passkeys/WebAuthn, step-up 2FA, device-bound sessions, and anti-bot challenge layers, were all built to keep humans safe and botnets out. That’s a good thing. But it means “just automate sign-in” can backfire, be brittle, or worse, violate terms or security expectations if done naively.
This article gives a practical, opinionated drill-down for building a compliant, 2FA-safe sign-in pipeline for agentic browsers, covering Federated Credential Management (FedCM) prompts, WebAuthn/passkeys, SMS/email OTP handling, device-bound cookies and session refresh, anti-bot challenge alignment, and deterministic replay without credential exposure. It targets builders of internal agents, test automation, and first-party bots for properties you own or have explicit authorization to automate.
The thread throughout: cooperate with the platform’s security model. Avoid credential exposure. Prefer first-party APIs and official flows to brittle DOM scraping. And be explicit about the human-in-the-loop boundaries when user presence/verification is required.
Why agentic browser login is uniquely hard
- Web is moving to privacy-preserving identity and anti-abuse:
- Third-party cookies are deprecated; federated login is moving to FedCM.
- Anti-bot challenges increasingly rely on attestation and behavior signals instead of simple CAPTCHAs.
- Sessions are becoming device-bound to mitigate cookie theft.
- Strong authentication is the norm:
- Passkeys/WebAuthn require user presence (UP) and often user verification (UV).
- 2FA is ubiquitous; some factors are intentionally non-automatable.
- Browser automation runs into platform guardrails:
- FedCM prompt UI is browser-owned, not DOM.
- WebAuthn authenticators don’t expose private keys to scripts.
- Challenge/attestation systems can detect headless patterns.
The result: robust login automation requires a cooperative design across product, auth, and automation, not just a headless driver.
Design principles for 2FA-safe agentic sign-in
- Respect the security model
- Only automate on properties you own or have been explicitly authorized to automate.
- Do not bypass anti-bot or 2FA protections; integrate with supported mechanisms.
- Separate duties
- Split the “secure auth broker” (which can see secrets) from the “agent browser” (which should not). Use capability tokens instead of raw credentials.
- Minimize secret surface area
- No passwords in code or logs. Use secret managers and ephemeral tokens. Redact HAR traces.
- Prefer official APIs
- For identity: FedCM/OIDC, OAuth device flow, service accounts. For testing: DevTools virtual authenticators only in pre-prod.
- Determinism and replay under test conditions only
- Deterministic replay is fine for staging with virtual authenticators and synthetic OTP; in production, treat authentication as non-deterministic and user/event driven.
- Human-in-the-loop for presence/verification
- When UP/UV is required (passkeys), build a user confirmation step to mint a delegated session for the agent.
A reference architecture
+-------------------+ +---------------------------+ +----------------------+
| Orchestrator/LLM | RPC | Secure Auth Broker (SAB) | OIDC | Identity Provider |
| (agent brain) +-------->+ - FedCM/OIDC client +------->+ (IdP / FedCM IdP) |
| | | - OTP inlets (email/SMS) | +----------------------+
| | | - Passkey delegation |
+--------+----------+ +-----+---------------------+
| |
| | capability tokens / session lift
| v
| +------+--------------------+
| | Agent Browser Runtime |
| | (Playwright/Puppeteer) |
| | - No raw credentials |
| | - HAR redaction |
| +------+--------------------+
| |
| v
| +------+-----------------+
| | Target RPs / Sites |
| +-----------------------+
- Secure Auth Broker (SAB) handles interaction with IdPs, OTP channels, and passkey delegation. It produces a scoped, time-boxed capability (session cookie, OIDC token, or session lift) for the agent browser.
- Agent browser runs with minimal privileges and no knowledge of master secrets.
- Observability is split: fine-grained event logs in SAB, high-level task logs in the agent. Sensitive payloads are structured, redacted, and encrypted.
FedCM: handling federated prompts the right way
FedCM (Federated Credential Management) moves federated sign-in UI and decisions into the browser, reducing cross-site tracking and phishing risk. RPs call navigator.credentials.get({ identity: ... }); the browser shows an account chooser and issues tokens by talking directly to the IdP. The page itself doesn’t see third-party cookies or raw federated assertions.
Key implications for agents:
- The FedCM prompt is browser UI, not DOM; it won’t show up in
document.querySelector. - Headless testing can use DevTools Protocol’s FedCm domain to observe and select accounts, but this is intended for testing and may be gated/flagged.
- In production automation, prefer pre-authenticated sessions or service tokens instead of trying to “click FedCM.”
Practical patterns
- For first-party RPs you control: build a “session lift” endpoint. A human authenticates via FedCM in a normal browser, then clicks “Authorize agent,” which issues a same-site, time-boxed session for the agent user-agent. No agent sees the FedCM UI.
- For test environments: use Chrome DevTools Protocol (CDP) FedCm domain to drive the dialog deterministically.
- For third-party RPs: if they support OIDC device flow or OAuth, prefer those flows. Avoid DOM automation of FedCM on sites you don’t control.
Example: selecting a FedCM account in test (Chromium, Puppeteer + CDP)
Note: This is for CI/testing in your own staging environments. The FedCM DevTools domain and permissions may change; consult current DevTools Protocol docs.
jsimport puppeteer from 'puppeteer'; (async () => { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); // Create a CDP session to control FedCM test hooks const cdp = await page.target().createCDPSession(); // Enable FedCM devtools domain (for testing only) await cdp.send('FedCm.enable'); // Optionally, grant permission if your test needs it // await cdp.send('Browser.grantPermissions', { permissions: ['fedcm'], origin: 'https://rp.example.test' }); cdp.on('FedCm.dialogShown', async (evt) => { // evt.accounts = [{ accountId, email, name, picture }, ...] const first = evt.accounts[0]; console.log('FedCM dialog shown with accounts:', evt.accounts.map(a => a.email)); await cdp.send('FedCm.selectAccount', { dialogId: evt.dialogId, accountId: first.accountId }); }); await page.goto('https://rp.example.test/login'); await page.waitForNavigation({ waitUntil: 'networkidle0' }); // Persist the authenticated storage for reuse (test only) const client = await page.target().createCDPSession(); const storage = await client.send('Storage.getStorageKeyForFrame', { frameId: page.mainFrame()._id }); // In practice, use Playwright/Puppeteer storage state export helpers instead await browser.close(); })();
Caveats:
- Treat FedCM CDP hooks as test-only. Avoid shipping production agents that "click" FedCM prompts.
- If you operate both RP and IdP in test, you can preconfigure accounts for deterministic selection.
Passkeys/WebAuthn: handling user presence and verification
Passkeys are built on WebAuthn. They are excellent for security precisely because they require user presence (touch/biometric) and keep private keys locked in authenticator hardware or platform keystores. Automation has to respect this:
- In production, you cannot and should not extract passkey private material.
- Automation cannot fake user presence on real authenticators.
- DevTools virtual authenticators exist for testing only.
Patterns that work in practice
- Delegated session after a human UP/UV
- A human signs in once with a passkey. Your RP issues a short-lived, scope-limited agent session (e.g., via a first-party “Authorize Agent” button). The agent uses that cookie/token; it never touches WebAuthn.
- Proof-of-possession tokens (when using OAuth/OIDC)
- If your RP is an OAuth client, prefer a back-channel token mint with DPoP/MTLS to bind the token to a client key rather than a browser cookie. The SAB does the mint; the agent gets a bound token.
- Virtual authenticators and synthetic credentials in CI
- For staging, use CDP WebAuthn domain to add a virtual authenticator and pre-register a synthetic credential for deterministic login tests.
Example: WebAuthn virtual authenticator in CI (Chromium + CDP)
jsimport puppeteer from 'puppeteer'; (async () => { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); const cdp = await page.target().createCDPSession(); await cdp.send('WebAuthn.enable'); const { authenticatorId } = await cdp.send('WebAuthn.addVirtualAuthenticator', { options: { protocol: 'ctap2', transport: 'usb', hasResidentKey: true, hasUserVerification: true, isUserVerified: true } }); // Optionally, pre-provision a resident credential for deterministic login flows // (Shape simplified; consult DevTools Protocol docs for exact fields.) await cdp.send('WebAuthn.addCredential', { authenticatorId, credential: { credentialId: 'BASE64URL_ID', rpId: 'rp.example.test', privateKey: 'PEM_OR_DER_PRIVATE_KEY_FOR_TEST_ONLY', userHandle: 'BASE64URL_USER', signCount: 0, isResidentCredential: true } }); await page.goto('https://rp.example.test/webauthn-login'); // Continue test flow... await browser.close(); })();
Operational guidance:
- Do not use virtual authenticators in production. They bypass UP/UV, which is the security guarantee of passkeys.
- For real agent operations, rely on delegated sessions minted by a human or by a backend OAuth client you control.
2FA channels: TOTP, SMS, email OTP, push
Agents should not "own" 2FA factors. If your property requires 2FA, coordinate a channel that supports automation in a controlled way without weakening overall security.
Recommended patterns:
- Prefer TOTP for service accounts when possible
- Create a dedicated automation account with TOTP, stored in a hardware or HSM-backed secret manager. Rotate and audit.
- Brokered OTP retrieval for accounts you own
- For SMS/email, route inbound OTPs to a Secure Auth Broker service via provider APIs, not by scraping mailboxes.
- Enforce origin binding: only OTPs from whitelisted sender IDs and matching ongoing auth transactions are forwarded.
- Push-based 2FA with explicit user consent
- Let a human approve a login request on their device; the SAB then mints a short-lived session for the agent.
Example: minimal SMS OTP inlet (Twilio) for first-party staging only:
js// Express.js endpoint that Twilio calls on inbound SMS. Store no OTP beyond short TTL. import express from 'express'; const app = express(); app.use(express.urlencoded({ extended: false })); // In-memory short TTL store keyed by transactionId type Pending = { code: string, expiresAt: number }; const pending = new Map<string, Pending>(); app.post('/otp/sms/inlet', (req, res) => { const body = req.body.Body || ''; const from = req.body.From || ''; // Simple extraction; in production use robust parsing and origin checks const match = body.match(/\b(\d{6})\b/); if (match) { const code = match[1]; // Associate with a transaction using metadata you control (e.g., include txn ID in RP SMS template in staging) const txnId = req.body.To; // placeholder; design this explicitly pending.set(txnId, { code, expiresAt: Date.now() + 2 * 60 * 1000 }); } res.type('text/xml').send('<Response></Response>'); }); app.get('/otp/sms/poll', (req, res) => { const txnId = String(req.query.txnId || ''); const p = pending.get(txnId); if (!p || p.expiresAt < Date.now()) return res.status(404).json({}); res.json({ code: p.code }); }); app.listen(8080);
Important:
- Only for accounts and environments you control. Respect the RP’s ToS.
- Implement strict origin validation (sender IDs, message templates, and correlation IDs) to avoid OTP leakage.
- Never store OTPs longer than necessary; encrypt in transit; audit access.
Device-bound cookies and session lift/refresh
The industry is exploring device-bound sessions to mitigate cookie theft (e.g., Device Bound Session Credentials). Meanwhile, many RPs already bind sessions to device signals. Agents need to plan for this.
Strategies:
- Prefer first-party "session lift" for properties you own
- A human session in a normal browser uses a first-party endpoint to mint an agent session bound to the agent’s user-agent and origin. Scope it narrowly and short-lived.
- Use OAuth/OIDC where available
- Refresh tokens with rotation, PoP (DPoP/MTLS) binding, and token introspection give better control than raw cookies.
- Persist state per agent identity
- Use separate user data directories per agent persona to keep device characteristics stable.
Playwright example: persist storage state for an agent identity
tsimport { chromium, BrowserContext } from '@playwright/test'; async function getContextFor(identity: string): Promise<BrowserContext> { const browser = await chromium.launch(); const context = await browser.newContext({ storageState: `./state.${identity}.json`, userAgent: `MyAgent/1.0 (${identity})` }); return context; } (async () => { const ctx = await getContextFor('sales-bot-1'); const page = await ctx.newPage(); await page.goto('https://rp.example.com/app'); // If first run requires login, do it once (ideally via SAB session lift), then persist await ctx.storageState({ path: './state.sales-bot-1.json' }); })();
Session refresh:
- Detect 401/403 and intercept redirect-to-login; pause agent actions.
- Ask the SAB for a fresh capability (token/cookie). Replace storage state atomically.
- Avoid storing refresh tokens inside the agent. Let the SAB be the only holder of long-lived tokens.
Anti-bot challenges: align, don’t evade
Anti-bot providers (reCAPTCHA Enterprise, hCaptcha Enterprise, Cloudflare Turnstile, etc.) are there to protect your users and your infrastructure. Do not try to defeat them. Instead:
- Register your automation use case with the provider (for properties you own)
- Many providers support lower-friction modes for known traffic (IP ranges, service accounts, server-side assessments).
- Use server-side risk assessment APIs when supported
- Let the SAB call an assessment endpoint to get a token that the agent later presents to the RP, instead of solving interactive challenges.
- Apply Privacy Sandbox primitives where applicable
- Private State Tokens allow first parties to convey trust signals across navigations without tracking users. Consider them if you operate both sides.
- Human-in-the-loop fallback
- If a challenge is presented, route the event to a human approver in your org; on approval, mint a session lift for the agent. This preserves security guarantees and respects ToS.
What not to do:
- Don’t use third-party CAPTCHA “solvers” on sites you don’t control.
- Don’t instrument headless browsers to mimic human micro-movements specifically to evade detections. It’s brittle and often a ToS violation.
Deterministic replay without credential exposure
For agent development and CI, you want reproducible flows. For production, you want zero credential exposure. You can have both with environment scoping:
- Staging/CI environments
- Use virtual authenticators, synthetic identities, and seeded data.
- Record/replay deterministic network interactions (e.g., HAR) with sensitive headers redacted.
- Inject OTPs from a synthetic inlet; never from real inboxes.
- Production
- No HAR of auth endpoints; no raw tokens in logs.
- Agent never holds long-lived credentials; it requests short-lived capabilities from the SAB on demand.
Playwright HAR with redaction (staging only):
tsimport { chromium } from '@playwright/test'; (async () => { const browser = await chromium.launch(); const context = await browser.newContext(); // Redact sensitive headers before saving HAR await context.route('**/*', async (route) => { const headers = await route.request().allHeaders(); const scrubbed = { ...headers }; delete scrubbed['authorization']; delete scrubbed['cookie']; await route.continue({ headers: scrubbed }); }); await context.tracing.start({ screenshots: true, snapshots: true }); const page = await context.newPage(); await page.goto('https://rp.example.test/login'); // Perform deterministic login using virtual authenticator... await context.tracing.stop({ path: 'trace.zip' }); await context.close(); })();
Additionally, represent login steps at the capability level in your test specs: “Obtain agent session for user X” instead of “type password, click passkey button.” This avoids baking brittle UI steps into tests and mirrors production design.
Putting it together: an end-to-end flow
Goal: An agent needs to access a first-party dashboard protected by FedCM + passkey + possible OTP step-up, with anti-bot protections enabled.
- One-time human bootstrap
- Human logs into the dashboard on their normal browser via FedCM and passkey.
- They navigate to “Authorize Agent,” which initiates a back-channel call to the SAB.
- SAB verifies their session, performs a server-side risk assessment, and mints a short-lived agent session cookie scoped to the agent’s UA and IP range. Optionally binds via PoP.
- Agent execution
- Orchestrator starts a job and requests a capability from SAB for identity “user-123.”
- SAB returns a capability (cookie/token) with 15–30 minute TTL.
- Agent browser context is launched with a stable user data dir, applies the capability, and navigates to the dashboard.
- If the RP triggers a step-up (e.g., 2FA due to anomaly), the page redirects; the agent pauses, emits an event to SAB.
- SAB either routes approval to the human (push) or uses a pre-approved TOTP for the service account, then re-mints the capability.
- Agent resumes.
- Refresh/rotation
- Five minutes before TTL expiry, agent asks SAB for refresh. SAB rotates token/cookie and invalidates the old one.
- Observability
- Metrics: login success rate, step-up rate, token issuance latency, agent idle time during auth.
- Auditing: who authorized which agent, when, and for what scope.
Operational guardrails and hard-won tips
- Keep environments cleanly separated
- Use separate domains and IdP tenants for staging vs prod. Never let test credentials leak into prod logs.
- Treat the agent’s user-agent string as a first-class identity
- Stable UA and stable device profile reduce anomaly flags. Don’t randomize UA unless required.
- Headless vs headed modes
- Some anti-abuse layers penalize headless signatures. For first-party automation, prefer headed mode in a secure enclave or a real desktop VM.
- Don’t conflate identity
- Use dedicated automation accounts with least privilege. Avoid sharing human accounts with agents.
- “Clicking” login UIs is a last resort
- If you own the RP, build a proper session lift or token grant. It’s more robust and secure than DOM automation.
What to do when you don’t own the RP
Sometimes your agent must log into a third-party site.
- Prefer documented APIs or OAuth device flow if offered.
- If only browser login exists and ToS permits automation:
- Keep a human-in-the-loop: someone completes FedCM/passkey steps; the agent takes over post-login.
- Persist session state per agent identity; handle refresh by re-prompting the human when needed.
- If anti-bot blocks the agent, stop. Engage the provider for an enterprise arrangement or redesign the workflow.
Compliance, privacy, and logs
- Apply data minimization: capture only what’s necessary to troubleshoot.
- Encrypt traces at rest; restrict access via RBAC.
- Redact tokens/cookies in any trace or network logs. Build lint checks that fail CI if sensitive headers are present.
- Document ToS posture for every RP your agent touches.
Checklist: building a 2FA-safe sign-in pipeline
- Architecture
- Secure Auth Broker separated from agent runtime
- Capability tokens instead of sharing raw credentials
- Storage state isolated per agent identity
- FedCM
- Use session lift for first-party; CDP FedCm only in staging
- Avoid DOM automation of FedCM in production
- Passkeys/WebAuthn
- Human UP/UV with delegated session for production
- Virtual authenticator only in CI
- 2FA
- Prefer TOTP for automation accounts
- OTP inlet with strict origin binding and TTL
- Anti-bot
- Register automation use case; use server-side assessments
- Human fallback for interactive challenges
- Sessions
- Refresh via SAB; agent holds only short-lived capability
- Device stability (UA, storage), rotation policies
- Observability
- Metrics on auth outcomes; redacted traces
- Audit logs of who authorized what and when
References and further reading
- FedCM specification (WICG): https://wicg.github.io/FedCM/
- Chrome status on FedCM: https://chromestatus.com/feature/5739734405267456
- DevTools Protocol FedCm domain (testing): https://chromedevtools.github.io/devtools-protocol/tot/FedCm/
- WebAuthn Level 3 (W3C): https://www.w3.org/TR/webauthn-3/
- Passkeys developer guides: https://passkeys.dev/
- DevTools Protocol WebAuthn domain: https://chromedevtools.github.io/devtools-protocol/tot/WebAuthn/
- Private State Tokens (Privacy Sandbox): https://developer.chrome.com/docs/privacy-sandbox/private-state-tokens/
- Device Bound Session Credentials (background/proposal): https://developer.chrome.com/blog/device-bound-session-credentials/
- OAuth 2.0 DPoP (IETF): https://datatracker.ietf.org/doc/draft-ietf-oauth-dpop/
- reCAPTCHA Enterprise assessment API: https://cloud.google.com/recaptcha-enterprise/docs/assessments
- Cloudflare Turnstile: https://developers.cloudflare.com/turnstile/
Closing perspective
The right way to let agents sign in is to treat authentication as a capability grant, not a sequence of clicks. Use FedCM in the user’s browser, not the agent’s. Use passkeys with genuine user presence, then delegate narrowly scoped sessions. For OTPs, route them through a broker with strict correlation, not through scraped inboxes. And for anti-bot controls, work with the platform: pre-register, assess, and fall back to a human when required.
Do this, and you’ll end up with a login pipeline that’s more reliable, faster to maintain, and, most importantly, keeps users and data safe while giving your auto-agents the access they legitimately need.
