建立可使用密碼金鑰的登入服務,同時繼續支援現有密碼使用者。
本指南說明如何使用表單自動填入功能,讓使用者除了密碼,也能透過密碼金鑰登入。使用表單自動填入功能可提供一致的登入體驗,簡化從密碼到更安全且容易使用的密碼金鑰驗證方式的轉換過程。
瞭解如何導入 WebAuthn 的條件式 UI,在現有的登入表單中支援密碼金鑰和密碼使用者,盡量減少摩擦。
為什麼要使用表單自動填入功能,透過密碼金鑰登入帳戶?
使用者可以透過指紋、臉部或裝置 PIN 碼登入網站。
如果所有使用者都有密碼金鑰,驗證流程可以簡化為單一登入按鈕。輕觸按鈕後,使用者即可直接透過螢幕鎖定驗證帳戶並登入。
不過,從密碼改用密碼金鑰的過程並不容易。在這段期間,網站必須同時支援密碼和密碼金鑰使用者。要求使用者記住哪些網站使用密碼金鑰,並預先選擇登入方式,會導致不良的使用者體驗。
密碼金鑰也是新技術,清楚說明可能會有難度。使用熟悉的自動填入介面,有助於解決轉換挑戰,並滿足使用者熟悉度的需求。
使用條件式 UI
如要有效支援密碼金鑰和密碼使用者,請在表單的自動填入建議中加入密碼金鑰。這個方法會使用 條件式 UI,這是 WebAuthn 標準的一項功能。
使用者著重在使用者名稱輸入欄位時,系統會顯示自動填入對話方塊,建議使用儲存的密碼金鑰和密碼。使用者可以選取密碼金鑰或密碼,然後繼續登入。如果選擇密碼金鑰,則須使用裝置螢幕鎖定功能。
這樣一來,使用者就能透過現有的登入表單登入網站,並在有密碼金鑰的情況下,享有密碼金鑰帶來的額外安全保障。
密碼金鑰驗證的運作方式
如要使用密碼金鑰進行驗證,請使用 WebAuthn API。
密碼金鑰驗證流程包含四個元件:
- 後端:儲存使用者帳戶詳細資料,包括公開金鑰。
- 前端:與瀏覽器通訊,並從後端擷取必要資料。
- 瀏覽器:執行 JavaScript 並與 WebAuthn API 互動。
- 密碼金鑰供應商:建立及儲存密碼金鑰。這通常是密碼管理工具 (例如 Google 密碼管理工具) 或安全金鑰。
密碼金鑰驗證程序流程如下:
- 使用者造訪登入頁面,前端向後端要求驗證問題。
- 後端會產生並傳回與使用者帳戶相關聯的 WebAuthn 驗證問題。
- 前端會使用驗證問題呼叫
navigator.credentials.get(),透過瀏覽器啟動驗證。 - 瀏覽器會與密碼金鑰供應商互動,提示使用者選取密碼金鑰 (通常是透過聚焦登入欄位觸發的自動填入對話方塊),並使用裝置螢幕鎖定或生物特徵辨識功能驗證身分。
- 成功驗證使用者後,密碼金鑰提供者會簽署驗證問題,瀏覽器則會將產生的公開金鑰憑證 (包括簽章) 傳回給前端。
- 前端會將這項憑證傳送至後端。
- 後端會根據使用者儲存的公開金鑰,驗證憑證的簽章。如果驗證成功,後端會讓使用者登入。
透過表單自動填入功能,使用密碼金鑰驗證
如要使用表單自動填入功能啟動密碼金鑰驗證,請在登入頁面載入時,發出有條件的 WebAuthn get 呼叫。這個對 navigator.credentials.get() 的呼叫包含 mediation: 'conditional' 選項。
對 WebAuthn 的
navigator.credentials.get() API 提出條件式要求時,不會立即顯示使用者介面。而是會處於待處理狀態,直到使用者與使用者名稱欄位的自動填入提示互動為止。如果使用者選取密碼金鑰,瀏覽器會使用憑證解析待處理的 Promise,讓使用者登入,略過傳統表單提交程序。如果使用者選擇密碼,系統不會解析 Promise,且會繼續執行標準密碼登入流程。接著,頁面會負責讓使用者登入。
為表單輸入欄位加上註解
如要啟用密碼金鑰自動填入功能,請將 autocomplete 屬性新增至表單的使用者名稱 input 欄位。以空格分隔 username 和 webauthn 的值。
<input type="text" name="username" autocomplete="username webauthn" autofocus>
在這個欄位中加入 autofocus,系統就會在載入網頁時自動觸發自動填入提示,立即顯示可用的密碼和密碼金鑰。
特徵偵測
在叫用條件式 WebAuthn API 呼叫之前,請檢查下列事項:
- 瀏覽器支援 WebAuthn 和
PublicKeyCredential。
- 瀏覽器支援使用
PublicKeyCredential.getClientCapabilities()偵測功能。
- 瀏覽器支援 WebAuthn 條件式 UI,並使用
conditionalGet。
下列程式碼片段說明如何檢查瀏覽器是否支援這些功能:
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
// Check if conditional mediation is available.
if (capabilities.conditionalGet === true) {
// The browser supports conditional mediation.
}
}
從後端擷取資訊
後端必須提供多種選項給前端,才能發起 navigator.credentials.get() 呼叫。這些選項通常會從伺服器上的端點擷取為 JSON 物件。
選項物件中的重要屬性包括:
challenge:伺服器產生的 ArrayBuffer 格式驗證問題 (通常會以 Base64URL 編碼,用於 JSON 傳輸)。這是防範重送攻擊的必要措施。伺服器必須為每次登入嘗試產生新的驗證問題,並在短時間後或嘗試失敗時失效。allowCredentials: 憑證描述元陣列。傳遞空陣列。這會提示瀏覽器列出指定rpId的所有憑證。userVerification: 指定使用者驗證偏好設定,例如要求裝置螢幕鎖定。建議您使用預設值"preferred"。可能的值包括:"required":驗證器 (例如 PIN 碼或生物特徵辨識) 必須執行使用者驗證。如果無法執行驗證,作業就會失敗。"preferred":驗證器會嘗試驗證使用者,但作業不一定要通過驗證才能成功。"discouraged":驗證器應盡可能避免驗證使用者。
rpId:您的依賴方 ID,通常是網站網域 (例如example.com)。這個值必須與建立密碼金鑰憑證時使用的rp.id完全相符。
伺服器應建構這個選項物件。ArrayBuffer 值 (例如 challenge) 必須經過 Base64URL 編碼,才能透過 JSON 傳輸。在前端,剖析 JSON 後,請使用 PublicKeyCredential.parseRequestOptionsFromJSON() 將物件 (包括解碼 Base64URL 字串) 轉換為 navigator.credentials.get() 預期的格式。
以下程式碼片段說明如何擷取及解碼使用密碼金鑰驗證所需的資訊。
// Fetch an encoded PubicKeyCredentialRequestOptions from the server.
const _options = await fetch('/webauthn/signinRequest');
// Deserialize and decode the PublicKeyCredentialRequestOptions.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseRequestOptionsFromJSON(decoded_options);
...
使用 conditional 旗標呼叫 WebAuthn API,驗證使用者
準備好 publicKeyCredentialRequestOptions 物件 (在下列程式碼範例中稱為 options) 後,請呼叫 navigator.credentials.get(),啟動條件式密碼金鑰驗證。
// To abort a WebAuthn call, instantiate an AbortController.
const abortController = new AbortController();
// Invoke WebAuthn to authenticate with a passkey.
const credential = await navigator.credentials.get({
publicKey: options,
signal: abortController.signal,
// Specify 'conditional' to activate conditional UI
mediation: 'conditional'
});
這項呼叫的重要參數:
publicKey:這必須是您從伺服器擷取並在上一個步驟中處理的publicKeyCredentialRequestOptions物件 (在範例中命名為options)。signal:傳遞AbortController的信號 (例如abortController.signal) 可讓您以程式輔助方式取消get()要求。如果您想叫用其他 WebAuthn 呼叫,這個方法就非常實用。mediation: 'conditional':這是重要旗標,可讓 WebAuthn 呼叫成為條件式呼叫。這會告知瀏覽器等待使用者與自動填入提示互動,而不是立即顯示模式對話方塊。
將傳回的公開金鑰憑證傳送至 RP 伺服器
如果使用者選取密碼金鑰並成功驗證身分 (例如使用裝置螢幕鎖定),navigator.credentials.get()
Promise 就會解析。這會將 PublicKeyCredential 物件傳回前端。
承諾遭拒的原因有很多,您應在程式碼中檢查 Error 物件的 name 屬性,處理這些錯誤:
NotAllowedError:使用者取消作業,或未選取密碼金鑰。AbortError:作業已中止,可能是因為您的程式碼使用AbortController。- 其他例外狀況:發生未預期的錯誤。瀏覽器通常會向使用者顯示錯誤對話方塊。
PublicKeyCredential 物件包含多個屬性。與驗證相關的主要屬性包括:
id: 經驗證密碼金鑰憑證的 Base64url 編碼 ID。rawId:憑證 ID 的 ArrayBuffer 版本。response.clientDataJSON:用戶端資料的 ArrayBuffer。這個欄位包含多項資訊,例如驗證問題和伺服器必須驗證的來源。response.authenticatorData: 驗證器資料的 ArrayBuffer。這個欄位包含 RP ID 等資訊。response.signature: 包含簽章的 ArrayBuffer。這個值是憑證的核心,您的伺服器必須使用憑證的已儲存公開金鑰,驗證這個簽章。response.userHandle:包含在密碼金鑰註冊期間提供的使用者 ID 的 ArrayBuffer。authenticatorAttachment: 指出驗證器是屬於用戶端裝置 (platform) 或外部 (cross-platform)。如果使用者 透過手機登入,可能會出現cross-platform附件。 在這種情況下,建議提示使用者在目前裝置上建立密碼金鑰,方便日後登入。type:這個欄位一律設為"public-key"。
如要將這個 PublicKeyCredential 物件傳送至後端,請先呼叫 .toJSON() 方法。這個方法會建立可序列化為 JSON 的憑證版本,並正確處理 ArrayBuffer 屬性 (例如 rawId、clientDataJSON、authenticatorData、signature 和 userHandle) 的轉換,將其轉換為 Base64URL 編碼字串。接著,請使用 JSON.stringify() 將這個物件轉換為字串,並在要求主體中傳送至伺服器。
...
// Encode and serialize the PublicKeyCredential.
const _result = credential.toJSON();
const result = JSON.stringify(_result);
// Encode and send the credential to the server for verification.
const response = await fetch('/webauthn/signinResponse', {
method: 'post',
credentials: 'same-origin',
body: result
});
驗證簽名
後端伺服器收到公開金鑰憑證後,必須驗證憑證的真實性。學習內容如下:
- 剖析憑證資料。
- 查詢與憑證
id相關聯的已儲存公開金鑰。 - 根據儲存的公開金鑰驗證收到的
signature。 - 驗證其他資料,例如挑戰和來源。
建議使用伺服器端 FIDO/WebAuthn 程式庫,安全地處理這些加密編譯作業。您可以在 awesome-webauthn GitHub 存放區中找到開放原始碼程式庫。
如果簽章和所有其他斷言都有效,伺服器就能讓使用者登入。如需伺服器端驗證的詳細步驟,請參閱「伺服器端密碼金鑰驗證」。
如果後端找不到相符的憑證,請發出信號
如果後端伺服器在登入期間找不到相符的 ID 憑證,可能是因為使用者先前已從伺服器刪除這組密碼金鑰,但未從密碼金鑰提供者刪除。如果密碼金鑰提供者持續建議不再適用於您網站的密碼金鑰,可能會導致使用者體驗不佳。為改善這點,您應向密碼金鑰供應商發出信號,要求移除孤立的密碼金鑰。
您可以使用 PublicKeyCredential.signalUnknownCredential() 方法 (Webauthn Signal API 的一部分),通知密碼金鑰提供者指定的憑證已移除或不存在。如果伺服器指出 (例如使用特定 HTTP 狀態碼 (如 404)) 顯示的憑證 ID 不明,請在用戶端呼叫這個靜態方法。將 RP ID 和不明憑證 ID 提供給這個方法。如果密碼金鑰供應商支援這項信號,應移除密碼金鑰。
// Detect authentication failure due to lack of the credential
if (response.status === 404) {
// Feature detection
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId: "example.com",
credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
});
} else {
// Encourage the user to delete the passkey from the password manager nevertheless.
...
}
}
驗證後
我們會根據使用者的登入方式,建議不同的流程。
使用者未透過密碼金鑰登入
如果使用者未透過密碼金鑰登入您的網站,可能是因為該帳戶或目前的裝置未註冊密碼金鑰。此時鼓勵使用者建立密碼金鑰,請考慮下列方法:
- 將密碼升級為密碼金鑰:使用條件式建立,這是 WebAuthn 功能,可讓瀏覽器在使用者成功登入密碼後,自動為使用者建立密碼金鑰。這項功能可簡化密碼金鑰的建立程序,大幅提升密碼金鑰採用率。瞭解運作方式,以及如何實作「協助使用者更順暢地採用密碼金鑰」
- 手動提示建立密碼金鑰:鼓勵使用者建立密碼金鑰。使用者完成較複雜的登入程序 (例如多重驗證) 後,這項功能就能發揮效用。但請避免過度提示,以免干擾使用者體驗。」
如要瞭解如何鼓勵使用者建立密碼金鑰,以及其他最佳做法,請參閱「向使用者說明密碼金鑰」一文中的範例。
如果使用者已透過密碼金鑰登入
使用者成功透過密碼金鑰登入後,您可把握幾個機會,進一步提升使用者體驗並維持帳戶一致性。
在跨裝置驗證後,鼓勵使用者建立新的密碼金鑰
如果使用者透過跨裝置機制 (例如使用手機掃描 QR code) 登入,系統可能不會在登入裝置上儲存密碼金鑰。造成這種情況的可能原因如下:
- 他們有密碼金鑰,但密碼金鑰供應商不支援登入作業系統或瀏覽器。
- 他們無法在登入裝置上存取密碼金鑰提供者,但仍可在其他裝置上使用密碼金鑰。
在這種情況下,請考慮提示使用者在目前裝置上建立新的密碼金鑰。這樣他們日後就不必重複執行跨裝置登入程序。如要判斷使用者是否使用跨裝置密碼金鑰登入,請檢查憑證的 authenticatorAttachment 屬性。如果值為 "cross-platform",表示已進行跨裝置驗證。如果是,請說明建立新密碼金鑰的便利性,並引導他們完成建立程序。
使用信號與供應商同步處理密碼金鑰詳細資料
為確保一致性並提供更優質的使用者體驗,信賴方 (RP) 可以使用 WebAuthn Signals API,將憑證和使用者資訊的更新內容傳送給密碼金鑰供應商。
舉例來說,如要確保密碼金鑰提供者的使用者密碼金鑰清單正確無誤,請讓後端的憑證保持同步。您可以發出信號,表示密碼金鑰已不存在,讓密碼金鑰提供者移除不必要的密碼金鑰。
同樣地,如果使用者在您的服務中更新使用者名稱或顯示名稱,您可以發出信號,協助密碼金鑰供應商 (例如在帳戶選取對話方塊中) 顯示最新使用者資訊。
如要進一步瞭解如何讓密碼金鑰保持一致,請參閱「使用 Signal API 讓密碼金鑰與伺服器上的憑證保持一致」。
不要要求第二重驗證
密碼金鑰提供強大的內建保護機制,可防範網路釣魚等常見威脅。因此,第二個驗證因素不會大幅提升安全性。反而會增加使用者登入時的步驟。
檢查清單
- 允許使用者透過表單自動填入功能,以密碼金鑰登入。
- 當後端找不到相符的密碼金鑰憑證時,發出信號。
- 如果使用者登入後尚未建立密碼金鑰,請提示他們手動建立。
- 使用者以密碼 (和第二個驗證因子) 登入後,自動建立密碼金鑰 (有條件建立)。
- 如果使用者已使用跨裝置密碼金鑰登入,請提示使用者建立本機密碼金鑰。
- 在登入後或發生變更時,向供應商傳送可用密碼金鑰清單和更新的使用者詳細資料 (使用者名稱、顯示名稱)。