TOTP二要素ログインをテストする
ユースケース: 自分のアプリ(あるいはテストの前段でログインが必要なアプリ)のアカウントが認証アプリ — Google Authenticator形式の6桁コード — で保護されている。テストはこの画面を通過しなければならない。
なぜ難しいのか
Section titled “なぜ難しいのか”コードは30秒ごとに変わり、スマホの中にあります。しかしTOTPは公開されたアルゴリズム(RFC 6238)です。スマホが知っているのは共有シークレットだけ — テストも同じシークレットを知っていれば、同じコードを計算できます。
npm install -D otplib登録時にシークレットを取得する
Section titled “登録時にシークレットを取得する”2FAセットアップ時に表示されるQRコードには、シークレットを含む otpauth:// URIがエンコードされています。ほぼすべてのアプリが「スキャンできない場合は手動で入力」用の文字列も表示するので、それを取得します:
import { test, expect } from '@playwright/test';import { authenticator } from 'otplib';
test('enroll TOTP and log in with it', async ({ page }) => { await page.goto('/account/security'); await page.getByRole('button', { name: 'Enable 2FA' }).click();
// QRコードの横にある手動入力用シークレット。 const secret = (await page.getByTestId('totp-secret').innerText()).replace(/\s/g, '');
// 認証アプリと同じ要領で登録を確定する。 await page.getByLabel('Verification code').fill(authenticator.generate(secret)); await page.getByRole('button', { name: 'Verify' }).click(); await expect(page.getByText('Two-factor enabled')).toBeVisible();
// 以後のログインはコードを再計算するだけ。 await page.goto('/logout'); await page.goto('/login'); await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('hunter2'); await page.getByRole('button', { name: 'Sign in' }).click(); await page.getByLabel('Verification code').fill(authenticator.generate(secret)); await page.getByRole('button', { name: 'Verify' }).click(); await expect(page).toHaveURL('/dashboard');});事前にプロビジョニング済みのテストアカウントなら、シークレットをパスワードと一緒にシークレットマネージャーに保存しておけば、登録画面のスクレイピングは不要です。
毎回UIからログインしない
Section titled “毎回UIからログインしない”2FAはUIログインを高コストにします。セットアッププロジェクトで一度だけログインし、認証状態(storageState)を使い回しましょう:
// auth.setup.ts — 一度だけ実行されるawait page.context().storageState({ path: 'playwright/.auth/user.json' });
use: { storageState: 'playwright/.auth/user.json' },2FAのUI全体を通しで検証するテストをちょうど1本だけ残し、それ以外はログイン済みの状態から始めます。
ウィンドウの29秒目に生成されたコードは、送信中に期限切れになることがあります。ウィンドウの早い段階で生成することでこのフレークを回避します:
const msLeft = authenticator.timeRemaining() * 1000;if (msLeft < 3000) await page.waitForTimeout(msLeft); // 次のウィンドウまで待つawait page.getByLabel('Verification code').fill(authenticator.generate(secret));- 登録画面がQR画像しか表示しない(手動入力用文字列がない)場合は、要素をスクリーンショットしてQRライブラリ(例:
jsqr)でデコードし、otpauth://URIをパースしてください。 - サーバーの時計のずれはTOTPを双方向に壊します。多くのサーバーは±1ウィンドウを許容します。CIでコードが「ランダムに」失敗する場合は、まずランナーのNTP同期を確認してください。
- SMSベースの2FAは別のレシピです(TwilioのようなテストSMS受信箱を使います)。テストアカウントには無料でオフラインでも動くTOTPを推奨します。
- パスキー(WebAuthn)をテストする
- トランザクションメールをテストする — メールコードによるログインは、同じパターン+受信箱APIで実現できます。