CloudWatch Synthetics で 2要素認証(2FA/MFA)が必要な Web アプリのブラウザテストを自動化してみた
こんにちは。テクニカルサポートチームのShiinaです。
はじめに
2要素認証(2FA/MFA)が設定されたアプリケーションのテスト自動化は、ワンタイムパスワードトークンの手動入力が必要なため困難です。
今回、AWS Amplify UI Authenticator を利用した Web アプリケーションに対し、CloudWatch Synthetics を用いて2要素認証を含むテストを自動化してみました。
本記事では、Secrets Manager を利用した安全な認証情報の管理と、Playwright を活用した認証プロセス操作を自動化する方法をご紹介します。
前提
- Canary ランタイムには Playwright を使用します。
 - Amplify UI Authenticator コンポーネントを使用したアプリケーションの認証プロセスを自動化する方法を取り扱います。

 
使用するライブラリ
- 
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>"
}
- シークレットの名前
任意の名前を入力します。 - ローテーションを設定
オプションの設定不要です。

 

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 はご自身のものに修正してください。

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 ステップはアプリケーションに沿ったもので実装ください。
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.依存関係ファイル配置
パッケージの依存関係をファイルに記述し、配置します。
{
  "type": "module",
  "main": "index.mjs",
  "dependencies": {
    "@aws-sdk/client-secrets-manager": "^3.734.0",
    "totp-generator": "^0.0.14"
  }
}
6.ランタイム動作設定ファイル配置
Synthetics Playwright ランタイム動作設定をファイルに記述し、配置します。
{
    "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 バケットにアップロードを行います。

9.Canary の作成
CloudWatch サービスにアクセスし、ナビゲーションメニューから 「Synthetics Canaries」 を選択します。
「Canary を作成」を選択します。
「S3 からインポート」を選択します。
以下のように以下の値を入力し、Canary の作成を行います。
- 名前:任意
 - ランタイムバージョン:syn-nodejs-playwright-1.0
 - S3 の場所:パッケージファイルをアップロードした S3 バケット
 - Lambda ハンドラー:index.handler
 - IAM ロール:SyntheticsRole

 


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

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






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






