パスキー対応のログイン画面を自動テストしたいのですが、実際の指紋認証やセキュリティキーをCIに挿すわけにはいきません。そこで登場するのが、Playwright 1.61で追加された仮想WebAuthn認証器です。
今回はbrowserContext.credentialsを使って、パスキーの登録とログインをテストしてみます。ハードウェア無しで、WebAuthnの儀式とサーバー側の検証まで確認できます。
パスキーの何をテストするのか
パスキーは、パスワードの文字列を入力する代わりに公開鍵暗号を使ってログインします。ブラウザはnavigator.credentials.create()で登録し、navigator.credentials.get()で認証します。
ここでテストしたいのは、ボタンを押したら画面が変わることだけではありません。サーバーがchallenge、RP ID、署名、ユーザー情報を正しく検証できることです。実際の生体認証UIやOSの画面そのものは別の確認になります。
仮想認証器をインストールする
では、まずBrowserContextに仮想認証器をインストールしましょう。
import { test, expect } from '@playwright/test';
test('register and sign in with a passkey', async ({ browser }) => {
const context = await browser.newContext();
await context.credentials.install();
const page = await context.newPage();
await page.goto('https://example.com/register');
await page.getByRole('button', { name: 'Create a passkey' }).click();
await expect(page.getByText('Passkey registered')).toBeVisible();
await context.close();
});
install()は、現在のページとこれから作るページのnavigator.credentials.create()とget()を仮想認証器へ接続します。これを呼ぶ前にページがWebAuthnへ触ると、実機の挙動になってしまうので順番に注意してください。
登録済みパスキーを次のテストで使う
一度登録した認証情報はcredentials.get()で取り出せます。テスト間で再利用するなら、認証情報をファイルへ保存して次のBrowserContextでseedします。
const [credential] = await context.credentials.get({ rpId: 'example.com' });
await fs.promises.writeFile(
'playwright/.auth/passkey.json',
JSON.stringify(credential),
'utf8',
);
const saved = JSON.parse(
await fs.promises.readFile('playwright/.auth/passkey.json', 'utf8'),
);
const loginContext = await browser.newContext();
await loginContext.credentials.create(saved.rpId, saved);
await loginContext.credentials.install();
const loginPage = await loginContext.newPage();
await loginPage.goto('https://example.com/login');
await loginPage.getByRole('button', { name: 'Use a passkey' }).click();
保存される認証情報には秘密鍵も含まれます。リポジトリへコミットするファイルではありません。CIではテスト専用のユーザーと、CIの秘密ストレージを使ってください。
失敗するログインも確認する
成功するケースだけでは不十分です。サーバーがchallengeを使い回していないか、別のRP IDを受け入れていないか、ユーザーの紐付けを誤っていないかを確認します。
仮想認証器を使えば、認証器を持っていない場合の画面、登録済みユーザーと未登録ユーザーの分岐、署名検証に失敗したときのエラー表示を、同じテストスイートで整理できます。実際のOS生体認証を完全に再現するものではないため、対応範囲はテスト設計に明記しておくのが安全です。
storageStateとの組み合わせ
Playwrightのstorage stateは、CookieやlocalStorageだけでなく、オプションを付ければ仮想WebAuthn認証器も保存できます。
await context.storageState({
path: 'playwright/.auth/passkey-state.json',
credentials: true,
});
const restored = await browser.newContext({
storageState: 'playwright/.auth/passkey-state.json',
});
認証情報を含むstateを復元すると、仮想認証器も自動でインストールされます。そのContextでは実際の認証器が使えなくなる点に注意してください。
まとめ
パスキーのE2Eテストは、もう「実機がないから無理」ではありません。credentials.create()で認証情報を用意し、credentials.install()でページへ接続すれば、登録とログインをCIで再現できます。
ただし、生体認証の見た目やOSのプラットフォームUIまで検証できるわけではありません。仮想認証器でWebAuthnの配線とサーバー検証を確認し、実機確認が必要な部分は別のテストとして切り分けましょう。これで大丈夫です。

コメント