🎭

PlaywrightのE2Eテストでパスキー(WebAuthn)を使った認証・ログインを再現する

2025/01/06に公開

やりたいこと

  • Playwrightのテストでパスキー(WebAuthn)を使った認証・ログインを再現する

アプローチ

PlaywrightのCDPSessionを使用して、ChromeのVirtual Authenticators(仮想認証器)を有効にします。

https://playwright.dev/docs/api/class-cdpsession

https://developer.chrome.com/docs/devtools/webauthn?hl=ja

導入

newCDPSessionでCDPセッションを作成し、addVirtualAuthenticatorでVirtual Authenticatorsを追加します。一度追加してしまえば後は特に操作する必要もなく、自動的にテストでWebAuthnのAuthenticatorを再現してくれます。

ハマった点

  • パスキー認証(Discoverable credentials)を再現したい場合、cdpSessionにaddVirtualAuthenticatorを投げるときhasResidentKeyを指定する必要がある
  • Authenticatorの挙動は自動で再現されるが、やたら時間がかかることがあるので長めに待機時間を入れる必要がある
    • 特にCI環境みたいな貧弱な環境だと顕著かもしれない

*.test.ts

import { test, expect } from "@playwright/test";

test.describe.serial("Authentications & API Calling", () => {
    let savedCookies: Cookie[] = [];

    test("パスキー(WebAuthn)でサインアップ => サインインできる", async ({ browserName, page }, testInfo) => {
        // Virtual Authenticatorに対応してるのは現状Chromeのみ
        test.skip(browserName !== "chromium", "This test runs only in Chromium");
        await page.goto("/");

        // Virtual Authenticatorを追加
        const cdpSession = await page.context().newCDPSession(page);
        await cdpSession.send("WebAuthn.enable");
        await cdpSession.send("WebAuthn.addVirtualAuthenticator", {
            options: {
                protocol: "ctap2",
                ctap2Version: "ctap2_1",
                hasUserVerification: true,
                transport: "internal",
                automaticPresenceSimulation: true,
                isUserVerified: true,
                // パスキー認証(WebAuthn単体での認証、ユーザー名を入れなくてもいい認証)の場合これを有効にする必要があるので注意
                hasResidentKey: true,
            }
        });

        cdpSession.on("WebAuthn.credentialAdded", () => {
            console.log("Credential Added!");
        });

        // サインアップする(この辺は適当に環境に合わせて改変してください)
        await page.click("#sign-up-button");
        await page.fill("#name", "Test User");
        await page.click("#terms");
        const beforeSignUpScreenshot = await page.screenshot();
        await testInfo.attach("Before sign-up", {
            body: beforeSignUpScreenshot,
            contentType: "image/png",
        });

        await page.click("#sign-up-submit-button");
        await page.waitForTimeout(10);
        const afterSignUpScreenshot = await page.screenshot();
        await testInfo.attach("After click sign-up button", {
            body: afterSignUpScreenshot,
            contentType: "image/png",
        });

        // やたら時間がかかることがあるので長めに待機時間を入れておく
        await page.waitForTimeout(8000);
        await page.reload();

        // サインインする
        await page.click("#button-sign-in-with-passkey");
        await page.waitForTimeout(500);
        const afterSignInScreenshot = await page.screenshot();
        await testInfo.attach("After click sign-in button", {
            body: afterSignInScreenshot,
            contentType: "image/png",
        });

        // やたら時間がかかることがあるので長めに待機時間を入れておく
        await page.waitForTimeout(8000);
        const appScreenshot = await page.screenshot();
        await testInfo.attach("After sign-in", {
            body: appScreenshot,
            contentType: "image/png",
        });

        // あとはログインに成功してるかどうか確認するだけ!
        expect(await page.locator("#my-app-sidebar").count()).toBeGreaterThan(0);

        // Cookieを保存
        savedCookies = await page.context().cookies();
    });

    test("認証が必要なテスト", async ({ browserName, page }, testInfo) => {
        // これはここでも必須
        test.skip(browserName !== "chromium", "This test runs only in Chromium");

        await page.context().addInitScript(() => {
            // localStorageを設定
            window.localStorage.setItem("isLoggedIn", "true");
        });

        // サインイン済みの状態を復元
        await page.context().addCookies(savedCookies);

        // 以下略
    });
});

上手く動かない時は

  • 環境によっては登録や認証にやたら時間がかかるので待機時間を増やしてみる
  • Chromeの開発者コンソールのVirtual Authenticatorで動作するかを確認してみる
    • そのときに設定したパラメーターとaddVirtualAuthenticatorに渡したオプションを比較してテスト側のオプションを変えてみる
  • page.on("console", msg => console.log(msg.text()));を追加してクライアント側でエラーが発生してないか調べる
  • 以下のコードでレポートにスクリーンショットを添付できるのでそれで何が起こってるかを調べてみる
    const homeScreenshot = await page.screenshot();
    await testInfo.attach("Top page", {
        body: homeScreenshot,
        contentType: "image/png",
    });
    

Discussion