可发现凭据深入探究

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

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

为确保将凭据创建为通行密钥(可发现凭据),请在创建凭据时指定 residentKey 和 requireResidentKey。

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

账号选择器界面(如前面讨论的界面)绝不会显示不可检测的凭据。

requireResidentKey和residentKey

如需创建通行密钥,请在 navigator.credentials.create() 上指定 authenticatorSelection.residentKey 和 authenticatorSelection.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 可以在 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',并使用 autocomplete="username webauthn" 为 HTML username 输入字段添加注解,或使用 autocomplete="password webauthn" 为 password 输入字段添加注解。

对 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=&qu>ot;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() 的调用结果。

摘要

可检测到的凭证让用户无需输入用户名,从而使通行密钥登录体验更加人性化。通过结合使用 residentKey、requireResidentKey 和 allowCredentials,RP 可以实现以下登录体验:

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

明智地使用可检测到的凭据。这样,您就可以设计出精致的通行密钥登录体验,让用户感到顺畅,并更有可能与之互动。