本指南將詳細說明如何使用可探索憑證,實作回訪者驗證,包括透過明確的按鈕觸發條件,以及流暢的瀏覽器自動填入建議 (條件式 UI)。
伺服器端
生成選項
建立端點,使用符合標準的審查程式庫產生 WebAuthn 要求參數。
- 使用預先定義的 RP ID:使用預先定義的適當 RP ID 做為常數字串。
- 產生驗證問題:產生高熵、加密安全的隨機緩衝區,安全地儲存在使用者工作階段中,並編碼為 Base64URL。
- 可供偵測憑證對應:為
allowCredentials指定空白陣列[]。這會要求可供偵測的憑證,也就是說,使用者不必先輸入使用者名稱,密碼金鑰提供者會顯示可用的帳戶。 - 使用者驗證層級:設為
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";
驗證端點
安全驗證用戶端傳回的斷言,以驗證使用者:
- 驗證工作階段驗證問題:強制用戶端回應與工作階段中儲存的預期驗證問題嚴格相符。
- 強制執行 UV 偏好設定:
- 如果工作階段的
expectedUserVerification要求"preferred",請將requireUserVerification: false傳遞至伺服器端驗證程式庫,允許不含 UV 的驗證器 (例如停用驗證器螢幕鎖定)。如果要求"required",請嚴格強制執行生物辨識/PIN 碼輸入。
- 如果工作階段的
- 清除伺服器錯誤 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) 呼叫。
條件式調解流程 (表單自動填入)
在載入網頁時啟用表單自動填入建議,當使用者選取登入欄位時,系統會自動提供密碼金鑰驗證:
- 功能偵測:在載入網頁時呼叫
PublicKeyCredential.getClientCapabilities(),如果conditionalGet無法使用,則略過使用密碼金鑰登入。 - 解碼選項:使用
PublicKeyCredential.parseRequestOptionsFromJSON()解碼擷取的憑證 JSON 物件。 - 叫用條件式 GET:使用
mediation: "conditional"呼叫navigator.credentials.get(),並傳遞AbortController信號。這項功能會自動註冊自動填入功能,不會顯示密碼金鑰對話方塊。 - Try/Catch 例外狀況隔離:在 try/catch 區塊中包裝
navigator.credentials.get呼叫:NotAllowedError:使用者取消密碼金鑰登入提示,或提示逾時。AbortError:驗證要求已透過程式取消。
- Call Signal API:在 try/catch 區塊中包裝伺服器驗證
fetch()呼叫:- 顯示錯誤訊息,讓使用者瞭解問題所在。
- 只有在伺服器明確回應 HTTP 狀態
404(找不到憑證) 且使用者未經驗證時,才呼叫signalUnknownCredential()。 - 傳遞至
signalUnknownCredential()的credentialId參數必須嚴格採用 Base64URL 編碼的憑證 ID 字串 (例如encoded.id),而非原始 ArrayBuffer 物件credential.rawId。
- 編碼回應:使用
.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';