CloudWatch Synthetics で 2要素認証(2FA/MFA)が必要な Web アプリのブラウザテストを自動化してみた

CloudWatch Synthetics で 2要素認証(2FA/MFA)が必要な Web アプリのブラウザテストを自動化してみた

Clock Icon2025.01.24

こんにちは。テクニカルサポートチームのShiinaです。

はじめに

2要素認証(2FA/MFA)が設定されたアプリケーションのテスト自動化は、ワンタイムパスワードトークンの手動入力が必要なため困難です。
今回、AWS Amplify UI Authenticator を利用した Web アプリケーションに対し、CloudWatch Synthetics を用いて2要素認証を含むテストを自動化してみました。
本記事では、Secrets Manager を利用した安全な認証情報の管理と、Playwright を活用した認証プロセス操作を自動化する方法をご紹介します。

前提

  • Canary ランタイムには Playwright を使用します。
  • Amplify UI Authenticator コンポーネントを使用したアプリケーションの認証プロセスを自動化する方法を取り扱います。
    React-App-01-24-2025_11_58_AM

使用するライブラリ

  • totp-generator
    TOTP (Time-based One-Time Password) を生成するためのライブラリ

  • @aws-sdk/client-secrets-manager
    Secrets Manager サービスとやり取りするためのクライアントライブラリ

設定の流れ

認証情報(ユーザー名、パスワード、TOTP キー)を安全に管理するため、Secrets Manager にシークレットとして登録します。
Playwright ランタイムの Canary スクリプトでシークレット値を参照できるよう、必要な IAM ロールを設定します。
パッケージ、設定ファイル、スクリプトをパッケージ化し、Canary の作成を行います。

設定手順

1.Secrets Manager へのシークレット登録

AWS Secrets Manager メニューの「シークレット」より、「新しいシークレットを保存する」を選択します。
次のように設定の上、シークレットを登録します。

  • シークレットタイプ:その他のシークレットのタイプ
  • キー/値のペア:プレーンテキスト
    {"":""}をクリアし、下記形式で認証情報を入力します。
{
    "username": "<your_username>",
    "password": "<your_password>",
    "totpSecret": "<your_totp_secretkey>"
}
  • シークレットの名前
     任意の名前を入力します。
  • ローテーションを設定
    オプションの設定不要です。
    新しいシークレットを保存する1

新しいシークレットを保存する2

2.IAM ロール設定

Canary を実行するために必要なポリシーを設定したロールの作成を行います。
IAM メニューの「ロール」より、「ロールの作成」を選択します。
以下の設定を行い、ロールを作成します。

  • サービスまたはユースケース:Lambda
  • ユースケース:Lambda
  • 許可ポリシー:なし
  • ロール名:SyntheticsRole

ロール作成後、インラインポリシーにて必要なポリシーの追加を行います。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cloudwatch:PutMetricData",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "cloudwatch:namespace": "CloudWatchSynthetics"
                }
            }
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "logs:CreateLogStream",
                "s3:ListAllMyBuckets",
                "s3:GetBucketLocation",
                "logs:CreateLogGroup",
                "logs:PutLogEvents",
                "xray:PutTraceSegments"
            ],
            "Resource": "*"
        },
        {
            "Sid": "secret",
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "arn:aws:secretsmanager:ap-northeast-1:XXXXXXXXXXXX:secret:XXXXXXXXXXX"
        }
    ]
}

※ Secrets Manager の ARN はご自身のものに修正してください。
SyntheticsRole-IAM-Global-01-24-2025_12_32_PM

3.必要パッケージのインストール

任意の名前のプロジェクトディレクトリを作成します。

mkdir synthetics_function
cd synthetics_function

必要なパッケージをインストールします。

npm i @aws-sdk/client-secrets-manager
npm i totp-generator

4.Canary スクリプトの作成

index.mjs ファイルにスクリプトコードを記述し、配置します。
2要素認証プロセスを行うサンプルコードは次のとおりです。
・##your_secret_name## は Secrets Manager に登録したシークレット名に修正してください。
・##your_application_url## はアプリケーションまたはエンドポイント URL に修正してください。
・verifyLogin ステップはアプリケーションに沿ったもので実装ください。

index.mjs
import { synthetics } from '@amzn/synthetics-playwright';
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
import totp from 'totp-generator';

// Secrets Managerから認証情報を取得する関数
const getCredentials = async () => {
    const client = new SecretsManagerClient({ region: 'ap-northeast-1' });
    const command = new GetSecretValueCommand({
        SecretId: '##your_secret_name##'
    });

    try {
        const response = await client.send(command);
        return JSON.parse(response.SecretString);
    } catch (error) {
        throw error;
    }
};

// 2FAコードを生成する関数
const generate2FACode = (totpSecret) => {
    try {
        return totp(totpSecret);
    } catch (error) {
        throw error;
    }
};

// リトライ用のユーティリティ関数
const retry = async (fn, retries = 3, delay = 1000) => {
    let lastError;
    for (let i = 0; i < retries; i++) {
        try {
            return await fn();
        } catch (error) {
            lastError = error;
            if (i < retries - 1) {
                await new Promise(resolve => setTimeout(resolve, delay));
            }
        }
    }
    throw lastError;
};

