可发现凭据深入探究

虽然通行密钥等 FIDO 凭据旨在替换密码,但大多数凭据也可以让用户免于输入用户名。这样一来,用户即可通过从拥有当前网站的通行密钥列表中选择一个账号来进行身份验证。

早期版本的安全密钥采用两步身份验证方法,需要潜在凭据的 ID,因此需要输入用户名。安全密钥可以在不知道其 ID 的情况下找到的凭据,称为可检测到的凭据。目前创建的大多数 FIDO 凭据都是可检测到的凭据,尤其是存储在密码管理工具或新型安全密钥中的通行密钥。

为了确保您的凭据可被发现,请在创建通行密钥时指定 residentKeyrequireResidentKey

信赖方 (RP) 可以在通行密钥身份验证期间省略 allowCredentials,从而使用可发现的凭据。在这些情况下,浏览器或系统会向用户显示可用通行密钥的列表,这些通行密钥由创建时设置的 user.name 属性标识。如果用户选择其中一个,生成的签名中将包含 user.id 值。然后,服务器可以使用该 ID 或返回的凭据 ID 来查找帐号,而不是输入用户名。

如前所述,帐号选择器界面从不显示不可检测到的凭据。

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;
  
  // ...
}

显示通行密钥表单自动填充

如果大多数用户使用通行密钥并在本地设备上提供通行密钥,上述模态账号选择器就能正常工作。对于没有本地通行密钥的用户,模态对话框仍会显示,并让用户显示通过其他设备使用的通行密钥。在为用户改用通行密钥时,建议您避免在尚未设置通行密钥的用户界面上显示该界面。

相反,在选择通行密钥时,系统可能会在提示中将通行密钥与已保存的用户名和密码一起折叠成针对传统登录表单中字段的自动填充提示。这样一来,拥有通行密钥的用户可通过选择通行密钥来“填充”登录表单,而保存了用户名/密码对的用户则可以选择这些通行密钥,而既没有通行密钥的用户也仍然可以输入自己的用户名和密码。

当 RP 中混合使用密码和通行密钥进行迁移时,这种用户体验非常理想。

为了实现这种用户体验,除了向 allowCredentials 属性传递空数组或省略该参数之外,还应在 navigator.credentials.get() 中指定 mediation: 'conditional',并为 HTML username 输入字段添加 autocomplete="username webauthn" 注解或为 password 输入字段添加 autocomplete="password webauthn" 注解。

调用 navigator.credentials.get() 不会导致显示任何界面,但如果用户聚焦到带注解的输入字段,则所有可用的通行密钥都会包含在自动填充选项中。如果用户选择一个,他们将进行常规的设备解锁验证,只有这样,.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" ...>

如需了解如何打造这种用户体验,请参阅通过表单自动填充功能使用通行密钥登录以及在 Web 应用中使用表单自动填充功能实现通行密钥 Codelab。

重新验证

在某些情况下(例如使用通行密钥重新进行身份验证时),用户的标识符是已知的。在本例中,我们希望使用通行密钥,而浏览器或操作系统应不显示任何形式的帐号选择器。这可以通过在 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:关于持有此凭据的设备支持的传输的提示,浏览器会使用该提示来优化要求用户展示外部设备的界面。此列表(如果提供)应包含每个凭据注册期间调用 getTransports() 的结果。

摘要

可检测到的凭据让用户无需输入用户名,从而为用户提供了更加人性化的通行密钥登录体验。通过结合使用 residentKeyrequireResidentKeyallowCredentials,RP 可以实现以下登录体验:

  • 显示模态账号选择器。
  • 显示通行密钥表单自动填充。
  • 重新验证。

明智地使用可检测到的凭据。这样一来,您可以设计复杂的通行密钥登录体验,让用户感觉到流畅自然,而且更有可能与之互动。