1. 打造內容
您一開始可以先使用支援密碼登入的基本網路應用程式。
接著,您將透過 WebAuthn,根據安全金鑰新增雙重驗證功能的支援。方法很簡單,只要進行以下操作即可:
- 使用者註冊 WebAuthn 憑證的方式。
- 雙重驗證流程,系統會要求使用者提供雙重驗證 (WebAuthn 憑證);如果已註冊該憑證,使用者就必須進行這項驗證。
- 憑證管理介面:可讓使用者重新命名及刪除憑證的憑證清單。
請參閱完成的網頁應用程式並試用看看。
2. WebAuthn 簡介
WebAuthn 基本概念
為什麼要使用 WebAuthn?
「網路詐騙」是網路上非常嚴重的安全性問題:大部分帳戶都使用低強度密碼或遭竊的密碼,可在各網站重複使用。業界對這個問題的集體反應已成為多重驗證,但實作方法是多段式技術,而許多技術仍不足以處理網路詐騙。
Web Authentication API (或稱 WebAuthn) 是一種標準化的標準化通訊協定,可供任何網路應用程式使用。
運作方式
資料來源:webauthn.guide
WebAuthn 可讓伺服器使用公開金鑰密碼編譯機制 (而非密碼) 註冊及驗證使用者。網站可以建立私人憑證,由私密/公開金鑰組組成。
- 私密金鑰會以安全的方式儲存在使用者的裝置上。
- 系統會將公開金鑰和隨機產生的憑證 ID 傳送至伺服器進行儲存。
伺服器會使用這組金鑰以驗證使用者身分。這並不容易,因為沒有對應的私密金鑰就沒有用了。
優點
WebAuthn 有兩大優點:
- 沒有共用密鑰:伺服器未儲存任何密鑰。這樣可以避免資料庫對駭客更有吸引力,因為公開金鑰對他們來說不實用。
- 限定範圍憑證:為
site.example
註冊的憑證無法用於evil-site.example
。以便使用 WebAuthn 網路詐騙。
用途
WebAuthn 的一個應用實例是使用安全金鑰的雙重驗證功能。這可能與企業網路應用程式相關。
瀏覽器支援
由 W3C 和 FIDO 撰寫,參與 Google、Mozilla、Microsoft、Yubico 等計劃的參與。
詞彙
- Authenticator:可註冊使用者,並用來宣告已註冊憑證的軟體或硬體實體。驗證器分為兩種類型:
- 漫遊驗證器:使用者可透過任何裝置登入,都可使用驗證器。例如:USB 安全金鑰、智慧型手機。
- 平台驗證器:內建於使用者裝置的驗證器。例如:Apple 的 Touch ID。
- 憑證:私密 - 公開金鑰組
- 信賴憑證者:用來驗證使用者的網站 (伺服器)
- FIDO 伺服器:用於驗證的伺服器。FIDO 是由 FIDO 聯盟開發的通訊協定組合,而其中一個通訊協定是 WebAuthn。
在這場研討會中,我們使用漫遊驗證工具。
3. 事前準備
軟硬體需求
如要完成這個程式碼研究室,您必須符合以下條件:
- 瞭解 WebAuthn 的基本知識。
- 對 JavaScript 和 HTML 有基本瞭解。
- 支援 WebAuthn 的最新版瀏覽器。
- 符合 UUF 規範的安全金鑰。
您可以使用下列其中一項做為安全金鑰:
- 搭載 Android>=7 (Nougat) 的 Android 手機,且搭載 Chrome。在這種情況下,您必須使用搭載藍牙功能的 Windows、macOS 或 Chrome OS 電腦。
- USB 金鑰,例如 YubiKey。
資料來源:https://www.yubico.com/products/security-key/
課程內容
你將學會 Blobstore
- 如何註冊並使用安全金鑰做為 WebAuthn 驗證的雙重驗證。
- 該如何讓使用者更容易完成這項程序。
你不會學會 ❌
- 如何建立 FIDO 伺服器 (用於驗證的伺服器)。這沒關係,因為一般而言,無論是網路應用程式或網站開發人員,還是必須仰賴現有的 FIDO 伺服器實作。請務必確認您採用的伺服器實作功能與品質。在這個程式碼研究室中,FIDO 伺服器使用 SimpleWebAuthn。如要瞭解其他選項,請參閱 FIDO 聯盟官方網頁。如需開放原始碼程式庫,請參閱 webauthn.io 或 AwesomeWebAuthn。
免責聲明
使用者必須輸入密碼才能登入。不過,為了方便起見,在這個程式碼研究室中,密碼不會儲存,也不會經過檢查。在實際應用程式中,您必須檢查伺服器端是否正確。
在這個程式碼研究室中,實作了基本安全性檢查,例如 CSRF 檢查、工作階段驗證和輸入內容清除。不過,我們仍有許多安全性措施。由於此處並未儲存密碼,因此無須這麼做。不過,請不要在實際工作環境中使用這組程式碼。
4. 設定驗證器
如果您使用的是 Android 手機做為驗證器
- 確認電腦和手機的 Chrome 皆為最新版本。
- 在電腦和手機上開啟 Chrome 並登入要用來存取這次工作坊的設定檔。
- 在電腦和手機上開啟這個設定檔的同步處理功能。請使用 chrome://settings/syncSetup 進行設定。
- 在電腦和手機中開啟藍牙。
- 在登入 Chrome 的電腦上,使用相同的設定檔開啟 webauthn.io。
- 輸入簡單的使用者名稱。將 [Attestation type] (認證類型) 和 [Authenticator type] (驗證者類型) 欄位保留為 [None] (無) 和 [Un 未指定] (預設) 值。按一下 [Register] (註冊)。
- 系統隨即會開啟瀏覽器視窗,要求您驗證身分。在清單中選取您的手機。
- 你會在手機上收到標題為「驗證您的身分」的通知。然後輕觸該應用程式。
- 在手機上,系統會要求你輸入手機的 PIN 碼 (或輕觸指紋感應器)。請輸入。
- 在桌面的 webauthn.io 上,應該會顯示「成功」指標。
- 在桌面上前往 webauthn.io,然後按一下 [登入] 按鈕。
- 同樣地,系統會開啟瀏覽器視窗,請在清單中選取您的手機。
- 在手機上輕觸彈出式通知,然後輸入 PIN 碼 (或輕觸指紋感應器即可)。
- webauthn.io 會通知您您已登入。你的手機目前是以安全金鑰正常使用;您已經為這次講習課程做好準備!
如果您使用 USB 安全金鑰做為驗證器
- 在 Chrome 桌面中開啟 webauthn.io。
- 輸入簡單的使用者名稱。將 [Attestation type] (認證類型) 和 [Authenticator type] (驗證者類型) 欄位保留為 [None] (無) 和 [Un 未指定] (預設) 值。按一下 [Register] (註冊)。
- 系統隨即會開啟瀏覽器視窗,要求您驗證身分。選取清單中的 [USB 安全金鑰]。
- 將安全金鑰插入桌面,然後輕觸該安全金鑰。
- 在桌面的 webauthn.io 上,應該會顯示「成功」指標。
- 在電腦版 的 webauthn.io 中,按一下 [登入] 按鈕。
- 同樣地,系統應會開啟瀏覽器視窗,請在清單中選取 [USB 安全金鑰]。
- 輕觸金鑰。
- Webauthn.io 應該會通知您您已登入。您的 USB 安全金鑰運作正常;您已為研討會做好準備!
5. 做好準備
在這個程式碼研究室中,您將使用 Glitch,這個線上程式碼編輯器會自動立即部署您的程式碼。
建立範例程式碼
開啟入門專案。
按一下 [Remix] 按鈕。
這會建立一份範例程式碼。現在,您可以自行編輯程式碼。您的分支 (在「Glitch」中稱為「重混」) 可讓您完成這個程式碼研究室的所有工作。
探索範例程式碼
探索您剛準備的範例程式碼。
在「libs
」下,系統已提供名為「auth.js
」的程式庫。這個自訂程式庫會負責管理伺服器端驗證邏輯。使用 fido
程式庫做為依附元件。
6. 實作憑證註冊
實作憑證註冊
如要透過安全金鑰設定雙重驗證功能,首先必須讓使用者建立憑證。
讓我們先在用戶端程式碼中新增 函式,然後加入這個函式。
在 public/auth.client.js
中,請注意「registerCredential()
」函式目前尚無任何作用。新增下列程式碼:
async function registerCredential() {
// Fetch the credential creation options from the backend
const credentialCreationOptionsFromServer = await _fetch(
"/auth/credential-options",
"POST"
);
// Decode the credential creation options
const credentialCreationOptions = decodeServerOptions(
credentialCreationOptionsFromServer
);
// Create a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
const credential = await navigator.credentials.create({
publicKey: {
...credentialCreationOptions,
}
});
// Encode the newly created credential to send it to the backend
const encodedCredential = encodeCredential(credential);
// Send the encoded credential to the backend for storage
return await _fetch("/auth/credential", "POST", encodedCredential);
}
請注意,您已匯出這個函式。
以下是registerCredential
的功能:
- 從伺服器 (
/auth/credential-options
) 擷取憑證建立選項 - 由於伺服器選項已重新編碼,因此會使用公用程式函式
decodeServerOptions
進行解碼。 - 會呼叫 Web API
navigator.credential.create
來建立憑證。呼叫navigator.credential.create
時,瀏覽器會接管,並提示使用者選擇安全金鑰。 - 將新建立的憑證解碼
- 它會向
/auth/credential
發出要求經過編碼的憑證,藉此在伺服器端註冊新的憑證。
另請複習一下:查看伺服器程式碼
registerCredential()
會呼叫兩個伺服器,因此請花點時間檢查後端發生了什麼事。
憑證建立選項
當用戶端向 (/auth/credential-options
) 發出要求時,伺服器會產生選項物件,並將物件傳回用戶端。
然後,用戶端會在實際憑證建立呼叫中使用這個物件:
navigator.credentials.create({
publicKey: {
// Options generated server-side
...credentialCreationOptions
// ...
}
那麼,此 credentialCreationOptions
在最後一步驟已經實作的用戶端 registerCredential
中有哪些部分?
查看 router.post:"quot;/credential-options", ... 底下的伺服器程式碼。
我們並不是要逐一查看每項資源,但是您可以在 Server code 選項選項 (可透過 fido2
程式庫產生,最後傳回用戶端) 中看到一些有趣的屬性:
rpName
和rpId
是用來註冊及驗證使用者的機構。請記住,在 WebAuthn 中,憑證範圍限定在特定網域,這是一種安全性優勢;rpName
和rpId
用於界定憑證範圍。有效的rpId
是指網站主機名稱。請注意,當您啟動新手專案時,系統會自動更新這些資訊 🧘? ♀️excludeCredentials
是憑證清單;系統無法在excludeCredentials
上列出包含其中一個憑證的驗證者。在我們的程式碼研究室中,excludeCredentials
是此使用者現有的憑證清單。有了這個憑證,user.id
就能確保使用者建立的每個憑證都會存放在不同的驗證器 (安全金鑰) 中。這是個不錯的做法,因為當使用者註冊多個憑證時,他們會位於不同的驗證器 (安全金鑰) 中,所以即使遺失一個安全金鑰,也無法鎖定使用者的帳戶。authenticatorSelection
定義您要允許在網頁應用程式中使用的驗證器類型。讓我們進一步探討authenticatorSelection
:residentKey: preferred
表示這個應用程式不強制執行用戶端可偵測的憑證。用戶端憑證我們已完成preferred
的設定程序,因為這個程式碼研究室將著重在基本實作;可供搜尋的憑證適用於更進階的流程。requireResidentKey
僅適用於與 WebAuthn v1 回溯相容。userVerification: preferred
表示驗證器支援使用者驗證功能 (例如驗證金鑰是否為生物特徵辨識安全金鑰或內建 PIN 碼功能的金鑰),信賴憑證者會在建立憑證時要求驗證。如果驗證工具沒有基本的安全金鑰,伺服器就不會要求驗證使用者。
pubKeyCredParam
會依照優先順序逐一說明憑證的加密編譯屬性。
這些選項皆為網路應用程式針對安全性模型做出的決定。在伺服器上觀察到這些選項時,系統會在單一 authSettings
物件中定義這些選項。
挑戰
這裡還有一個有趣的一點:req.session.challenge = options.challenge;
由於 WebAuthn 是加密編譯通訊協定,因此必須仰賴隨機驗證來避免重複進行攻擊。此外,當攻擊者竊取酬載以重新執行驗證時,不會成為可進行驗證的私密金鑰擁有者。
為降低這種狀況,我們會在伺服器上產生驗證,並即時簽署;隨後,系統會將該簽名與預期值比較。這可以驗證使用者是否在產生憑證時保留私密金鑰。
憑證註冊碼
查看 router.post:"quot;/credential", ... 底下的伺服器程式碼。
也就是在伺服器端註冊憑證。
這是怎麼了?
驗證碼中最值得注意的其中一個是 fido2.verifyAttestationResponse
上的驗證呼叫:
- 已勾選「已簽署的驗證」,以確保憑證是在建立時實際保留私密金鑰的使用者所建立。
- 而相依的一方 ID 則與其原始來源相同。這樣可以確保憑證繫結至此網路應用程式 (僅限此網路應用程式)。
將這項功能新增至使用者介面
現在您的函式會建立憑證,因此「RegisterCredential(),
」已可供使用,這時請開放使用者存取憑證。
請從「帳戶」頁面執行這項操作,因為這是管理驗證的一般位置。
在 account.html
標記中,使用者名稱下方有個空白的 div
,其版面配置類別為 class="flex-h-between"
。我們會使用這個 div
來處理與 2FA 功能相關的 UI 元素。
新增此 div:
- 標題顯示「雙重驗證」
- 建立憑證的按鈕
<div class="flex-h-between">
<h3>
Two-factor authentication
</h3>
<button class="create" id="registerButton" raised>
➕ Add a credential
</button>
</div>
在 div 下方,新增稍後將需要的憑證 div:
<div class="flex-h-between">
(HTML you've just added)
</div>
<div id="credentials"></div>
在 account.html
內嵌指令碼中,匯入您剛建立的函式、加入可呼叫該函式的函式 register
,以及附加於您剛建立的按鈕的事件處理常式。
// Set up the handler for the button that registers credentials
const registerButton = document.querySelector('#registerButton');
registerButton.addEventListener('click', register);
// Register a credential
async function register() {
let user = {};
try {
const user = await registerCredential();
} catch (e) {
// Alert the user that something went wrong
if (Array.isArray(e)) {
alert(
// `msg` not `message`, this is the key's name as per the express validator API
`Registration failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
);
} else {
alert(`Registration failed. ${e}`);
}
}
}
顯示使用者看到的憑證
現在您已經新增了建立憑證的功能,使用者需要查看他們新增的憑證。
「帳戶」頁面是很好的選擇。
在 account.html
中,尋找名為 updateCredentialList()
的函式。
加入下列程式碼,以發出後端呼叫,以擷取目前登入使用者的所有已註冊憑證,並顯示顯示的憑證:
// Update the list that displays credentials
async function updateCredentialList() {
// Fetch the latest credential list from the backend
const response = await _fetch('/auth/credentials', 'GET');
const credentials = response.credentials || [];
// Generate the credential list as HTML and pass remove/rename functions as args
const credentialListHtml = getCredentialListHtml(
credentials,
removeEl,
renameEl
);
// Display the list of credentials in the DOM
const list = document.querySelector('#credentials');
render(credentialListHtml, list);
}
現在,別忘記removeEl
和renameEl
,你之後可以在這個程式碼研究室中進一步瞭解這些方法。
在內嵌指令碼開始時,在 account.html
內於 updateCredentialList
中新增一個呼叫。透過這個呼叫,系統會擷取使用者到達帳戶頁面時所取得的憑證。
<script type="module">
// ... (imports)
// Initialize the credential list by updating it once on page load
updateCredentialList();
現在,在 registerCredential
順利完成後呼叫 updateCredentialList
,讓清單顯示新建立的憑證:
async function register() {
let user = {};
try {
// ...
} catch (e) {
// ...
}
// Refresh the credential list to display the new credential
await updateCredentialList();
}
馬上來試用個人助理功能吧!👩?會
您已完成憑證註冊!使用者現在可以建立安全金鑰憑證,並在「帳戶」頁面中以視覺化的方式呈現這些憑證。
試試看:
- 。
- 以使用者和密碼登入。如前所述,密碼其實不會經過檢查,以確保內容正確。請輸入任何非空白密碼。
- 請在「Account」(帳戶) 頁面上按一下 [Add a credentials] (新增憑證)。
- 系統應會提示你插入及輕觸安全金鑰。所以,
- 憑證建立成功後,帳戶頁面應該就會顯示憑證。
- 重新載入「帳戶」頁面。系統應會顯示憑證。
- 如果您有兩個金鑰可用,請嘗試新增兩個不同的安全金鑰做為憑證。兩者都應顯示。
- 嘗試使用相同的驗證器 (金鑰) 建立兩個憑證;請注意,系統不支援這種憑證。這正是我們刻意在後端使用
excludeCredentials
的原因。
7. 啟用雙重驗證功能
使用者可以註冊及取消註冊憑證,但系統只會顯示憑證,但實際上並未使用。
現在該讓他們實際使用,並且設定實際的雙重驗證。
在本節中,您將變更此網路應用程式中應用程式的驗證流程:
傳送至以下雙重流程:
實作雙重驗證
我們先加入所需的功能,並且實作與後端的通訊功能;我們會在下一個步驟中,將這組金鑰新增至前端。
您需要實作的函式,可透過憑證驗證使用者。
在 public/auth.client.js
中,尋找空函式 authenticateTwoFactor
,並新增下列程式碼:
async function authenticateTwoFactor() {
// Fetch the 2F options from the backend
const optionsFromServer = await _fetch("/auth/two-factor-options", "POST");
// Decode them
const decodedOptions = decodeServerOptions(optionsFromServer);
// Get a credential via the browser API; this will prompt the user to touch their security key or tap a button on their phone
const credential = await navigator.credentials.get({
publicKey: decodedOptions
});
// Encode the credential
const encodedCredential = encodeCredential(credential);
// Send it to the backend for verification
return await _fetch("/auth/authenticate-two-factor", "POST", {
credential: encodedCredential
});
}
請注意,此函式已經匯出完成,因此將在下一個步驟中使用。
以下是authenticateTwoFactor
的功能:
- 可向伺服器要求雙重驗證選項。就跟您先前看過的憑證建立選項一樣,這些選項都是在伺服器上定義,且取決於網路應用程式的安全性模型。詳情請參閱
router.post("/two-factors-options", ...
下方的伺服器代碼。 - 呼叫
navigator.credentials.get
可讓瀏覽器接管並提示使用者插入及輕觸先前註冊的金鑰。這樣就能為這項特定的雙重驗證作業選擇憑證。 - 接著,所選憑證隨即會在後端要求中傳送到 fetchfetch_quo//auth/authenticate-two-result".。如果憑證適用於該使用者,系統就會驗證該使用者。
另請複習一下:查看伺服器程式碼
請注意,server.js
已經處理了一些瀏覽和存取作業,可確保只有「已驗證」使用者才能存取「帳戶」網頁,並且執行一些必要的重新導向作業。
現在,請查看在 router.post("/initialize-authentication", ...
下的伺服器程式碼。
這裡有兩個要點:
- 在這個階段,密碼和憑證都會同時檢查。這是一項安全措施:對於已設定雙重驗證功能的使用者,我們不建議使用密碼驗證 (視密碼是否正確而定) 的流程。因此,在這個步驟中,我們會同時檢查密碼和憑證。
completeAuthentication(req, res);
在使用者流程中加入雙重驗證頁面
然後在「views
」資料夾中查看新頁面「second-factor.html
」。
裡面有 [使用安全金鑰] 按鈕,但目前尚未提供任何功能。
讓這個按鈕在點擊時呼叫 authenticateTwoFactor()
。
- 如果
authenticateTwoFactor()
成功,請將使用者重新導向至對方的「Account」(帳戶) 頁面。 - 如果失敗,請告知使用者發生錯誤。在實際應用中,您實作了更多實用的錯誤訊息 — 為求簡單易懂,我們會僅使用視窗警示。
<main>
...
</main>
<script type="module">
import { authenticateTwoFactor, authStatuses } from "/auth.client.js";
const button = document.querySelector("#authenticateButton");
button.addEventListener("click", async e => {
try {
// Ask the user to authenticate with the second factor; this will trigger a browser prompt
const response = await authenticateTwoFactor();
const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
// The user is properly authenticated => Navigate to the Account page
location.href = "/account";
} else {
throw new Error("Two-factor authentication failed");
}
} catch (e) {
// Alert the user that something went wrong
alert(`Two-factor authentication failed. ${e}`);
}
});
</script>
</body>
</html>
使用雙重驗證功能
現在您已完成所有設定,以便新增雙重驗證步驟。
現在,您需要為已設定雙重驗證功能的使用者,在 index.html
中新增這個步驟。
在 index.html
中,在 location.href = "/account";
下方加入程式碼,引導使用者前往雙重驗證頁面 (前提是他們已設定兩步驟驗證功能)。
在這個程式碼研究室中,建立憑證會自動為使用者啟用雙重驗證功能。
請注意,server.js
也會執行伺服器端工作階段檢查,確保只有經過驗證的使用者可以存取 account.html
。
const { authStatus } = response;
if (authStatus === authStatuses.COMPLETE) {
// The user is properly authenticated => navigate to account
location.href = '/account';
} else if (authStatus === authStatuses.NEED_SECOND_FACTOR) {
// Navigate to the two-factor-auth page because two-factor-auth is set up for this user
location.href = '/second-factor';
}
馬上來試用個人助理功能吧!👩?會
- 使用新使用者 johndoe 登入。
- 登出。
- 以 johndoe 的身分登入帳戶;使用者只須輸入密碼即可。
- 建立憑證。也就是說,您啟用了 johndoe 的雙重驗證功能。
- 登出。
- 輸入您的使用者名稱 johndoe 和密碼。
- 瞭解系統如何自動前往雙重驗證頁面。
- (請前往
/account
存取「帳戶」頁面;請注意,由於未通過驗證,所以系統如何將您重新導向至索引頁面:您缺少第二項因素) - 返回雙重驗證頁面,然後按一下 [使用安全金鑰] 進行雙重驗證。
- 您已經登入,系統應該就會顯示「帳戶」頁面!
8. 更輕鬆地使用憑證
你已使用安全金鑰完成雙重驗證的基本功能 🚀?
但是... 你注意到嗎?
我們的憑證清單目前並不方便:憑證 ID 和公開金鑰是管理憑證時的長字串,並不是用來管理憑證!人類使用長字串和數字表示不好 🤖?
因此,我們改善了這項功能,並新增功能,使用使用者可理解的字串為憑證命名及重新命名。
查看重命名 Credential
為了節省您執行這個函式時耗費的時間,我們已在 auth.client.js
的範例程式碼中加入了用來重新命名憑證的函式:
async function renameCredential(credId, newName) {
const params = new URLSearchParams({
credId,
name: newName
});
return _fetch(
`/auth/credential?${params}`,
"PUT"
);
}
這是一般的資料庫更新呼叫:用戶端會傳送 PUT
要求至後端,同時具有該憑證的憑證 ID 和新名稱。
實作自訂憑證名稱
在 account.html
中,請注意空白函式 rename
。
新增下列程式碼:
// Rename a credential
async function rename(credentialId) {
// Let the user input a new name
const newName = window.prompt(`Name this credential:`);
// Rename only if the user didn't cancel AND didn't enter an empty name
if (newName && newName.trim()) {
try {
// Make the backend call to rename the credential (the name is sanitized) server-side
await renameCredential(credentialId, newName);
} catch (e) {
// Alert the user that something went wrong
if (Array.isArray(e)) {
alert(
// `msg` not `message`, this is the key's name as per the express validator API
`Renaming failed. ${e.map((err) => `${err.msg} (${err.param})`)}`
);
} else {
alert(`Renaming failed. ${e}`);
}
}
// Refresh the credential list to display the new name
await updateCredentialList();
}
}
憑證建立完成後,您不妨為憑證命名。因此,請建立一個沒有名稱的憑證,並在建立成功後重新命名憑證。不過這樣會導致兩個後端呼叫發生。
請在 register()
中使用 rename
函式,讓使用者在註冊時為憑證命名:
async function register() {
let user = {};
try {
const user = await registerCredential();
// Get the latest credential's ID (newly created credential)
const allUserCredentials = user.credentials;
const newCredential = allUserCredentials[allUserCredentials.length - 1];
// Rename it
await rename(newCredential.credId);
} catch (e) {
// ...
}
// Refresh the credential list to display the new credential
await updateCredentialList();
}
請注意,系統會在後端驗證使用者輸入內容,並予以處理:
check("name")
.trim()
.escape()
顯示憑證名稱
前往templates.js
的「getCredentialHtml
」。
請注意,目前已有憑證可在憑證卡頂端顯示憑證名稱:
// Register credential
const getCredentialHtml = (credential, removeEl, renameEl) => {
const { name, credId, publicKey } = credential;
return html`
<div class="credential-card">
<div class="credential-name">
${name
? html`
${name}
`
: html`
<span class="unnamed">(Unnamed)</span>
`}
</div>
// ...
</div>
`;
};
馬上來試用個人助理功能吧!👩?會
- 建立憑證。
- 系統會提示您命名。
- 輸入新名稱,然後按一下 [確定]。
- 憑證現已重新命名。
- 重複以上名稱,看看是否將名稱欄位留空,即可順利進行作業。
啟用憑證重新命名
使用者可能需要重新命名憑證,例如新增第二個金鑰,並重新命名第一個金鑰,以便清楚區分憑證。
在 account.html
中,找出最寬鬆的空白函式 renameEl
,並新增下列程式碼:
// Rename a credential via HTML element
async function renameEl(el) {
// Define the ID of the credential to update
const credentialId = el.srcElement.dataset.credentialId;
// Rename the credential
await rename(credentialId);
// Refresh the credential list to display the new name
await updateCredentialList();
}
現在,在 templates.js
的 getCredentialHtml
中,於 class="flex-end"
div 中加入以下程式碼,即可在憑證卡範本中加入 [重新命名] 按鈕;按一下該按鈕,即會呼叫我們剛剛建立的 renameEl
函式:
const getCredentialHtml = (credential, removeEl, renameEl) => {
// ...
<div class="flex-end">
<button
data-credential-id="${credId}"
@click="${renameEl}"
class="secondary right"
>
Rename
</button>
</div>
// ...
`;
};
馬上來試用個人助理功能吧!👩?會
- 按一下 [Rename] (重新命名)。
- 請在系統提示時輸入新名稱。
- 按一下「OK」(確定)。
- 憑證應已成功重新命名,且清單應自動更新。
- 重新載入網頁仍應顯示新名稱 (這表示新名稱仍會顯示在伺服器端)。
顯示憑證建立日期
透過 navigator.credential.create()
建立的憑證中沒有建立日期。
但是,由於這項資訊對使用者而言非常實用,因此有助於區分憑證憑證,因此我們在範例程式碼中調整了伺服器端程式庫,並且在儲存新憑證時新增了 creationDate
欄位 (等於 Date.now()
)。
在「class="creation-date"
」的div
「templates.js
」中,加入以下內容,向使用者顯示建立日期資訊:
<div class="creation-date">
<label>Created:</label>
<div class="info">
${new Date(creationDate).toLocaleDateString()}
${new Date(creationDate).toLocaleTimeString()}
</div>
</div>
9. 讓程式碼更易於使用
目前,我們只要求使用者註冊簡單的漫遊驗證器,在登入時再使用第二重驗證。
更進階的方法是採用更強大的驗證工具:使用者驗證漫遊驗證器 (UVRA)。UVRA 可在單一登入流程中提供兩種驗證因子,以及網路詐騙。
理想上,您能同時使用這兩種方法。為此,您必須自訂使用者體驗:
- 如果使用者只採用簡單的 (未驗證) 漫遊驗證工具,請讓使用者透過該憑證執行可防範網路詐騙的帳戶啟動程序,但也必須輸入使用者名稱和密碼。我們的程式碼研究室已經完成這一步。
- 如果其他使用者採用更進階的驗證機制,可驗證漫遊驗證工具,則在帳戶啟動期間,他們將略過密碼步驟 (甚至可能輸入使用者名稱)。
如要進一步瞭解相關資訊,請參閱使用無密碼登入程序時,以防範網路詐騙的方式啟動帳戶防護功能一文。
在這個程式碼研究室中,我們「不會」自訂使用者體驗,但我們會設定程式碼集,讓您取得必要的資料,以便自訂使用者體驗。
您需要兩個動作:
- 在後端設定中調整
residentKey: preferred
。系統已為您完成這個動作。 - 設定尋找可探索憑證 (也稱為居民金鑰) 的方式。
如要查詢是否已建立可供搜尋的憑證,請按照下列步驟操作:
- 在憑證建立時查詢
credProps
的值 (credProps: true
)。 - 建立憑證時查詢
transports
的值。這有助於您確定基礎平台是否支援 UVRA 功能,例如該功能是否真的是行動電話。 - 在後端儲存
credProps
和transports
的值。系統已在範例程式碼中為您完成這項操作。想知道嗎?歡迎參考auth.js
。
讓我們取得 credProps
和 transports
的值,並將值傳送到後端。在 auth.client.js
中,按照下列方式修改 registerCredential
:
- 在呼叫
navigator.credentials.create
時新增extensions
欄位 - 請先設定
encodedCredential.transports
和encodedCredential.credProps
,再將憑證傳送至後端進行儲存。
registerCredential
應如下所示:
async function registerCredential() {
// Fetch the credential creation options from the backend
const credentialCreationOptionsFromServer = await _fetch(
'/auth/credential-options',
'POST'
);
// Decode the credential creation options
const credentialCreationOptions = decodeServerOptions(
credentialCreationOptionsFromServer
);
// Create a credential via the browser API; this will prompt the user
const credential = await navigator.credentials.create({
publicKey: {
...credentialCreationOptions,
extensions: {
credProps: true,
},
},
});
// Encode the newly created credential to send it to the backend
const encodedCredential = encodeCredential(credential);
// Set transports and credProps for more advanced user flows
encodedCredential.transports = credential.response.getTransports();
encodedCredential.credProps =
credential.getClientExtensionResults().credProps;
// Send the encoded credential to the backend for storage
return await _fetch('/auth/credential', 'POST', encodedCredential);
}
10. 確保跨瀏覽器支援
支援非 Chromium 瀏覽器
在 public/auth.client.js
的 registerCredential
函式中,我們呼叫了新建的憑證的 credential.response.getTransports()
,以在後端將這些資訊儲存為伺服器提示。
不過,getTransports()
目前尚未在所有瀏覽器中實作 (與 getClientExtensionResults
不同瀏覽器支援的功能不同):getTransports()
呼叫會在 Firefox 和 Safari 中擲回錯誤,因而無法在這些瀏覽器中建立憑證。
為了確保程式碼在所有的主要瀏覽器中都能執行,請將 encodedCredential.transports
呼叫納入條件中:
if (credential.response.getTransports) {
encodedCredential.transports = credential.response.getTransports();
}
請注意,伺服器中的 transports
已設為 transports || []
。在 Firefox 和 Safari 中,transports
清單不會為 undefined
,但會排除空白的 []
清單,因此不會發生錯誤。
警告使用者使用不支援 WebAuthn 的瀏覽器
即使所有主要瀏覽器皆支援 WebAuthn,但建議您在不支援 WebAuthn 的瀏覽器中顯示警告訊息。
在 index.html
中,觀察這項 div 的存在:
<div id="warningbanner" class="invisible">
⚠️ Your browser doesn't support WebAuthn. Open this demo in Chrome, Edge, Firefox or Safari.
</div>
在 index.html
的內嵌指令碼中加入以下程式碼,在不支援 WebAuthn 的瀏覽器中顯示橫幅:
// Display a banner in browsers that don't support WebAuthn
if (!window.PublicKeyCredential) {
document.querySelector('#warningbanner').classList.remove('invisible');
}
在實際的網路應用程式中,您可以更精準地執行這些瀏覽器,並對這些瀏覽器使用適當的備用機制,但您可以學習如何檢查 WebAuthn 的支援。
11. 非常好!
☆你已完成!
您已使用安全金鑰實作雙重驗證。
在本程式碼研究室中,我們探討了基本概念。如果您想進一步探索 WebAuthn 的 2FA 功能,可以參考下列建議:
- 將「上次使用」新增至憑證資訊卡。這項資訊可協助使用者判斷特定安全金鑰是否遭到主動使用,特別是他們是否已註冊過多個金鑰。
- 採用更強大的錯誤處理機制,並提供更精準的錯誤訊息。
- 請仔細查看
auth.js
,並說明當您變更部分authSettings
時會發生什麼情況,尤其是使用支援使用者驗證的金鑰時。