密碼金鑰驗證

本指南將詳細說明如何使用可探索憑證,實作回訪者驗證,包括透過明確的按鈕觸發條件,以及流暢的瀏覽器自動填入建議 (條件式 UI)。

伺服器端

生成選項

建立端點,使用符合標準的審查程式庫產生 WebAuthn 要求參數。

  1. 使用預先定義的 RP ID:使用預先定義的適當 RP ID 做為常數字串。
  2. 產生驗證問題:產生高熵、加密安全的隨機緩衝區,安全地儲存在使用者工作階段中,並編碼為 Base64URL。
  3. 可供偵測憑證對應:為 allowCredentials 指定空白陣列 []。這會要求可供偵測的憑證,也就是說,使用者不必先輸入使用者名稱,密碼金鑰提供者會顯示可用的帳戶。
  4. 使用者驗證層級:設為 userVerification: "preferred" (如果公司法規遵循政策明確要求,則設為 "required")。
    • 要求的 userVerification 限制層級必須保留在選項端點的伺服器工作階段記錄中,而不是透過查詢字串從用戶端傳回。這樣一來,驗證端點就能安全地強制執行嚴格的比對限制,不必擔心用戶端遭到操控。
// Options generation example (discoverable flow)
const options = {
  challenge: serverGeneratedBase64UrlChallenge, // High-entropy random challenge stored in session
  rpId: "example.com",
  allowCredentials: [], // Request discoverable passkeys
  userVerification: "preferred",
};

// Persist expected UV level to user session
req.session.expectedUserVerification = "preferred";

驗證端點

安全驗證用戶端傳回的斷言,以驗證使用者:

  1. 驗證工作階段驗證問題:強制用戶端回應與工作階段中儲存的預期驗證問題嚴格相符。
  2. 強制執行 UV 偏好設定:
    • 如果工作階段的 expectedUserVerification 要求 "preferred",請將 requireUserVerification: false 傳遞至伺服器端驗證程式庫,允許不含 UV 的驗證器 (例如停用驗證器螢幕鎖定)。如果要求 "required",請嚴格強制執行生物辨識/PIN 碼輸入。
  3. 清除伺服器錯誤 404:如果在資料庫中找不到用戶端傳回的憑證 ID,請傳回明確的 HTTP 404 錯誤,讓用戶端觸發 Signal API。

用戶端邏輯

HTML 表單註解

為使用者名稱和密碼輸入欄位加上註解,以原生方式運用條件式 UI。自動完成權杖會結合 webauthn 規格參數,而自動聚焦功能會在輸入欄位成為焦點時,立即觸發瀏覽器自動填入彈出式視窗。

<!-- Autocomplete tokens must contain webauthn space-separated -->
<form id="signin-form">
  <input
    type="text"
    name="username"
    autocomplete="username webauthn"
    autofocus
    data-testid="username-field"
  />
  <input type="password" name="password" autocomplete="current-password" />
  <button type="submit">Sign in</button>
</form>

明確按鈕流程

當使用者點選「使用密碼金鑰登入」按鈕時,觸發密碼金鑰驗證。叫用密碼金鑰提示前,請先取消任何進行中的表單自動填入 (條件式 Get) 呼叫。

條件式調解流程 (表單自動填入)

在載入網頁時啟用表單自動填入建議,當使用者選取登入欄位時,系統會自動提供密碼金鑰驗證:

  1. 功能偵測:在載入網頁時呼叫 PublicKeyCredential.getClientCapabilities(),如果 conditionalGet 無法使用,則略過使用密碼金鑰登入。
  2. 解碼選項:使用 PublicKeyCredential.parseRequestOptionsFromJSON() 解碼擷取的憑證 JSON 物件。
  3. 叫用條件式 GET:使用 mediation: "conditional" 呼叫 navigator.credentials.get(),並傳遞 AbortController 信號。這項功能會自動註冊自動填入功能,不會顯示密碼金鑰對話方塊。
  4. Try/Catch 例外狀況隔離:在 try/catch 區塊中包裝 navigator.credentials.get 呼叫:
    • NotAllowedError:使用者取消密碼金鑰登入提示,或提示逾時。
    • AbortError:驗證要求已透過程式取消。
  5. Call Signal API:在 try/catch 區塊中包裝伺服器驗證 fetch() 呼叫:
    • 顯示錯誤訊息,讓使用者瞭解問題所在。
    • 只有在伺服器明確回應 HTTP 狀態 404 (找不到憑證) 且使用者未經驗證時,才呼叫 signalUnknownCredential()。
    • 傳遞至 signalUnknownCredential() 的 credentialId 參數必須嚴格採用 Base64URL 編碼的憑證 ID 字串 (例如 encoded.id),而非原始 ArrayBuffer 物件 credential.rawId。
  6. 編碼回應:使用 .toJSON() 編碼憑證 AuthenticatorAssertionResponse,再傳送至伺服器進行驗證。
