密碼金鑰註冊

本指南將詳細說明如何讓使用者為帳戶註冊密碼金鑰,提供高度安全、可防範網路釣魚的無密碼登入替代方案。

資料庫需求

如要支援密碼金鑰註冊,資料庫憑證資料表必須儲存下列欄位:

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 要求旗標,即可透過單一端點,有條件地處理這兩種流程。
// 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. 驗證使用者是否在場:
    • 確認剖析後的驗證器資料中傳回的使用者存在 (UP) 旗標為 true,以確認建立時有使用者在場。
  3. 放寬「preferred」的驗證條件:
    • 如果建立選項指定 userVerification: "preferred",則伺服器端驗證呼叫必須設定 requireUserVerification: false。否則,在未經驗證使用者身分的情況下註冊的驗證器 (例如停用螢幕鎖定),會觸發假伺服器驗證失敗。

用戶端邏輯

  1. 在網頁載入時限制 UI:
    • 載入網頁時,如果 conditionalGet 或 passkeyPlatformAuthenticator 無法使用,請呼叫 PublicKeyCredential.getClientCapabilities() 並停用「建立密碼金鑰」按鈕。
  2. 叫用建立和序列化:使用 PublicKeyCredential.parseCreationOptionsFromJSON() 解碼伺服器選項,並將產生的設定傳遞至 navigator.credentials.create()。
    • 擷取驗證端點前,請呼叫 credential.toJSON(),將 AuthenticatorAttestationResponse 編碼為有效的 JSON 可序列化物件。
  3. 處理 WebAuthn 例外狀況:
    • InvalidStateError:已有相符的密碼金鑰 (依 excludeCredentials 比對)。
    • NotAllowedError:使用者取消驗證密碼金鑰對話方塊,或對話方塊逾時。
    • AbortError:作業已取消。
    • SecurityError:安全來源 (HTTPS) 或 RP ID 不符錯誤 (設定問題)。
  4. Try/Catch Segregation for Signal API:
    • 將伺服器驗證 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';