伺服器端密碼金鑰註冊

總覽

以下概略說明註冊密碼金鑰時須採取的重要步驟:

密碼金鑰註冊流程

  • 定義建立密碼金鑰的選項。請將這些金鑰傳送給用戶端,以便將對方傳送到密碼金鑰建立呼叫:WebAuthn API 呼叫 navigator.credentials.create (網頁版),以及 Android (Android) 呼叫 credentialManager.createCredential。使用者確認建立密碼金鑰後,系統會解決密碼金鑰建立呼叫並傳回憑證 PublicKeyCredential
  • 驗證憑證並儲存在伺服器上。

以下各節將深入說明每個步驟的詳細資訊。

建立憑證建立選項

您必須在伺服器上建立 PublicKeyCredentialCreationOptions 物件。

如要這麼做,請使用 FIDO 伺服器端程式庫。通常會提供公用程式函式,讓您建立這些選項。SimpleWebAuthn 提供範例,例如:generateRegistrationOptions

PublicKeyCredentialCreationOptions 應包含建立密碼金鑰所需的各項資訊:使用者相關資訊、RP 資訊,以及您所建立的憑證屬性的設定。定義完所有項目後,視需要將這些內容傳送至 FIDO 伺服器端程式庫中負責建立 PublicKeyCredentialCreationOptions 物件的函式。

PublicKeyCredentialCreationOptions 的部分欄位可以是常數。其他屬性則應在伺服器上動態定義:

  • rpId:如要在伺服器上填入 RP ID,請使用可提供網頁應用程式主機名稱的伺服器端函式或變數,例如 example.com
  • user.nameuser.displayName如要填入這些欄位,請使用已登入使用者的工作階段資訊。如果使用者正在註冊時建立密碼金鑰,則請使用新的使用者帳戶資訊。user.name 通常是電子郵件地址,而且對於 RP 而言是專屬值。user.displayName 是易記的名稱。請注意,並非所有平台都會使用 displayName
  • user.id:建立帳戶時產生的隨機不重複字串。這個名稱必須永久有效,與可編輯的使用者名稱不同。User ID 可用來識別帳戶,但不得含有任何個人識別資訊 (PII)。系統中可能已有使用者 ID,但您可以視需要建立使用者 ID,專門用於密碼金鑰,避免產生任何個人識別資訊。
  • excludeCredentials:現有憑證 ID 清單,避免與密碼金鑰提供者複製密碼金鑰。如要填入這個欄位,請在資料庫中查詢這位使用者的現有憑證。詳情請參閱「禁止建立新的密碼金鑰 (如果有的話)」。
  • challenge:如要註冊憑證,就必須使用認證 (此為更進階的技巧,用於驗證密碼金鑰提供者的身分及其發出的資料),否則這項驗證問題與此無關。然而,即使您並未使用認證,挑戰仍是必要欄位。在這種情況下,為了簡單起見,您可以將這項挑戰設為單一 0。如需建立安全驗證方式的操作說明,請參閱「伺服器端密碼金鑰驗證」一文。

編碼和解碼

伺服器傳送的 PublicKeyCredentialCreationOptions
伺服器已傳送 PublicKeyCredentialCreationOptionschallengeuser.idexcludeCredentials.credentials 必須在伺服器端編碼至 base64URL,才能讓 PublicKeyCredentialCreationOptions 透過 HTTPS 傳送。

PublicKeyCredentialCreationOptions 包含 ArrayBuffer 的欄位,因此 JSON.stringify() 不支援這些欄位。也就是說,目前如要透過 HTTPS 提供 PublicKeyCredentialCreationOptions,部分欄位必須在伺服器上使用 base64URL 手動編碼,然後在用戶端上解碼。

  • 在伺服器上,編碼和解碼作業通常是由 FIDO 伺服器端程式庫負責。
  • 在用戶端,目前需手動進行編碼和解碼。日後會變得更加簡單:只要使用 JSON 格式,即可將選項轉換為 PublicKeyCredentialCreationOptions方法您可以在 Chrome 中查看導入狀態。

範例程式碼:建立憑證建立選項

我們在範例中使用了 SimpleWebAuthn 程式庫。接下來,我們將把公開金鑰憑證選項的建立流程交給其 generateRegistrationOptions 函式。

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

儲存公開金鑰

伺服器傳送的 PublicKeyCredentialCreationOptions
navigator.credentials.create 會傳回 PublicKeyCredential 物件。

如果 navigator.credentials.create 在用戶端成功解析,表示已成功建立密碼金鑰。系統會傳回 PublicKeyCredential 物件。

PublicKeyCredential 物件包含 AuthenticatorAttestationResponse 物件,代表密碼金鑰提供者對用戶端操作說明建立密碼金鑰的回應。當中包含新憑證資訊,您必須做為 RP,以便日後驗證使用者。如要進一步瞭解 AuthenticatorAttestationResponse,請參閱附錄:AuthenticatorAttestationResponse

PublicKeyCredential 物件傳送至伺服器。收到驗證後,請進行驗證。

請將這個驗證步驟交給 FIDO 伺服器端程式庫。通常會為此提供公用程式函式。SimpleWebAuthn 提供範例,例如:verifyRegistrationResponse。如要瞭解運作原理,請參閱附錄:註冊回應驗證

驗證成功後,請將憑證資訊儲存在資料庫中,方便使用者日後使用與該憑證相關聯的密碼金鑰進行驗證。

