If you have ever built end-to-end (E2E) testing pipelines or automation scripts with Puppeteer, Playwright, or Selenium, you have likely hit the dreaded roadblock: Two-Factor Authentication (2FA).
While SMS-based verification is cumbersome to automate, Time-based One-Time Passwords (TOTP)—the 6-digit codes used by Google Authenticator, Authy, and 1Password—are fully deterministic. Instead of attempting to "bypass" the security layer, you can programmatically satisfy the challenge by acting as the authenticator device directly within your script.
Here is a step-by-step guide to extracting the TOTP secret, generating valid tokens on the fly using Node.js, and submitting them during automated login runs.
How TOTP Actually Works
TOTP is defined by RFC 6238. It relies on two pieces of information:
- A shared secret key established during initial setup.
- The current Unix timestamp (divided into 30-second windows).
Because the calculation uses a predictable time step, any client with access to the shared secret key can calculate the exact 6-digit token that Google or any other service expects at that exact second.
Step 1: Extract the Shared Secret Key
When configuring an authenticator app on an account:
- Navigate to the account's security settings and select Set up Authenticator.
- A QR code modal will appear.
- You have two options to retrieve the secret:
- Direct text: Most providers offer a link like "Can't scan it?" or "Set up by key", which reveals the Base32-encoded secret string directly.
-
Decoding the QR code: If only the QR code is displayed, scan or decode the image using any standard QR decoder tool or CLI (e.g.,
zbarimg).
A decoded TOTP QR code yields an otpauth:// URI:
otpauth://totp/Google:user@example.com?secret=NMGJRKZOEJRSYKKTBLDU7QRJFPIM6EDB7YOIEC6ALXUC76LHVD5Q&issuer=Google
Extract the value assigned to the secret query parameter. This Base32 string is what generates your codes.
Step 2: Install otplib
Install otplib, a reliable, zero-dependency TOTP/HOTP implementation for JavaScript/TypeScript:
npm install otplib
Step 3: Generate the TOTP Token
Using otplib, generating a current token takes two lines of code:
import { authenticator } from 'otplib';
// Retrieve your secret from environment variables (never hardcode in production)
const secret = process.env.TOTP_SECRET || 'NMGJRKZOEJRSYKKTBLDU7QRJFPIM6EDB7YOIEC6ALXUC76LHVD5Q';
// Generate the 6-digit time-based code
const token = authenticator.generate(secret);
console.log(`Current 2FA Token: ${token}`);
authenticator.generate() hashes the secret with the current time bucket (Math.floor(Date.now() / 1000 / 30)) using HMAC-SHA1 to produce the active 6-digit code.
Step 4: Plug the Token into Your Automation Flow
Below is an end-to-end example using Playwright to handle the 2FA input prompt:
import { chromium } from 'playwright';
import { authenticator } from 'otplib';
async function runAutomation() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
// 1. Primary authentication (Username / Password)
await page.goto('https://example.com/login');
await page.fill('input[type="email"]', process.env.LOGIN_EMAIL);
await page.fill('input[type="password"]', process.env.LOGIN_PASSWORD);
await page.click('button[type="submit"]');
// 2. Wait for the 2FA challenge field
const otpInputSelector = 'input[name="totpPin"], input[autocomplete="one-time-code"]';
await page.waitForSelector(otpInputSelector, { timeout: 10000 });
// 3. Generate a fresh TOTP code
const secret = process.env.TOTP_SECRET;
const token = authenticator.generate(secret);
// 4. Fill the token and submit
await page.fill(otpInputSelector, token);
await page.keyboard.press('Enter');
// 5. Verify navigation into authenticated area
await page.waitForURL('**/dashboard');
console.log('Successfully authenticated past 2FA!');
await browser.close();
}
runAutomation().catch(console.error);
Handling the 30-Second Drift Window
One edge case in CI/CD pipelines is running authenticator.generate() right at the end of a 30-second window (e.g., at second 29.8). By the time your browser fills the input and submits, the server's clock may have rolled over.
To make runs deterministic, check remaining window validity using authenticator.timeRemaining():
import { authenticator } from 'otplib';
async function getStableToken(secret) {
// If less than 3 seconds remain in the current cycle, wait for the next step
if (authenticator.timeRemaining() < 3) {
await new Promise((resolve) => setTimeout(resolve, 4000));
}
return authenticator.generate(secret);
}
Security Best Practices for Automation Accounts
- Dedicated Test Accounts: Never run automated scripts or CI/CD pipelines against personal accounts. Provision dedicated service accounts.
-
Secrets Management: Keep the Base32 secret string in environment variables (
process.env.TOTP_SECRET) or your CI secret store (GitHub Secrets, AWS Secrets Manager, Vault). Never commit it to git history. -
Session Persistence: When running high-frequency tests, avoid logging in from scratch every run. Save and reuse browser storage state (
storageStatein Playwright) to bypass login flows entirely on repeated runs.
Top comments (0)