可发现凭据深入探究

虽然通行密钥等 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 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 属性传递空数组或省略该参数之外,还需在 mediation: 'conditional' 上指定 navigator.credentials.get(),并使用 username 注释 HTML 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 应该能够查询这些 ID,因为用户是已知的。在 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 可以实现以下登录体验:

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

明智地使用可发现的凭据。这样做,您可以设计出复杂而无缝的通行密钥登录体验,用户会觉得顺畅,并且更有可能与您的应用互动。