使用專屬資料表,查看與密碼金鑰相關聯的公開金鑰憑證。使用者只能設定一組密碼,但可以有多個密碼金鑰,例如透過 Apple iCloud 鑰匙圈同步處理密碼金鑰,以及透過 Google 密碼管理工具同步處理一組密碼金鑰。

以下結構定義範例可以用來儲存憑證資訊:

密碼金鑰的資料庫結構定義

  • 「使用者」資料表:
    • user_id:主要使用者 ID。使用者的隨機唯一永久 ID。將此項目做為「使用者」資料表的主鍵。
    • username。使用者定義的使用者名稱,可能可供編輯。
    • passkey_user_id:密碼金鑰專屬且不含個人識別資訊的使用者 ID,在註冊選項中以 user.id 表示。使用者之後嘗試進行驗證時,驗證器會在 userHandle 中的驗證回應中提供這個 passkey_user_id。建議您不要將 passkey_user_id 設為主鍵。主鍵在系統中廣泛使用,因此往往會變成真實的 PII。
  • 「公開金鑰憑證」資料表:
    • id:憑證 ID。將其做為「公開金鑰憑證」資料表的主鍵。
    • public_key:憑證的公開金鑰。
    • passkey_user_id:將此項目當做外鍵,與「使用者」資料表建立連結。
    • backed_up:如果密碼金鑰已由密碼金鑰提供者同步處理,系統會備份密碼金鑰。如果您日後想為擁有 backed_up 密碼金鑰的使用者捨棄密碼,建議您儲存備份狀態。如要檢查密碼金鑰是否已備份,您可以查看 authenticatorData 中的旗標,或使用通常可用於輕鬆存取這項資訊的 FIDO 伺服器端程式庫功能。儲存備份資格有助於解決潛在的使用者問題。
    • name:(選用) 提供憑證的顯示名稱,可讓使用者提供憑證自訂名稱。
    • transports傳輸陣列。儲存傳輸資訊對於驗證使用者體驗來說相當實用。如果有傳輸機制可用,瀏覽器就能據此行為,並顯示與密碼金鑰提供者用於與用戶端通訊的傳輸相符的 UI (尤其是在 allowCredentials 不是空白的重新驗證用途中)。

您可以儲存其他資訊,以便提供使用者體驗,包括密碼金鑰提供者、憑證建立時間和上次使用時間等項目。詳情請參閱「密碼金鑰使用者介面設計」。

範例程式碼:儲存憑證

我們在範例中使用了 SimpleWebAuthn 程式庫。 以下,我們將註冊回應的驗證工作交給其 verifyRegistrationResponse 函式。

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

附錄:AuthenticatorAttestationResponse

AuthenticatorAttestationResponse 包含兩個重要物件:

  • response.clientDataJSON用戶端資料的 JSON 版本,其中的資料是瀏覽器所顯示的資料。其中包含 RP 來源、驗證問題和 androidPackageName (如果用戶端是 Android 應用程式)。您可以將 clientDataJSON 設為 RP,藉此取得瀏覽器在 create 要求執行時看到的資訊。
  • response.attestationObject 包含兩項資訊:
    • attestationStatement,除非您使用認證,否則這與無關。
    • authenticatorData 是密碼金鑰提供者所顯示的資料。作為 RP,讀取 authenticatorData 可讓您存取密碼金鑰供應者看到的資料,並在執行 create 要求時傳回。

authenticatorData 包含與新建密碼金鑰相關聯的公開金鑰憑證重要資訊:

  • 公開金鑰憑證本身,以及專屬憑證 ID。
  • 與憑證相關聯的 RP ID。
  • 此標記說明密碼金鑰建立時間的使用者狀態:使用者是否確實存在,以及使用者是否已成功驗證 (請參閱 userVerification)。
  • AAGUID,用於識別密碼金鑰提供者。如果使用者已向多個密碼金鑰供應商註冊服務密碼金鑰,更適合採用密碼金鑰提供者顯示這項做法。

雖然 authenticatorData 以巢狀結構在 attestationObject 內,但無論您是否使用認證,都必須提供密碼金鑰實作中包含的資訊。authenticatorData 經過編碼,且其中包含以二進位格式編碼的欄位。伺服器端程式庫通常會處理剖析和解碼作業。如果您不是使用伺服器端程式庫,請考慮利用 getAuthenticatorData() 用戶端節省一些在伺服器端剖析及解碼作業。

附錄:註冊回應驗證

基本上,驗證註冊回應包含下列檢查:

  • 確認 RP ID 與你的網站相符。
  • 確認要求的來源是網站 (主網站網址、Android 應用程式) 的來源。
  • 如果您需要進行使用者驗證,請確認使用者驗證旗標 authenticatorData.uvtrue。檢查使用者狀態標記 authenticatorData.up 是否為 true,因為密碼金鑰一律必須向使用者顯示。
  • 確認客戶能夠提供您提供的驗證資訊。如果您不使用認證,這項檢查就不重要。不過,實作這項檢查是最佳做法,因為如果日後決定使用認證,這項檢查可確保程式碼已準備就緒。
  • 確認尚未為任何使用者註冊憑證 ID。
  • 確認密碼金鑰供應商用來建立憑證的演算法是你列出的演算法 (在 publicKeyCredentialCreationOptions.pubKeyCredParams 的每個 alg 欄位中,通常在伺服器端定義,並不會向你顯示)。這可確保使用者只能透過您選擇允許的演算法進行註冊。

詳情請參閱 SimpleWebAuthn 的 verifyRegistrationResponse 原始碼,或查閱規格的完整清單。

下一步

伺服器端密碼金鑰驗證