4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MFA認証があってもE2Eテストを諦めない - PlaywrightでTOTPを自動入力するまでの道のり

Last updated at Posted at 2025-12-10

image.png

はじめに

E2Eテストを書いていると、こんなモヤモヤに出会いませんか?

「モックでは通るけど、本番やステージングで本当に動くのか…?」
「検証環境で試したいのに、MFAが邪魔してログインできない…」

この記事は、そんな悩みを抱えた自分が、検証環境のテスト用アカウントを対象に、 TOTP(ワンタイムパスワード)認証を自動化 して、実環境に近いE2Eテストを回せるようになるまでの記録です。

対象読者

  • E2Eテストを実環境(QA/Staging等)で動かしたい人
  • MFA認証がテスト自動化の壁になっている人
  • TOTPの仕組みを理解したい人

技術スタック

技術 用途
Playwright E2Eテストフレームワーク
TypeScript 実装言語
Node.js crypto HMAC-SHA1によるTOTP生成
Prism OpenAPIモックサーバー(比較用)

この記事でわかること

  1. モックテストの限界と実環境テストの必要性
  2. TOTP認証を突破する3つの選択肢
  3. TOTPの仕組み(RFC 6238)
  4. TypeScriptでのTOTP実装方法
  5. 実装時のハマりポイントと対処法

本編

1. モックだけじゃ怖かった話

image.png

最初は Prism のモックサーバーでサクサクE2Eを進めていました。

✅ 良かったところ
   - ローカル完結で速い
   - バックエンドが無くても開発できる

❌ もやっとしたところ
   - ダミーデータ固定で現実味が薄い
   - 本物のAPIとは微妙に挙動が違う
   - 「本番で本当に動くの?」の不安が残る

検証環境につないで、リアルな挙動でテストしたい!


2. 立ちはだかったMFAの壁

image.png

検証環境に向けてテストを書き換えた途端、ログイン画面で足が止まりました。

メール & パスワード入力  ✅ ここは余裕
       ↓
MFAコード入力            😱 ここで詰まる

スマホの Google Authenticator に出る6桁コード、これを自動で入れられないと話が進みません。


3. どう突破するかを考えた

image.png

候補を洗い出してみました。

方法 メリット デメリット
テストユーザーだけMFA無効化 実装が一番ラク セキュリティポリシー違反の香り、本番と挙動がズレる
認証スキップ用バックドアAPI 柔軟 バックエンド改修が必要 & セキュリティリスク大
秘密鍵からTOTPを自前生成 本番と同じフローで試せる、改修不要 秘密鍵の管理が必要

今回は 「秘密鍵から自前でTOTPを生成する」 を選びました。理由はシンプルで、余計な改修をしたくなかったし、本番に限りなく近い流れでテストしたかったからです。


4. そもそもTOTPって何?

TOTP = Time-based One-Time Password(時間ベースのワンタイムパスワード)

Google AuthenticatorやMicrosoft Authenticatorで表示される、あの6桁のコードです。

┌─────────────────────────────────────────────────────────────┐
│  📱 認証アプリ (Google Authenticator)                         │
│                                                             │
│  秘密鍵: "JBSWY3DPEHPK3PXP"  ← 最初にQRコードで共有された        │
│          +                                                  │
│  現在時刻: 14:30:15                                          │
│          ↓                                                  │
│  🔐 暗号計算(HMAC-SHA1)                                     │
│          ↓                                                  │
│  6桁コード: 123456                                           │
└─────────────────────────────────────────────────────────────┘

💡 ポイント

秘密鍵と現在時刻さえあれば、誰でも同じコードが作れる!

つまり、テストコードでも生成可能なんです。

30秒ごとに変わる仕組み

14:30:00 〜 14:30:29  →  カウンター = 1000  →  コード "123456"
14:30:30 〜 14:30:59  →  カウンター = 1001  →  コード "789012"
14:31:00 〜 14:31:29  →  カウンター = 1002  →  コード "345678"

時刻をカウンター(30秒ごとに1増える数値)に変換し、そこから6桁コードを生成しています。

秘密鍵はどこから持ってくる?

image.png

