服务器端通行密钥注册

概览

下面简要介绍了注册通行密钥所涉及的关键步骤:

通行密钥注册流程

  • 定义通行密钥创建选项。将它们发送给客户端,以便将它们传递给通行密钥创建调用:WebAuthn API 会调用 navigator.credentials.create(在网页上)和 credentialManager.createCredential(在 Android 上)。在用户确认创建通行密钥后,系统会解析通行密钥创建调用,并返回凭据 PublicKeyCredential
  • 验证凭据并将其存储在服务器上。

以下部分详细介绍了每个步骤。

<ph type="x-smartling-placeholder">

创建凭据创建选项

您需要在服务器上执行的第一步是创建 PublicKeyCredentialCreationOptions 对象。

为此,依赖于 FIDO 服务器端库。它通常会提供一个实用函数,可为您创建这些选项。例如,SimpleWebAuthn 会提供 generateRegistrationOptions

PublicKeyCredentialCreationOptions 应包含创建通行密钥所需的一切:用户相关信息、RP 以及您正在创建的凭据的属性配置。定义好上述所有设置后,根据需要将它们传递给 FIDO 服务器端库中负责创建 PublicKeyCredentialCreationOptions 对象的函数。

部分PublicKeyCredentialCreationOptions'字段可以是常量。其他的 ID 应在服务器上动态定义:

  • rpId:如需在服务器上填充 RP ID,请使用可为您提供 Web 应用主机名的服务器端函数或变量,例如 example.com
  • user.nameuser.displayName:如需填充这些字段,请使用已登录用户的会话信息(如果该用户在注册时创建通行密钥,则使用新用户账号信息)。user.name 通常是电子邮件地址,对于 RP 而言是唯一的。user.displayName 是一个简单易懂的名称。请注意,并非所有平台都会使用 displayName
  • user.id:创建账号时生成的随机唯一字符串。不同于可修改的用户名,它应该是永久性的。用户 ID 用于标识账号,但不得包含任何个人身份信息 (PII)。您的系统中可能已经有一个用户 ID,但如果需要,请专门为通行密钥创建一个用户 ID,使其不包含任何个人身份信息。
  • excludeCredentials:现有凭据的列表用于防止从通行密钥提供程序中复制通行密钥的 ID。要填充此字段,请在数据库中查找此用户的现有凭据。如需了解详情,请参阅禁止创建新的通行密钥(如果已存在)
  • challenge:对于凭据注册,除非您使用证明(一种更高级的技术来验证通行密钥提供程序的身份及其发出的数据),否则验证过程无关紧要。不过,即使您不使用证明,验证仍然是必填字段。在这种情况下,为简单起见,您可以将此质询设置为单个 0。如需了解如何创建安全的身份验证方式,请参阅服务器端通行密钥身份验证

编码和解码

<ph type="x-smartling-placeholder">
</ph> 服务器发送的 PublicKeyCredentialCreationOptions
PublicKeyCredentialCreationOptionschallengeuser.idexcludeCredentials.credentials 必须在服务器端编码为 base64URL,以便通过 HTTPS 传送 PublicKeyCredentialCreationOptions

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 });
  }
});

存储公钥

<ph type="x-smartling-placeholder">
</ph> 服务器发送的 PublicKeyCredentialCreationOptions
navigator.credentials.create 会返回一个 PublicKeyCredential 对象。

如果 navigator.credentials.create 在客户端上成功解析,则表示已成功创建通行密钥。系统会返回 PublicKeyCredential 对象。

PublicKeyCredential 对象包含一个 AuthenticatorAttestationResponse 对象,该对象表示通行密钥提供程序对客户端关于创建通行密钥的指令的响应。它包含您作为 RP 稍后对用户进行身份验证所需的新凭据的相关信息。如需详细了解 AuthenticatorAttestationResponse,请参阅附录:AuthenticatorAttestationResponse

PublicKeyCredential 对象发送到服务器。收到验证码后,请进行验证。

将此验证步骤移交给您的 FIDO 服务器端库。它通常会提供用于此目的的实用函数。例如,SimpleWebAuthn 会提供 verifyRegistrationResponse。请参阅附录:注册响应验证,了解后台发生的情况。

验证成功后,将凭据信息存储在您的数据库中,以便用户稍后可以使用与该凭据关联的通行密钥进行身份验证。

使用专用表来存储与通行密钥关联的公钥凭据。一位用户只能有一个密码,但可以有多个通行密钥 - 例如,一个通过 Apple iCloud 钥匙串同步的通行密钥,另一个是通过 Google 密码管理工具同步的。

以下是可用于存储凭据信息的架构示例:

通行密钥的数据库架构

  • Users 表: <ph type="x-smartling-placeholder">
      </ph>
    • user_id:主要用户 ID。用户的随机、唯一的永久 ID。将此用作 Users 表的主键。
    • username。用户定义的用户名,可能可以修改。
    • passkey_user_id:通行密钥专有的不含个人身份信息的用户 ID,在您的注册选项中以 user.id 表示。当用户稍后尝试进行身份验证时,身份验证器会在 userHandle 的身份验证响应中提供此 passkey_user_id。建议您不要将 passkey_user_id 设置为主键。由于主键被广泛使用,因此往往在系统中往往会变成个人身份信息。
  • 公钥凭据表: <ph type="x-smartling-placeholder">
      </ph>
    • id:凭据 ID。将此用作公钥凭据表的主键。
    • public_key:凭据的公钥。
    • passkey_user_id:将此用作外键,与 Users 表建立关联。
    • backed_up:如果通行密钥由通行密钥提供程序同步,系统会备份通行密钥。如果您想日后考虑为持有 backed_up 个通行密钥的用户删除密码,存储备份状态会非常有用。您可以通过检查 authenticatorData 中的标志或使用 FIDO 服务器端库功能(通常可用于轻松访问这些信息)来检查是否已备份通行密钥。存储备份资格条件有助于解决潜在的用户咨询。
    • name:(可选)凭据的显示名称,可让用户为凭据自定义名称。
    • transports传输的数组。存储传输对于身份验证用户体验非常有用。当有传输可用时,浏览器可以相应地执行操作,并显示与通行密钥提供程序用于与客户端通信的传输相匹配的界面,尤其是在 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 版本,在网络上,则是浏览器看到的数据。如果客户端是 Android 应用,则该字段包含 RP 来源、质询和 androidPackageName。作为 RP,通过读取 clientDataJSON,您可以访问在发出 create 请求时浏览器看到的信息。
  • response.attestationObject 包含两条信息: <ph type="x-smartling-placeholder">
      </ph>
    • 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 源代码,或深入了解规范中的完整验证列表。

后续步骤

服务器端通行密钥身份验证