可发现凭据深入探究

虽然 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 列表来实现。

在这种情况下,如果任何指定的凭据在本地可用,系统会立即提示用户解锁设备。否则,系统会提示用户提供持有有效凭据的其他设备(手机或安全密钥)。

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

如需实现此用户体验,请为登录用户提供凭据 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 可以实现以下登录体验:

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

谨慎使用可检测到的凭据。这样,您就可以设计出完善的通行密钥登录体验,让用户感到无缝且更有可能与之互动。