// optionsFetch and loginVerifyFetch are app-defined HTTP methods
import { optionsFetch, loginVerifyFetch } from "./api.js";

let autofillAbortController = new AbortController();

async function initializeConditionalAutofill() {
  // Feature detect Conditional Get autofill support
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true) {
    const loginOptionsJSON = await optionsFetch();
    const publicKey =
      PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

    try {
      // Initiate Conditional UI form autofill suggestions
      const credential = await navigator.credentials.get({
        publicKey,
        signal: autofillAbortController.signal,
        mediation: "conditional",
      });

      // Segregated verification fetch
      const encoded = credential.toJSON();
      const response = await loginVerifyFetch(encoded);
      if (!response.ok && response.status === 404) {
        // Note: this code path runs pre-authentication, satisfying the unauth precondition
        if (PublicKeyCredential.signalUnknownCredential) {
          await PublicKeyCredential.signalUnknownCredential({
            rpId, // RP ID must match the one defined on the server
            credentialId: encoded.id,
          });
        }
      }
    } catch (err) {
      // Silently swallow expected client WebAuthn exceptions
      if (["NotAllowedError", "AbortError"].includes(err.name)) {
        return;
      }
      console.error("Unexpected conditional get error:", err);
    }
  }
}

async function triggerButtonAuthentication() {
  // Abort any pending Conditional Get call to prevent passkey prompt collisions
  autofillAbortController.abort();
  autofillAbortController = new AbortController(); // Reset controller for next triggers

  const loginOptionsJSON = await optionsFetch();
  const publicKey =
    PublicKeyCredential.parseRequestOptionsFromJSON(loginOptionsJSON);

  let credential;
  try {
    // Passkey explicit prompt trigger
    credential = await navigator.credentials.get({
      publicKey,
      signal: autofillAbortController.signal,
    });
  } catch (err) {
    if (err.name === "NotAllowedError") {
      console.log("User cancelled passkey login.");
    } else if (err.name === "AbortError") {
      console.log("The authentication operation was aborted.");
    }
    // Re-arm Conditional autofill Suggestions after cancelled explicit button prompts
    initializeConditionalAutofill();
    return; // Safe exit
  }

  // Segregated verification try/catch (HTTP 404 trigger)
  const encoded = credential.toJSON();
  try {
    const response = await loginVerifyFetch(encoded);
    if (!response.ok && response.status === 404) {
      // Note: this code path runs pre-authentication, satisfying the unauth precondition
      await PublicKeyCredential.signalUnknownCredential({
        rpId, // RP ID must match the one defined on the server
        credentialId: encoded.id, // Base64URL-encoded credential ID
      });
    }
  } catch (serverErr) {
    console.error("Verification request error:", serverErr);
  }
}

// Trigger Conditional Get on load
window.addEventListener("DOMContentLoaded", initializeConditionalAutofill);

備用策略

密碼金鑰功能偵測備用方案

請務必安裝「webauthn-polyfills」,並將其匯入結構定義。只要支援 PublicKeyCredential,就支援 PublicKeyCredential.getClientCapabilities。

import 'webauthn-polyfills';

Signal API 同步處理備用方案

WebAuthn Signal API (webauthn-signals) 是一種漸進式最佳化功能,可讓密碼管理工具與伺服器憑證狀態保持同步。

  • 備用體驗:透過 if (PublicKeyCredential.signalUnknownCredential) 限制。如果瀏覽器不支援,系統會略過背景驗證同步作業,不會擲回瀏覽器例外狀況。

簡易 JSON 序列化備用方案

請務必安裝「webauthn-polyfills」,並將其匯入結構定義。請注意,只要支援 PublicKeyCredential,系統也會支援 PublicKeyCredential.parseRequestOptionsFromJSON 和 PublicKeyCredential.prototype.toJSON。

import 'webauthn-polyfills';