export const handler = async (event, context) => {
        const screenshotOptions = {
        screenshotOnStepStart: true,
        screenshotOnStepSuccess: true,
        screenshotOnStepFailure: true
        };
    try {
        // 認証情報の取得
        const credentials = await getCredentials();
        const { username, password, totpSecret } = credentials;

        if (!username || !password || !totpSecret) {
            throw new Error('Missing required credentials');
        }
        // 2FAコードの生成
        const tfaCode = generate2FACode(totpSecret);

        // Launch a browser
        const browser = await synthetics.launch();
        // Create a new page
        const page = await synthetics.newPage(browser);
        await page.goto('##your_application_url##', {timeout: 30000});
        // Login Steps
        await synthetics.executeStep('enterUsername', async function () {
            await page.waitForSelector('input[placeholder="Enter your Username"]');
            await page.click('input[placeholder="Enter your Username"]');
            await page.fill('input[placeholder="Enter your Username"]', username);
        }, screenshotOptions);

        await synthetics.executeStep('enterPassword', async function () {
            await page.waitForSelector('input[placeholder="Enter your Password"]');
            await page.click('input[placeholder="Enter your Password"]');
            await page.fill('input[placeholder="Enter your Password"]', password);
        }, screenshotOptions);

        await synthetics.executeStep('clickSignIn', async function () {
            const signInButton = page.locator('xpath=//button[contains(text(), "Sign in")]');
            await signInButton.waitFor({ state: 'visible' });
            await signInButton.click();
        }, screenshotOptions);

        // 2FA Steps with retry
        await synthetics.executeStep('enter2FACode', async function () {
            await retry(async () => {
                await page.waitForSelector('input[placeholder="Code"]');
                await page.click('input[placeholder="Code"]');
                await page.fill('input[placeholder="Code"]', tfaCode);
            });
        }, screenshotOptions);

        await synthetics.executeStep('confirm2FA', async function () {
            const signInButton = page.locator('xpath=//button[contains(text(), "Confirm")]');
            await signInButton.waitFor({ state: 'visible' });
            await signInButton.click();
        }, screenshotOptions);

        // Verification with retry
        await synthetics.executeStep('verifyLogin', async function () {
            await retry(async () => {
                await page.waitForSelector(`h1:has-text("Hello ${username}")`, { timeout: 5000 });
                const heading = await page.textContent(`h1:has-text("Hello ${username}")`);
                if (!heading.includes(`Hello ${username}`)) {
                    throw new Error('Login verification failed');
                }
            });
        }, screenshotOptions);
    } finally {
        // Ensure browser is closed
        await synthetics.close();
    }
};

なお、アプリケーションのテストには Playwright の codegen コマンドを利用することでブラウザ操作を記録し、簡単にコードを生成できます。

npx playwright codegen https://example.com

5.依存関係ファイル配置

パッケージの依存関係をファイルに記述し、配置します。

package.json
{
  "type": "module",
  "main": "index.mjs",
  "dependencies": {
    "@aws-sdk/client-secrets-manager": "^3.734.0",
    "totp-generator": "^0.0.14"
  }
}

6.ランタイム動作設定ファイル配置

Synthetics Playwright ランタイム動作設定をファイルに記述し、配置します。

synthetics.json
{
    "step": {
        "screenshotOnStepStart": true,
        "screenshotOnStepSuccess": true,
        "screenshotOnStepFailure": true,
        "stepSuccessMetric": true,
        "stepDurationMetric": true,
        "continueOnStepFailure": true
    },
    "canaryMetrics": {
        "failedCanaryMetric": true,
        "aggregatedFailedCanaryMetric": true
    },
    "report": {
        "includeRequestHeaders": false,
        "includeResponseHeaders": false,
        "includeUrlPassword": false,
        "includeRequestBody": false,
        "includeResponseBody": false,
        "restrictedHeaders": [],
        "restrictedUrlParameters": []
    },
    "logging": {
        "logRequest": false,
        "logResponse": false,
        "logResponseBody": false,
        "logRequestBody": false,
        "logRequestHeaders": false,
        "logResponseHeaders": false
    }
}

7.Canary スクリプトのパッケージ化

次のようなディレクトリ構造になっていることを確認します。

synthetics_function
├── index.mjs
├── node_modules
├── package-lock.json
├── package.json
└── synthetics.json

パッケージ化を行います。

zip -r my_deployment_package.zip .

8.Canary スクリプトのアップロード

パッケージファイルはCanary スクリプトのインポートに必要となるため、任意の S3 バケットにアップロードを行います。
cm-shiina-windows-perfomance-S3-バケット-S3-ap-northeast-1-01-24-2025_12_36_PM

9.Canary の作成

CloudWatch サービスにアクセスし、ナビゲーションメニューから 「Synthetics Canaries」 を選択します。
「Canary を作成」を選択します。
「S3 からインポート」を選択します。
以下のように以下の値を入力し、Canary の作成を行います。

  • 名前:任意
  • ランタイムバージョン:syn-nodejs-playwright-1.0
  • S3 の場所:パッケージファイルをアップロードした S3 バケット
  • Lambda ハンドラー:index.handler
  • IAM ロール:SyntheticsRole
    canary作成1

canary作成2

canary作成3

テスト結果の確認

作成した Canary の名前を一覧から選択します。
可用性タブではステップのステータスが確認できます。
実行されたステップのステータスが「成功」となっていることを確認します。
canarystatus

スクリーンタブではスクリーンショットを確認します。
ユーザー名、パスワード、ワンタイムパスワードトークンが自動で入力されていることが確認できます。
canary1

canary2

canary3

canary4

canary5

canary6

まとめ

2要素認証(2FA/MFA)が必要な Web アプリケーションのテストも、CloudWatch Synthetics を使用することができます。
Secrets Manager を利用して安全に認証情報を管理することで、セキュリティを維持しながら効率的なテスト自動化が可能となります。

本記事が誰かのお役に立てれば幸いです。

参考

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/Synthetics_WritingCanary_Nodejs_Playwright.html
https://ui.docs.amplify.aws/react/connected-components/authenticator
https://github.com/bellstrand/totp-generator
https://playwright.dev/docs/codegen-intro#running-codegen

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.