本指南將詳細說明如何讓使用者為帳戶註冊密碼金鑰,提供高度安全、可防範網路釣魚的無密碼登入替代方案。
資料庫需求
如要支援密碼金鑰註冊,資料庫憑證資料表必須儲存下列欄位:
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 建立參數的端點。請依據類別標準,使用經過審查的程式庫,而非手動編寫密碼編譯。
- 使用預先定義的 RP ID:使用預先定義的適當 RP ID 做為常數字串。
- 建立安全的驗證碼:在伺服器上產生高熵、加密安全的隨機緩衝區,安全地儲存在使用者的工作階段中,並編碼為 Base64URL,以便傳送選項。
- 避免重複的密碼金鑰:將使用者現有的預先註冊憑證 ID 對應至
excludeCredentials選項陣列。這可防止驗證器在同一個密碼金鑰提供者帳戶上註冊重複的憑證。 - 強制執行可探索憑證:在
authenticatorSelection選項中設定requireResidentKey: true和residentKey: "required",要求可探索憑證,這是可探索登入的必要條件。 - 設定使用者驗證:指定
userVerification: "preferred"或userVerification: "required"。許多法規遵循用途 (例如金融、醫療照護) 都需要'required'在建立時強制執行使用者驗證。 - 決定連結範圍:
- 宣傳流程:在標準密碼登入或註冊後宣傳活動後,建議建立密碼金鑰時,請設定
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" }),
},
};
驗證
- 驗證驗證問題:安全地驗證驗證問題,確保與預期的工作階段繫結驗證問題相符。
- 驗證使用者是否在場:
- 確認剖析後的驗證器資料中傳回的使用者存在 (UP) 旗標為
true,以確認建立時有使用者在場。
- 確認剖析後的驗證器資料中傳回的使用者存在 (UP) 旗標為
- 放寬「preferred」的驗證條件:
- 如果建立選項指定
userVerification: "preferred",則伺服器端驗證呼叫必須設定requireUserVerification: false。否則,在未經驗證使用者身分的情況下註冊的驗證器 (例如停用螢幕鎖定),會觸發假伺服器驗證失敗。
- 如果建立選項指定
用戶端邏輯
- 在網頁載入時限制 UI:
- 載入網頁時,如果
conditionalGet或passkeyPlatformAuthenticator無法使用,請呼叫PublicKeyCredential.getClientCapabilities()並停用「建立密碼金鑰」按鈕。
- 載入網頁時,如果
- 叫用建立和序列化:使用
PublicKeyCredential.parseCreationOptionsFromJSON()解碼伺服器選項,並將產生的設定傳遞至navigator.credentials.create()。- 擷取驗證端點前,請呼叫
credential.toJSON(),將AuthenticatorAttestationResponse編碼為有效的 JSON 可序列化物件。
- 擷取驗證端點前,請呼叫
- 處理 WebAuthn 例外狀況:
InvalidStateError:已有相符的密碼金鑰 (依excludeCredentials比對)。NotAllowedError:使用者取消驗證密碼金鑰對話方塊,或對話方塊逾時。AbortError:作業已取消。SecurityError:安全來源 (HTTPS) 或 RP ID 不符錯誤 (設定問題)。
- 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';