パスキーの登録

このガイドでは、ユーザーがアカウントにパスキーを登録できるようにする方法について詳しく説明します。これにより、フィッシングに強い、安全性の高いパスワードレスのログイン方法を提供できます。

データベース要件

パスキーの登録をサポートするには、データベースの認証情報テーブルに次のフィールドを保存する必要があります。

export interface StoredPasskeyCredential {
  id: string; // Base64URL-encoded credential ID (unique lookup key)
  passkeyUserId: string; // Associated application user ID
  credentialPublicKey: string; // Base64URL-encoded public key used to verify assertion signatures
  credentialType: "public-key";
  credentialDeviceType: "singleDevice" | "multiDevice"; // Helps distinguish device-bound vs cloud-synced passkeys
  credentialBackedUp: boolean; // Boolean backup state reported by the authenticator
  aaguid: string; // Authenticator Attestation GUID
  providerIcon?: string; // Provider icon derived from the AAGUID registry (dark or light theme URLs)
  name: string; // Provider name derived from AAGUID registry
  transports: string[]; // Array of transport names (e.g. 'internal', 'hybrid') necessary for exclusion options
  lastUsedAt?: number; // Optional epoch timestamp of last sign-in
  registeredAt: number; // Registration epoch timestamp
  counter: number; // Authenticator sign-in signature counter used to prevent replay attacks
}

サーバー側

オプションの生成

WebAuthn 作成パラメータを生成するエンドポイントを作成します。暗号化を手動で実装するのではなく、カテゴリ標準ごとに検証済みのライブラリを使用します。

  1. 事前定義された RP ID を使用する: 事前定義された適切な RP ID を定数文字列として使用します。
  2. 安全なチャレンジを作成する: サーバー上で、エントロピーが高く、暗号的に安全なランダム バッファを生成し、ユーザーのセッションに安全に保存して、オプション配信用に Base64URL としてエンコードします。
  3. パスキーの重複を回避する: ユーザーの既存の事前登録済み認証情報 ID を excludeCredentials オプション配列にマッピングします。これにより、認証システムが同じパスキー プロバイダ アカウントに重複する認証情報を登録することを防ぎます。
  4. 検出可能な認証情報を適用する: authenticatorSelection オプションで requireResidentKey: true と residentKey: "required" を設定して、検出可能な認証情報をリクエストします。これは、検出可能なログインに必要です。
  5. ユーザー確認を構成する: userVerification: "preferred" または userVerification: "required" を指定します。多くのコンプライアンス ユースケース(金融、医療など)では、作成時にユーザー確認を適用するために 'required' が必要です。
  6. アタッチメントのスコープを決定する:
    • プロモーション フロー: 標準のパスワード ログインまたは登録後のプロモーションの直後にパスキーの作成を提案する場合は、authenticatorAttachment: "platform" を設定してプラットフォーム認証システムを適用し、外部セキュリティ キーのプロンプトをバイパスします。
    • 管理フロー: プラットフォーム認証システムに加えて外部セキュリティ キーがサポートされている専用の設定パネルまたはセキュリティ パネルから呼び出される場合は、authenticatorAttachment プロパティを完全に省略します。
    • ヒント: promotion: boolean リクエスト フラグを受け入れて、1 つのエンドポイントで両方のフローを条件付きで処理します。
// Options generation example
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // Cryptographically random challenge
  rp: { id: "example.com", name: "Secure Application" },
  user: {
    id: userBase64UrlId, // Unique base64url string identifying the account
    name: "user@example.com",
    displayName: "Jane Doe",
  },
  pubKeyCredParams: [
    {
      type: "public-key",
      alg: -7,
    },
    {
      type: "public-key",
      alg: -257,
    },
  ],
  excludeCredentials: userExistingCredentials.map((cred) => ({
    type: "public-key",
    id: cred.id,
    transports: cred.transports,
  })),
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "preferred",
    ...(isPromotionFlow && { authenticatorAttachment: "platform" }),
  },
};

