可偵測的憑證深入探索

密碼金鑰等 FIDO 憑證主要是用來取代密碼,但大部分機制也可讓使用者不必輸入使用者名稱,這樣使用者就能從目前網站的密碼金鑰清單中選取帳戶,藉此進行驗證。

舊版安全金鑰的設計屬於兩步驟驗證方法,且需要潛在憑證的 ID,因此需要輸入使用者名稱。安全金鑰在不知道金鑰 ID 的情況下找到的憑證稱為可偵測憑證。今天建立的大多數 FIDO 憑證都是可供偵測的憑證,特別是儲存在密碼管理工具或新型安全金鑰中的密碼金鑰。

為確保您的憑證可供偵測,請在建立密碼金鑰時指定 residentKeyrequireResidentKey

依賴方 (RP) 可以在密碼金鑰驗證期間省略 allowCredentials,使用可搜尋的憑證。在這種情況下,瀏覽器或系統會向使用者顯示可用的密碼金鑰清單,並由建立時設定的 user.name 屬性識別。如果使用者選取某個選項,產生的簽名中就會包含 user.id 值。然後伺服器就能使用該或傳回的憑證 ID 來查詢帳戶,而不使用輸入的使用者名稱。

帳戶選取器 UI 和先前討論的使用者介面一樣,一律不會顯示無法偵測的憑證。

requireResidentKeyresidentKey

如要建立可探索的憑證,請在 navigator.credentials.create() 中指定 authenticatorSelection.residentKeyauthenticatorSelection.requireResidentKey,並使用以下顯示的值。

async function register () {
  // ...

  const publicKeyCredentialCreationOptions = {
    // ...
    authenticatorSelection: {
      authenticatorAttachment: 'platform',
      residentKey: 'required',
      requireResidentKey: true,
    }
  };

  const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // ...
}

residentKey:

  • 'required':必須建立可探索的憑證。如果無法建立,系統會傳回 NotSupportedError
  • 'preferred':RP 偏好建立可探索的憑證,但接受無法偵測的憑證。
  • 'discouraged':RP 偏好建立無法偵測的憑證,但接受可探索的憑證。

requireResidentKey:

  • 為了回溯相容於 WebAuthn 第 1 級 (舊版規格),這個屬性則會保留。如果 residentKey'required',請將其設為 true,否則請設為 false

allowCredentials

RP 可以在 navigator.credentials.get() 上使用 allowCredentials 控管密碼金鑰驗證機制。密碼金鑰驗證程序通常有三種類型:

透過可供搜尋的憑證,RP 會顯示強制使用者選取帳戶選取器,讓使用者先選取要登入的帳戶,接著再進行驗證。這種做法適用於按下密碼金鑰驗證專用按鈕啟動的密碼金鑰驗證流程。

如要提供這種使用者體驗,請省略空白陣列或傳遞 navigator.credentials.get() 中的 allowCredentials 參數。

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // You can omit `allowCredentials` as well:
    allowCredentials: []
  };

  const credential = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}

顯示密碼金鑰表單自動填入功能

如果大多數使用者都使用密碼金鑰,並能在本機裝置上使用密碼金鑰,上述強制回應帳戶選取器就能正常運作。如果使用者沒有本機密碼金鑰,系統仍會顯示互動對話方塊,並提供另一部裝置的密碼金鑰。將使用者轉換為密碼金鑰時,建議您避免針對尚未設定這類金鑰的使用者套用 UI。

而是在傳統登入表單中為欄位選取密碼金鑰,以及已儲存的使用者名稱和密碼。這樣一來,擁有密碼金鑰的使用者只要選取密碼金鑰,即可在登入表單中「填寫」;擁有已儲存使用者名稱/密碼組合的使用者可以選取上述項目,即使無法自行輸入使用者名稱和密碼,也無法輸入。

如果 RP 正在進行遷移,但使用了混合用的密碼和密碼金鑰,這項功能就非常實用。

為提供這種使用者體驗,除了將空白陣列傳遞至 allowCredentials 屬性或省略參數之外,請在 navigator.credentials.get() 中指定 mediation: 'conditional',並使用 autocomplete="username webauthn"password 輸入欄位為 HTML username 輸入欄位加上 autocomplete="password webauthn" 註解。

呼叫 navigator.credentials.get() 時不會顯示任何 UI,但如果使用者聚焦在註解的輸入欄位,任何可用的密碼金鑰都會納入自動填入選項中。使用者選取任一驗證後,就會進行一般裝置解鎖驗證,而且只有 .get() 傳回的承諾會獲得結果。使用者如未選取密碼金鑰,承諾會一律無法解決。

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // You can omit `allowCredentials` as well:
    allowCredentials: []
  };

  const cred = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal,
    // Specify 'conditional' to activate conditional UI
    mediation: 'conditional'
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}
<input type="text" name="username" autocomplete="username webauthn" ...>

如要瞭解如何建構這項使用者體驗,請參閱「透過表單自動填入功能使用密碼金鑰登入」,以及「在網頁應用程式中使用表單自動填入功能實作密碼金鑰」程式碼研究室。

重新驗證

在某些情況下 (例如使用密碼金鑰重新驗證時),系統已經知道使用者的 ID。在這種情況下,我們想要使用密碼金鑰,但瀏覽器或作業系統不會顯示任何形式的帳戶選取器。實際做法為在 allowCredentials 參數中傳遞憑證 ID 清單。

在這些情況下,如果本機具有任何已命名的憑證,系統會提示使用者直接解鎖裝置。如果不是,系統會提示使用者提供另一部包含有效憑證的裝置 (手機或安全金鑰)。

為提供這項使用者體驗,請提供登入使用者的憑證 ID 清單。由於使用者已經知道使用者,因此 RP 應能夠查詢這些值。在 navigator.credentials.get()allowCredentials 屬性中,以 PublicKeyCredentialDescriptor 物件形式提供憑證 ID。

async function authenticate() {
  // ...

  const publicKeyCredentialRequestOptions = {
    // Server generated challenge:
    challenge: ****,
    // The same RP ID as used during registration:
    rpId: 'example.com',
    // Provide a list of PublicKeyCredentialDescriptors:
    allowCredentials: [{
      id: ****,
      type: 'public-key',
      transports: [
        'internal',
        'hybrid'
      ]
    }, {
      id: ****,
      type: 'public-key',
      transports: [
        'internal',
        'hybrid'
      ]
    }, ...]
  };

  const credential = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions,
    signal: abortController.signal
  });

  // This does not run until the user selects a passkey.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;
  
  // ...
}

PublicKeyCredentialDescriptor 物件由以下項目組成:

  • id:RP 所取得的公開金鑰憑證 ID 是註冊密碼金鑰時取得的。
  • type:這個欄位通常是 'public-key'
  • transports:持有此憑證的裝置支援的傳輸提示,瀏覽器用於最佳化要求使用者顯示外部裝置的 UI。這份清單 (如有提供) 應包含每個憑證註冊期間呼叫 getTransports() 的結果。

摘要

有了可供偵測的憑證功能,使用者就不必輸入使用者名稱,更易於使用密碼金鑰登入。玩家只要搭配使用 residentKeyrequireResidentKeyallowCredentials,就能達成下列目標的登入體驗:

  • 顯示強制回應帳戶選取器。
  • 顯示密碼金鑰表單已自動填入。
  • 重新驗證。

慎選可搜尋的憑證。如此一來,您可以設計複雜的密碼金鑰登入體驗,讓使用者享有順暢且更順暢的互動體驗。