可搜尋的憑證深入探索

雖然 FIDO 憑證 (例如密碼金鑰) 的目標是取代密碼,但大多數這類憑證也能讓使用者不必輸入使用者名稱。使用者可以從目前網站的密碼金鑰清單中選取帳戶,藉此完成驗證。

舊版安全金鑰是設計為兩步驟驗證方法,需要潛在憑證的 ID,因此必須輸入使用者名稱。安全金鑰不需知道 ID 即可找到的憑證稱為可探索憑證。目前建立的 FIDO 憑證大多是可探索的憑證,特別是儲存在密碼管理工具或新式安全金鑰中的密碼金鑰。

如要確保憑證是以密碼金鑰 (可探索的憑證) 形式建立,請在建立憑證時指定 residentKeyrequireResidentKey

信賴方 (RP) 可以省略密碼金鑰驗證期間的 allowCredentials,使用可探索的憑證。在這種情況下,瀏覽器或系統會向使用者顯示可用密碼金鑰清單,這些密碼金鑰是透過建立時設定的 user.name 屬性識別。如果使用者選取其中一個,user.id值會納入產生的簽章。伺服器接著可以使用該 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 Level 1 (規格的舊版)。如果 residentKey'required',請將此值設為 true,否則請設為 false

allowCredentials

RP 可以使用 allowCredentials 上的 navigator.credentials.get() 控制密碼金鑰驗證體驗。通常有三種密碼金鑰驗證體驗:

有了可探索的憑證,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" 註解 HTML username 輸入欄位,或使用 autocomplete="password webauthn" 註解 password 輸入欄位。

呼叫 navigator.credentials.get() 不會顯示任何 UI,但如果使用者將焦點放在已註解的輸入欄位,系統就會在自動填入選項中加入任何可用的密碼金鑰。如果使用者選取其中一個選項,系統會進行一般裝置解鎖驗證,只有在驗證通過後,.get() 傳回的 Promise 才會解析結果。如果使用者未選取密碼金鑰,Promise 就不會解析。

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。在這種情況下,我們希望使用密碼金鑰,但瀏覽器或 OS 不會顯示任何形式的帳戶選取器。只要在 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,RP 可以提供以下登入體驗:

  • 顯示模式帳戶選取器。
  • 顯示密碼金鑰表單自動填入功能。
  • 重新驗證。

請謹慎使用可探索的憑證。這樣一來,您就能設計出精密的密碼金鑰登入體驗,讓使用者覺得流暢,並更願意參與。