確認

  1. チャレンジの検証: 予想されるセッション バインド チャレンジに対してチャレンジを安全に検証します。
  2. ユーザーの存在を確認する:
    • 解析された認証器データで返される User Present(UP)フラグが true であることを確認し、作成時にユーザーが物理的に存在していることを確認します。
  3. 「preferred」の検証を緩和する:
    • 作成オプションで userVerification: "preferred" が指定されている場合、サーバーサイド認証呼び出しは requireUserVerification: false で構成する必要があります。そうしないと、ユーザー認証なしで登録された認証システム(画面ロックが無効になっているなど)で、サーバー認証エラーが誤ってトリガーされます。

クライアントサイド ロジック

  1. ページ読み込み時に UI を制限する:
    • ページの読み込み時に PublicKeyCredential.getClientCapabilities() を呼び出し、conditionalGet または passkeyPlatformAuthenticator が使用できない場合は [パスキーを作成] ボタンを無効にします。
  2. 作成とシリアル化を呼び出す: PublicKeyCredential.parseCreationOptionsFromJSON() を使用してサーバー オプションをデコードし、結果の構成を navigator.credentials.create() に渡します。
    • credential.toJSON() を呼び出して、検証エンドポイントを取得する前に AuthenticatorAttestationResponse を有効な JSON シリアル化可能なオブジェクトにエンコードします。
  3. WebAuthn の例外を処理する:
    • InvalidStateError: 一致するパスキーがすでに存在します(excludeCredentials で一致)。
    • NotAllowedError: ユーザーが認証パスキー ダイアログをキャンセルしたか、タイムアウトしました。
    • AbortError: オペレーションが中断されました。
    • SecurityError: セキュアなオリジン(HTTPS)または RP ID の不一致エラー(構成の問題)。
  4. Signal API の Try / Catch 分離:
    • サーバー検証の fetch() 呼び出しを try / catch ブロックでラップします。サーバー検証のフェッチが失敗した場合(ステータスが response.ok === false の場合、またはネットワークがスローされた場合)に signalUnknownCredential() を呼び出します。
// optionsFetch and registerVerifyFetch are app-defined HTTP methods
import { optionsFetch, registerVerifyFetch } from "./api.js";

async function registerPasskey(isPromotion = false) {
  // Verify passkey capability and conditional UI are available
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (
    !capabilities.passkeyPlatformAuthenticator ||
    !capabilities.conditionalGet
  ) {
    // Hide "Create passkey" buttons and fall back to password flows instead
    showStandardPasswordFallbackUI();
    return;
  }

  const creationOptionsJSON = await optionsFetch({ promotion: isPromotion });
  const publicKey =
    PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJSON);

  let credential;
  try {
    // passkey prompt execution
    credential = await navigator.credentials.create({ publicKey });
  } catch (err) {
    if (err.name === "InvalidStateError") {
      console.log("A passkey already exists for this account.");
      alert("A passkey already exists for this account.");
    } else if (err.name === "SecurityError") {
      console.error("Configuration RP ID or Secure Context error.");
      alert("Configuration RP ID or Secure Context error.");
    } else if (err.name === "NotAllowedError") {
      console.log("User cancelled the passkey dialog.");
    } else if (err.name === "AbortError") {
      console.log("The creation operation has been aborted.");
    }
    return; // Safe API exit, do not signal unknown for standard WebAuthn cancels
  }

  // Server Verification phase (Segregated Try/Catch)
  let encodedResponse = credential.toJSON();
  try {
    const response = await registerVerifyFetch(encodedResponse);
    if (!response.ok) {
      // Server verification failed to verify/authenticate the credential (orphaned)
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encodedResponse.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Server verification network failure:", serverErr);
    await publickeycredential.signalunknowncredential({
      rpId, // RP ID must match the one defined on the server
      credentialid: encodedresponse.id, // base64url-encoded credential id
    });
  }
}

代替戦略

Signal API の同期フォールバック

WebAuthn Signal API(webauthn-signals)は、パスワード マネージャーとサーバーの認証情報の状態を同期するために使用されるプログレッシブ最適化です。

  • フォールバック エクスペリエンス: PublicKeyCredential.signalUnknownCredential がブラウザでサポートされていない場合、呼び出しは機能検出ゲート(if (PublicKeyCredential.signalUnknownCredential))を介して安全にバイパスされなければならず、サーバーサイド認証はマネージャーの更新をトリガーせずに失敗をログに記録するだけです。

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

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

import 'webauthn-polyfills';