可发现凭据深入探究

虽然 FIDO 凭据(例如通行密钥)旨在替换密码,但大部分凭据都可以让用户免于输入用户名。这样,用户就可以从其为当前网站拥有的通行密钥列表中选择一个账号来进行身份验证。

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

为确保您的凭据创建为通行密钥(可检测到的凭据),请在创建凭据时指定 residentKeyrequireResidentKey

依赖方 (RP) 可以在通行密钥身份验证期间省略 allowCredentials,以使用可检测到的凭据。在这些情况下,浏览器或系统会向用户显示可用通行密钥的列表,该列表由创建时设置的 user.name 属性标识。如果用户选择了某个,则生成的签名中将包含 user.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',并使用 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="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 可以实现以下登录体验:

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

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