使用安全金鑰 (WebAuthn) 使用雙重驗證功能來保護您的網站

1. 打造內容

您一開始可以先使用支援密碼登入的基本網路應用程式。

接著,您將透過 WebAuthn,根據安全金鑰新增雙重驗證功能的支援。方法很簡單,只要進行以下操作即可:

  • 使用者註冊 WebAuthn 憑證的方式。
  • 雙重驗證流程,系統會要求使用者提供雙重驗證 (WebAuthn 憑證);如果已註冊該憑證,使用者就必須進行這項驗證。
  • 憑證管理介面:可讓使用者重新命名及刪除憑證的憑證清單。

16ce77744061c5f7.png

請參閱完成的網頁應用程式並試用看看。

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

6539dc7ffec2538c.png

資料來源:https://www.yubico.com/products/security-key/

dd56e2cfe0f7ced2.png

課程內容

你將學會 Blobstore

  • 如何註冊並使用安全金鑰做為 WebAuthn 驗證的雙重驗證。
  • 該如何讓使用者更容易完成這項程序。

你不會學會 ❌

  • 如何建立 FIDO 伺服器 (用於驗證的伺服器)。這沒關係,因為一般而言,無論是網路應用程式或網站開發人員,還是必須仰賴現有的 FIDO 伺服器實作。請務必確認您採用的伺服器實作功能與品質。在這個程式碼研究室中,FIDO 伺服器使用 SimpleWebAuthn。如要瞭解其他選項,請參閱 FIDO 聯盟官方網頁。如需開放原始碼程式庫,請參閱 webauthn.ioAwesomeWebAuthn

免責聲明

使用者必須輸入密碼才能登入。不過,為了方便起見,在這個程式碼研究室中,密碼不會儲存,也不會經過檢查。在實際應用程式中,您必須檢查伺服器端是否正確。

在這個程式碼研究室中,實作了基本安全性檢查,例如 CSRF 檢查、工作階段驗證和輸入內容清除。不過,我們仍有許多安全性措施。由於此處並未儲存密碼,因此無須這麼做。不過,請不要在實際工作環境中使用這組程式碼。

4. 設定驗證器

如果您使用的是 Android 手機做為驗證器

  • 確認電腦和手機的 Chrome 皆為最新版本。
  • 在電腦和手機上開啟 Chrome 並登入要用來存取這次工作坊的設定檔。
  • 電腦手機上開啟這個設定檔的同步處理功能。請使用 chrome://settings/syncSetup 進行設定。
  • 在電腦和手機中開啟藍牙。
  • 在登入 Chrome 的電腦上,使用相同的設定檔開啟 webauthn.io
  • 輸入簡單的使用者名稱。將 [Attestation type] (認證類型) 和 [Authenticator type] (驗證者類型) 欄位保留為 [None] (無) 和 [Un 未指定] (預設) 值。按一下 [Register] (註冊)。

6b49ff0298f5a0af.png

  • 系統隨即會開啟瀏覽器視窗,要求您驗證身分。在清單中選取您的手機。

ffebe58ac826eaf2.png 852de328fcd4eb42.png

  • 你會在手機上收到標題為「驗證您的身分」的通知。然後輕觸該應用程式。
  • 在手機上,系統會要求你輸入手機的 PIN 碼 (或輕觸指紋感應器)。請輸入。
  • 在桌面的 webauthn.io 上,應該會顯示「成功」指標。

fc0acf00a4d412fa.png

  • 在桌面上前往 webauthn.io,然後按一下 [登入] 按鈕。
  • 同樣地,系統會開啟瀏覽器視窗,請在清單中選取您的手機。
  • 在手機上輕觸彈出式通知,然後輸入 PIN 碼 (或輕觸指紋感應器即可)。
  • webauthn.io 會通知您您已登入。你的手機目前是以安全金鑰正常使用;您已經為這次講習課程做好準備!

如果您使用 USB 安全金鑰做為驗證器

  • 在 Chrome 桌面中開啟 webauthn.io
  • 輸入簡單的使用者名稱。將 [Attestation type] (認證類型) 和 [Authenticator type] (驗證者類型) 欄位保留為 [None] (無) 和 [Un 未指定] (預設) 值。按一下 [Register] (註冊)。
  • 系統隨即會開啟瀏覽器視窗,要求您驗證身分。選取清單中的 [USB 安全金鑰]

ffebe58ac826eaf2.png 9fe75f04e43da035.png

  • 將安全金鑰插入桌面,然後輕觸該安全金鑰。

923d5adb8aa8286c.png

  • 在桌面的 webauthn.io 上,應該會顯示「成功」指標。

fc0acf00a4d412fa.png

  • 在電腦版 的 webauthn.io 中,按一下 [登入] 按鈕。
  • 同樣地,系統應會開啟瀏覽器視窗,請在清單中選取 [USB 安全金鑰]
  • 輕觸金鑰。
  • Webauthn.io 應該會通知您您已登入。您的 USB 安全金鑰運作正常;您已為研討會做好準備!

7e1c0bb19c9f3043.png

5. 做好準備

在這個程式碼研究室中,您將使用 Glitch,這個線上程式碼編輯器會自動立即部署您的程式碼。

建立範例程式碼

開啟入門專案

按一下 [Remix] 按鈕。

這會建立一份範例程式碼。現在,您可以自行編輯程式碼。您的分支 (在「Glitch」中稱為「重混」) 可讓您完成這個程式碼研究室的所有工作。

cf2b9f552c9809b6.png

探索範例程式碼

探索您剛準備的範例程式碼。

在「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 程式庫產生,最後傳回用戶端) 中看到一些有趣的屬性:

  • rpNamerpId 是用來註冊及驗證使用者的機構。請記住,在 WebAuthn 中,憑證範圍限定在特定網域,這是一種安全性優勢;rpNamerpId 用於界定憑證範圍。有效的 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);
}    

現在,別忘記removeElrenameEl,你之後可以在這個程式碼研究室中進一步瞭解這些方法。

在內嵌指令碼開始時,在 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. 啟用雙重驗證功能

使用者可以註冊及取消註冊憑證,但系統只會顯示憑證,但實際上並未使用。

現在該讓他們實際使用,並且設定實際的雙重驗證。

在本節中,您將變更此網路應用程式中應用程式的驗證流程:

6ff49a7e520836d0.png

傳送至以下雙重流程:

e7409946cd88efc7.png

實作雙重驗證

我們先加入所需的功能,並且實作與後端的通訊功能;我們會在下一個步驟中,將這組金鑰新增至前端。

您需要實作的函式,可透過憑證驗證使用者。

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&quot.。如果憑證適用於該使用者,系統就會驗證該使用者。

另請複習一下:查看伺服器程式碼

請注意,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 中新增這個步驟。

322a5c49d865a0d8.png

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.jsgetCredentialHtml 中,於 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"」的divtemplates.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 功能,例如該功能是否真的是行動電話。
  • 在後端儲存 credPropstransports 的值。系統已在範例程式碼中為您完成這項操作。想知道嗎?歡迎參考 auth.js

讓我們取得 credPropstransports 的值,並將值傳送到後端。在 auth.client.js 中,按照下列方式修改 registerCredential

  • 在呼叫 navigator.credentials.create 時新增 extensions 欄位
  • 請先設定 encodedCredential.transportsencodedCredential.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.jsregisterCredential 函式中,我們呼叫了新建的憑證的 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 的瀏覽器

1e9c1be837d66ce8.png

即使所有主要瀏覽器皆支援 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 時會發生什麼情況,尤其是使用支援使用者驗證的金鑰時。