近年のセキュリティ強化の流れに伴い、ログイン時にMFA(多要素認証)を必須とするWebサイトが増加しています。MFAの認証要素にはいくつかの種類がありますが、本記事では「メールで配信されるワンタイムパスワード(OTP)」を用いたMFAログインを、New Relic SyntheticsのScripted Browserで自動化・監視するための実装手順をご紹介します。

解決策の全体像とアーキテクチャ

New Relic SyntheticsのScripted Browser(以下、Scripted Browser)は内部でSeleniumを採用しているため、ブラウザ経由でアクセスして画面上に表示できる情報であれば、基本的にどのような情報でも取得が可能です。

また、Syntheticsの実行環境では複数タブを切り替えたアクセス(マルチタブ操作)がサポートされています。この特性を活かし、ログインページとは別のタブでメール受信用のWeb APIを開いてワンタイムパスワードを取得し、その値を元のタブのログインページに入力する、というアプローチをとっています。

なお、メールからワンタイムパスワードをブラウザ経由で取得する手段として、Gmailおよび Google Apps Script (GAS) を組み合わせて利用します。

 

セキュリティに関する注意点

本記事で紹介するサンプルスクリプトでは、ワンタイムパスワードを取得するためのGAS APIに対して追加の認証を施していません。そのため、APIのURLが漏洩すると、第三者がAPI経由で情報を取得できてしまうリスクがあります。

安全に本検証・運用を行うために、以下のセキュリティ対策を強くお勧めします。

  • 独立した専用アカウントの使用: 機微な情報や個人情報が一切保存されていない、本監視専用に独立したGoogleアカウントを新規に作成して利用すること。
  • 追加のセキュリティ実装: 共有パスワード(固定トークン)をリクエストクエリに含めるなど、簡易的な認証ロジックを実装すること。

免責事項

本記事に含まれるサンプルスクリプトには、生成AIを用いて出力されたコードが含まれています。動作検証は実施しておりますが、コードに不具合や脆弱性が全く含まれていないことを保証するものではありません。万が一、不具合やセキュリティ上の問題が確認された場合の修正対応は、ベストエフォートとなりますのであらかじめご容赦ください。

1. メールからワンタイムパスワードを抽出するGASコード

まずは、GASを用いてGmailの特定メールからワンタイムパスワードを抽出する簡易APIを作成します。

/**
 * 静的ページからのGETリクエストを受け付け、最新のOTPをテキストで返却する
 */
function doGet(e) {
  let responseData = '------';
  
  try {
    // 過去1分間のタイムスタンプを計算
    const oneMinuteAgo = Math.floor((Date.now() - 60 * 1000) / 1000);


    // TODO クエリー条件は、ワンタイムパスワード発行対象のサービスから送られるメールの仕様に応じてカスタマイズしてください。
    // 「【ログイン】ワンタイムパスワードのご案内」という件名のメールを探す
    const searchQuery = `is:unread subject:"【ログイン】ワンタイムパスワードのご案内" after:${oneMinuteAgo}`;
    
    // Gmailを検索
    const threads = GmailApp.search(searchQuery, 0, 1);
    
    if (threads.length === 0) {
      console.log('過去1分以内に届いた未読のOTPメールは見つかりませんでした。');
    } else {
      const messages = threads[0].getMessages();
      const latestMessage = messages[messages.length - 1];
      const body = latestMessage.getPlainBody();

      // TODO 本文からの抽出方法はワンタイムパスワード発行対象のサービスから送られるメールの仕様に応じてカスタマイズしてください。
      // 本文から6桁の数字を探す
      const otpMatch = body.match(/\d{6}/);
      
      if (!otpMatch) {
      console.log('メールは見つかりましたが、OTPコードを抽出できませんでした。');
      } else {
        const otpCode = otpMatch[0];
        
        // ★OTPが見つかったので、対象のメールを既読にする
        latestMessage.markRead();

        responseData = otpCode;
      }
    }
  } catch (error) {
      console.log('error: ' + error.toString());
    responseData = { success: false, message: error.toString() };
  }

  return ContentService.createTextOutput(responseData)
    .setMimeType(ContentService.MimeType.TEXT);
}

💡 実装時のポイント
Gmailから目的のメールを検索する際のクエリ条件や、メール本文からのワンタイムパスワード抽出ロジックは、対象サービスから配信されるメールのフォーマット(件名、本文構成など)に合わせて適切に調整してください。

GAS APIの基本動作:

デフォルト設定では、過去1分以内に受信した特定のテキスト(例:「------」)を含むメールを検索し、本文からワンタイムパスワードとして「6桁の数字の文字列」が検出された場合にその値を返却するシンプルな動作となっています。

このGASコードを 「Webアプリケーション(全員に公開、認証なし)」 としてデプロイし、生成されたURLにブラウザからアクセスすることで、API経由で簡単にワンタイムパスワードが取得できるようになります。

2. Synthetics監視のメインスクリプト(Scripted Browser)

続いて、Syntheticsテスト側で実行するメインのスクリプトです。

// 省略
  const originalWindow = await $webDriver.getWindowHandle();
  const baseWindowCount = (await $webDriver.getAllWindowHandles()).length;
// 省略


// 新規タブを開いて OTP を取得し、元のタブに戻る
async function fetchOtpInNewTab(originalWindow, baseWindowCount) {
  await $webDriver.switchTo().newWindow('tab');

  // 新規タブが作成されるのを待つ
  await $webDriver.wait(
    async () =>
      (await $webDriver.getAllWindowHandles()).length === baseWindowCount + 1,
    CONFIG.timeouts.newTab
  );

  const otpValue = await getOtpValue();
  await $webDriver.switchTo().window(originalWindow);
  return otpValue;
}


// OTP 発行用 URL をポーリングし、6桁の OTP を取得する
async function getOtpValue() {
  await $webDriver.get(CONFIG.otpCodeUrl);

  const startTime = Date.now();
  log.otp(`${CONFIG.otpCodeUrl} の監視を開始します...`);

  while (Date.now() - startTime < CONFIG.timeouts.otpPoll) {
    const rawText = (
      await $webDriver.findElement($selenium.By.tagName('body')).getText()
    ).trim();
    log.otp(`取得値: "${rawText}"`);

    if (CONFIG.otp.pattern.test(rawText)) {
      log.otp('成功: 6桁のOTPを発見しました。');
      return rawText;
    }

    await delay(CONFIG.otp.intervalMs);
    await $webDriver.navigate().refresh();
  }

  throw new Error(
    '1分間待機しましたが、有効な6桁のOTPを取得できませんでした(タイムアウト)。'
  );
}

// 省略

メインスクリプトの基本動作:

  1. fetchOtpInNewTab() 関数の処理
    新規タブを立ち上げ、先ほどデプロイしたGAS APIのページ(URL)を開きます。
  2. getOtpValue() 関数の処理 (リトライ処理)
    GAS APIにアクセスし、メール受信が完了してワンタイムパスワードが取得できるまで、ブラウザのリロード(再読み込み)を複数回繰り返します。一定時間内に取得できない場合は、タイムアウトとなりテストは失敗(Fail)となります。
  3. 元のタブへフォーカスを復帰
    ワンタイムパスワードが正常に取得できた場合、フォーカスをログイン画面が開いている元のタブに戻し、取得したパスワードをログイン画面に入力します。

全体のコードについて

ログイン画面の操作部分を含む全体のコードは、GitHubにて一般公開しています。実装の全容や詳細な流れを確認されたい場合は、ぜひ下記のリンク先をご参照ください。

https://github.com/newrelickk/synthetics-email-mfa-login