一般的なMFAサービスでは、初回設定時にQRコード(otpauth://...形式のURI)が表示されます。このURIに含まれるBase32エンコードされた文字列が「秘密鍵」です。

  • ローカル開発では .env ファイルに秘密鍵を設定し、テスト実行時に読み込みます
  • CI環境では GitHub Actions Secrets や AWS Secrets Manager などの環境変数ストアに登録し、コード側は process.env 経由で参照します(リポジトリに直書きしない)
  • 秘密鍵はQR発行時の一度きりしか表示されないことが多いので、紛失した場合はMFA設定をリセットして再発行する必要があります

5. TypeScriptでTOTPを作る

流れはこうなります。

秘密鍵(Base32) → デコード → HMAC-SHA1(カウンター) → 動的切り詰め → 6桁に整形

サンプルコードはこんな感じ(Playwrightのテストでそのまま使えます)。

import { createHmac } from 'crypto';

export const generateTotp = (secret: string): string => {
  const period = 30; // 30秒区切り
  const counter = BigInt(Math.floor(Date.now() / 1000 / period));

  const decodedSecret = decodeBase32(secret);

  const hmac = createHmac('sha1', decodedSecret);
  hmac.update(counterToBuffer(counter));
  const digest = hmac.digest();

  const offset = digest[digest.length - 1] & 0x0f; // 動的切り詰め
  const code =
    ((digest[offset] & 0x7f) << 24) |
    ((digest[offset + 1] & 0xff) << 16) |
    ((digest[offset + 2] & 0xff) << 8) |
    (digest[offset + 3] & 0xff);

  return (code % 1000000).toString().padStart(6, '0');
};

「カウンターを作る → HMACを取る → 6桁に整形する」だけ。ライブラリに頼らずに済むので、挙動を把握しやすいのも良いところです。


6. 詰まったポイントと対処法

タイミングずれ問題

生成したコードを入力する間に30秒が切り替わると、認証に失敗します。

14:30:28 に生成 → "123456"
14:30:31 に入力 → もう期限切れ…

解決:残り時間を見て、危なそうなら待つ

const periodMs = 30 * 1000;
const timeRemaining = periodMs - (Date.now() % periodMs);

if (timeRemaining < 5000) {
  await page.waitForTimeout(timeRemaining + 1000); // 1秒余裕を持たせる
}

const otp = generateTotp(secret);

これで「ギリギリでコードが変わる」問題をほぼ潰せました。


7. Playwrightで組み込む

実際にPlaywrightのテストに組み込むには、globalSetup を使います。

// globalSetup.ts - テスト実行前に1度だけ走る認証処理
import { chromium } from '@playwright/test';
import { generateTotp } from './totp';

export default async function globalSetup() {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // ログイン
  await page.goto('https://your-app.com/');
  await page.getByRole('textbox', { name: /email/i }).fill(process.env.E2E_EMAIL!);
  await page.getByRole('textbox', { name: /password/i }).fill(process.env.E2E_PASSWORD!);
  await page.getByRole('button', { name: /sign in/i }).click();

  // MFA画面でTOTPを入力
  await page.waitForURL(/mfa/i);
  const otp = generateTotp(process.env.E2E_TOTP_SECRET!);
  await page.getByPlaceholder(/code/i).fill(otp);
  await page.getByRole('button', { name: /verify/i }).click();

  // セッション保存 → 2回目以降はログイン不要
  await page.context().storageState({ path: 'storageState.json' });
  await browser.close();
}

ポイント:

  • globalSetup でテスト前に1度だけ認証処理を実行
  • storageState で認証状態(Cookie、localStorage等)を保存
  • 各テストはログイン済みの状態で開始できる

この方法でうまくTOTPを突破することができました!


8. セッションを保存してさらにラクする

毎回TOTPを生成するのは面倒なので、Playwrightの storageState で認証後セッションを保存します。

await page.context().storageState({ path: 'storageState.json' });
  • 2回目以降はログインステップが丸ごとスキップ
  • 実行時間も短縮
  • TOTPの計算も最小限

9. セキュリティ上の注意事項

⚠️ この手法を使う際は、以下の点に十分注意してください。

絶対に守るべきこと

  1. 本番環境の秘密鍵は使わない

    • テスト用に専用のアカウントを作成し、そのアカウントの秘密鍵のみを使用する
    • 本番アカウントの秘密鍵をテストコードに含めると、漏洩時のリスクが甚大
  2. テスト用アカウントは最小権限にする

    • E2Eテストに必要な最低限の権限のみを付与
    • 管理者権限や機密データへのアクセス権は不要
  3. 秘密鍵は安全に管理する

    • Gitリポジトリに直書きは絶対NG
    • .env ファイルは .gitignore に追加
    • CI/CDでは GitHub Actions Secrets、AWS Secrets Manager、HashiCorp Vault などを活用
# .env(リポジトリには含めない)
TOTP_SECRET_FOR_TESTS=XXXXXXXXXXXXXXXX

おわりに

「MFA認証があるからE2Eテスト無理かもな...」と思っていましたが、「秘密鍵からTOTPを自前生成する」方法で、仕組みを理解したら意外とシンプルに解決できました。
同じ壁にぶつかっている人の参考になれば嬉しいです!


参考リンク

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?