otplib × Playwright でGoogleアカウントのTOTP認証を突破してログインする

前提(重要): これは自分が管理するアカウントのE2E(End-to-End)テスト自動化を想定した手法。第三者のアカウントへの不正ログインは不正アクセス禁止法に抵触するためNG。CI/CDでGoogleログインを伴う画面をテストしたい、といった正当なユースケースで使う。

全体像:なぜ「自動化」が可能なのか

通常、TOTPの6桁コードはスマホの認証アプリが生成する。これを人間が目で見て手入力するのが普通のフロー。

しかしTOTPコードは「秘密鍵 + 現在時刻」だけで計算できる(前述の生成原理)。つまり秘密鍵さえ手元にあれば、スマホがなくてもプログラムが同じ6桁コードを生成できる。この「秘密鍵からコードを計算する」役割を担うのが otplib(Node.jsのTOTPライブラリ)。

  ┌─────────────────────────────────────────────────────────┐
  │  普段の人間のログイン                                     │
  │                                                         │
  │   スマホアプリ ──[6桁コードを目視]──> 人間 ──[手入力]──> Google │
  └─────────────────────────────────────────────────────────┘
                          ↓ これを丸ごとプログラムに置き換える
  ┌─────────────────────────────────────────────────────────┐
  │  自動化したログイン                                      │
  │                                                         │
  │   otplib ──[6桁コードを計算]──> Playwright ──[自動入力]──> Google │
  │     ▲                                                   │
  │   秘密鍵(環境変数などで安全に保管)                       │
  └─────────────────────────────────────────────────────────┘
  • otplib … 秘密鍵と現在時刻から6桁コードを生成する(スマホ認証アプリと同じ計算をするライブラリ)
  • Playwright … ブラウザを自動操作するツール。メール・パスワード・6桁コードの入力やボタンクリックを人間の代わりに実行する

事前準備:秘密鍵(TOTP Secret)の入手

Googleアカウントで2段階認証の「認証システムアプリ」を登録する際、通常はQRコードを表示してスマホで読み取る。このとき画面の「QRコードをスキャンできない場合」というリンクを押すと、**Base32形式の文字列(秘密鍵)**が表示される。

例: JBSW Y3DP EHPK 3PXP

この文字列こそがotplibに渡す秘密鍵。一度きりしか表示されないので控えておく。漏洩すると誰でもコードを生成できてしまうため、Gitにコミットせず環境変数やシークレットマネージャーで管理する。

QRコードの実体は otpauth://totp/...?secret=JBSWY3DPEHPK3PXP&... というURL。この secret= の値がBase32の秘密鍵そのもの。

コード例

import { chromium } from 'playwright';
import { authenticator } from 'otplib';

const EMAIL = process.env.GOOGLE_EMAIL!;
const PASSWORD = process.env.GOOGLE_PASSWORD!;
const TOTP_SECRET = process.env.GOOGLE_TOTP_SECRET!; // Base32の秘密鍵

const browser = await chromium.launch();
const page = await browser.newPage();

// ① ログイン画面へ
await page.goto('https://accounts.google.com/signin');

// ② メールアドレス入力 → 次へ
await page.fill('input[type="email"]', EMAIL);
await page.click('#identifierNext');

// ③ パスワード入力 → 次へ(第1要素)
await page.fill('input[type="password"]', PASSWORD);
await page.click('#passwordNext');

// ④ TOTPコードを「その場で」生成(第2要素)
//    authenticator.generate() が 秘密鍵 + 現在時刻 から6桁を計算する
const token = authenticator.generate(TOTP_SECRET); // 例: "847392"

// ⑤ 6桁コードを入力して認証完了
await page.fill('input[type="tel"]', token);
await page.click('#totpNext');

await page.waitForURL('https://myaccount.google.com/**');
console.log('ログイン成功');