パスキーの再認証

ウェブ機能: ウェブ認証

この差分に焦点を当てたガイドでは、ログインしたユーザーがアカウントの機密性の高い変更(パスワードの更新、金融取引など)を行う前に、ステップアップ認証または再検証を実装する方法について詳しく説明します。

デルタフロー アーキテクチャ

通常の認証とは異なり、パスキーの再認証では、パスキー ダイアログのプロンプトが、ログインしているユーザーの事前登録済みの認証情報に厳密に制限されます。これにより、アクティブなセッション中にアカウントの混在やパスキーのなりすましを防ぐことができます。

サーバー側

オプション生成デルタ

アクティブな既知のユーザー専用の許可された認証情報パラメータを入力するエンドポイントを作成します。

認証情報を制限する: ログインしたユーザーの登録済み認証情報 ID をすべてマッピングする特定の PublicKeyCredentialDescriptor レコードを allowCredentials オプション配列に入力します。このフィールドを空のままにするか省略すると、検出可能な認証情報に回帰し、セッションの安全性が損なわれます。

// Node.js step-up options generation example
router.post("/api/reauth/options", enforceActiveSession, async (req, res) => {
  const userPasskeys = await db.findCredentialsByUserId(req.user.id);

  const options = {
    challenge: serverGeneratedBase64UrlChallenge, // Random challenge stored in user session
    rpId: "example.com",
    // Enforce allowance strictly limited to the user's credentials list
    allowCredentials: userPasskeys.map((cred) => ({
      type: "public-key",
      id: cred.id,
      transports: cred.transports, // Speeds up resolution by indicating platform transports
    })),
  };
  return res.json(options);
});

検証エンドポイントの差分

クライアントから返されたアサーションを確認します。

アカウントの所有権を確認する: 検証エンドポイントは、クライアントから返された認証済みの認証情報 ID が、関連付けられたユーザー ID がアクティブなログイン ユーザー(storedCredential.passkeyUserId === req.user.id)と厳密に一致する保存済みの認証情報レコードに解決されることを明示的に確認する必要があります。別のユーザーの有効なパスキーが返された場合、認証は直ちに拒否される必要があります。

クライアントサイド フローの差分

アプリケーションは、トランザクション UI に応じて 2 つの再認証インターフェースから選択します。

A. ボタンフロー(入力フィールドなし)

ユーザーが [本人確認] ボタンまたは [取引を続行] ボタンを押したときに再認証をトリガーします。

<button id="reauth-btn" data-testid="reauth-button">Confirm Transaction</button>
let reauthAbortController = new AbortController();

async function triggerButtonReauth() {
  // Abort any background suggestion flows to avoid passkey prompt collisions
  reauthAbortController.abort();
  reauthAbortController = new AbortController();

  const optionsResponse = await fetch("/api/reauth/options", {
    method: "POST",
  });
  const optionsJSON = await optionsResponse.json();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON);

  try {
    const credential = await navigator.credentials.get({
      publicKey,
      signal: reauthAbortController.signal,
    });

    if (credential) {
      const encodedCredential = credential.toJSON();
      const verifyResponse = await fetch("/api/reauth/verify", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(encodedCredential),
      });

      if (verifyResponse.ok) {
        showTransactionSuccessUI();
      } else if (verifyResponse.status === 404 && PublicKeyCredential.signalUnknownCredential) {
        await PublicKeyCredential.signalUnknownCredential({
          rpId, // RP ID must match the one defined on the server
          credentialId: encodedCredential.id
        });
      }
    }
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled reauthentication.");
    }
  }
}

document
  .getElementById("reauth-btn")
  .addEventListener("click", triggerButtonReauth);

代替戦略

パスキー機能検出のフォールバック

常に「webauthn-polyfills」をインストールし、コンテキストにインポートします。PublicKeyCredential がサポートされている限り、PublicKeyCredential.getClientCapabilities もサポートされます。

import 'webauthn-polyfills';

簡単な JSON シリアル化フォールバック

常に「webauthn-polyfills」をインストールし、コンテキストにインポートします。PublicKeyCredential がサポートされている限り、PublicKeyCredential.parseRequestOptionsFromJSON と PublicKeyCredential.prototype.toJSON もサポートされます。

import 'webauthn-polyfills';