Web Authentication API

Baseline 广泛可用 *

此功能已成熟,并可在多种设备和浏览器版本上运行。自 2021 年 9 月起,所有浏览器均已支持此功能。

* 此特性的某些部分可能存在不同级别的支持。

安全上下文: 此功能仅在安全上下文(HTTPS)中可用,且支持此功能的浏览器数量有限。

Web 身份验证 API (WebAuthn) 是凭据管理 API 的扩展,它通过公钥加密实现强大的身份验证,支持无密码身份验证和无需短信的安全多因素身份验证 (MFA)。

WebAuthn 概念和用法

WebAuthn 使用非对称(公钥)加密而不是密码或短信进行网站注册、身份验证和多因素身份验证。这带来了一些好处:

  • 防范网络钓鱼: 攻击者创建虚假登录网站也无法以用户身份登录,因为签名会随网站的而改变。
  • 减少数据泄露的影响: 开发人员无需对公钥进行哈希处理,如果攻击者获得了用于验证身份的公钥,他们也无法进行身份验证,因为他们需要私钥。
  • 不受密码攻击的影响: 有些用户可能会重复使用密码,攻击者可能会获取用户在其他网站上的密码(例如,通过数据泄露)。此外,文本密码比数字签名更容易被暴力破解。

许多网站已经有允许用户注册新账户或登录现有账户的页面,而 WebAuthn 作为系统身份验证部分的替代或增强。它扩展了凭据管理 API,抽象了用户代理和身份验证器之间的通信,并提供了以下新功能:

  • navigator.credentials.create()publicKey 选项一起使用时,用户代理通过身份验证器创建新凭据——用于注册新账户或将新的非对称密钥对与现有账户关联。
    • 注册新账户时,这些凭据存储在服务器上(也称为服务或信赖方),后续可用于用户登录。
    • 非对称密钥对存储在身份验证器中,然后可用于在 MFA 期间对信赖方进行用户身份验证。身份验证器可以嵌入到用户代理中,嵌入到操作系统中(例如 Windows Hello),也可以是物理令牌(例如 USB 或蓝牙安全密钥)。
  • navigator.credentials.get()publicKey 选项一起使用时,用户代理使用现有凭据集对信赖方进行身份验证(作为主要登录或在 MFA 期间提供额外的因素,如上所述)。

在最基本的形式中,create()get() 都从服务器接收一个称为“挑战”的非常大的随机数,并将由私钥签名的挑战返回给服务器。这向服务器证明用户拥有身份验证所需的私钥,而不会通过网络泄露任何秘密。

注意:“挑战”必须是一个至少 16 字节大小的随机信息缓冲区。

创建密钥对和注册用户

为了说明凭据创建过程的工作原理,让我们描述用户希望向信赖方注册凭据时发生的典型流程:

  1. 信赖方服务器使用适当的安全机制(例如FetchXMLHttpRequest)将用户和信赖方信息以及“挑战”发送到处理注册的 Web 应用程序。

    注意: 信赖方服务器和 Web 应用程序之间共享信息的格式由应用程序决定。推荐的方法是交换JSON 类型表示对象,用于凭据和凭据选项。PublicKeyCredential 中已创建了方便的方法,用于将 JSON 表示转换为身份验证 API 所需的形式:parseCreationOptionsFromJSON()parseRequestOptionsFromJSON()PublicKeyCredential.toJSON()

  2. Web 应用程序通过调用navigator.credentials.create(),代表信赖方启动通过身份验证器生成新凭据的过程。此调用会传入一个 publicKey 选项,指定设备功能,例如设备是否提供自己的用户身份验证(例如,使用生物识别)。

    典型的 create() 调用可能如下所示:

    js
    let credential = await navigator.credentials.create({
      publicKey: {
        challenge: new Uint8Array([117, 61, 252, 231, 191, 241 /* … */]),
        rp: { id: "acme.com", name: "ACME Corporation" },
        user: {
          id: new Uint8Array([79, 252, 83, 72, 214, 7, 89, 26]),
          name: "jamiedoe",
          displayName: "Jamie Doe",
        },
        pubKeyCredParams: [{ type: "public-key", alg: -7 }],
      },
    });
    

    create() 调用的参数以及经过签名的 SHA-256 哈希值会传递给身份验证器,以确保其未被篡改。

  3. 身份验证器获得用户同意后,会生成一个密钥对,并将公钥和可选的签名证明返回给 Web 应用程序。这会在 create() 调用返回的Promise fulfilled 时提供,形式为PublicKeyCredential 对象实例(PublicKeyCredential.response 属性包含证明信息)。

  4. Web 应用程序再次使用适当的机制将PublicKeyCredential 转发给信赖方服务器。

  5. 信赖方服务器存储公钥,并与用户身份绑定,以便记住凭据以供将来的身份验证。在此过程中,它会执行一系列检查,以确保注册完成且未被篡改。这些检查包括:

    1. 验证挑战是否与发送的挑战相同。
    2. 确保来源是预期的来源。
    3. 验证签名和证明是否使用了首次生成密钥对所用的特定身份验证器模型的正确证书链。

警告: 证明为信赖方提供了一种确定身份验证器来源的方法。信赖方不应尝试维护身份验证器白名单。

用户身份验证

用户使用 WebAuthn 注册后,即可使用该服务进行身份验证(登录)。身份验证流程类似于注册流程,主要区别在于身份验证:

  1. 不需要用户或信赖方信息
  2. 使用先前为服务生成的密钥对创建断言,而不是身份验证器的密钥对。

典型的身份验证流程如下:

  1. 信赖方生成一个“挑战”并使用适当的安全机制将其发送给用户代理,同时发送信赖方和用户凭据列表。它还可以指示在哪里查找凭据,例如,在本地内置身份验证器上,或通过 USB、BLE 等外部身份验证器上。

  2. 浏览器请求身份验证器通过调用navigator.credentials.get()来签署挑战,该调用将凭据传递给 publicKey 选项。

    典型的 get() 调用可能如下所示:

    js
    let credential = await navigator.credentials.get({
      publicKey: {
        challenge: new Uint8Array([139, 66, 181, 87, 7, 203 /* … */]),
        rpId: "acme.com",
        allowCredentials: [
          {
            type: "public-key",
            id: new Uint8Array([64, 66, 25, 78, 168, 226, 174 /* … */]),
          },
        ],
        userVerification: "required",
      },
    });
    

    get() 调用的参数会传递给身份验证器进行身份验证。

  3. 如果身份验证器包含给定的凭据之一并且能够成功签署挑战,则在收到用户同意后,它会向 Web 应用程序返回一个签名的断言。这在 get() 调用返回的Promise 完成时以PublicKeyCredential 对象实例的形式提供(PublicKeyCredential.response 属性包含断言信息)。

  4. Web 应用程序将签名的断言转发给信赖方服务器,供信赖方验证。验证检查包括:

    1. 使用注册请求期间存储的公钥验证身份验证器的签名。
    2. 确保身份验证器签名的挑战与服务器生成的挑战匹配。
    3. 检查信赖方 ID 是否是该服务所预期的 ID。
  5. 一旦服务器验证通过,身份验证流程就被认为是成功的。

可发现凭据和有条件中介

可发现凭据是从身份验证器中检索(由浏览器发现)的,当用户登录信赖方 Web 应用程序时,将其作为登录选项提供。相比之下,不可发现凭据由信赖方服务器提供,供浏览器作为登录选项提供。

可发现凭据 ID 和相关元数据,例如用户名显示名称,存储在客户端身份验证器中,例如浏览器密码管理器、身份验证应用程序或硬件解决方案(如 YubiKey)。身份验证器中提供此信息意味着用户可以方便地登录,而无需提供凭据,并且信赖方在断言时无需提供credentialId(尽管如果需要也可以这样做;如果凭据由 RP 断言,则遵循不可发现的工作流)。

可发现凭据通过带有指定residentKeycreate()调用创建。新凭据的 credentialId、用户元数据和公钥由身份验证器存储,如上所述,但也会返回给 Web 应用程序并存储在 RP 服务器上。

为了进行身份验证,RP 服务器调用get(),并指定有条件中介,即mediation设置为conditional,一个空的allowCredentials列表(意味着只能显示可发现凭据)和一个挑战。

有条件中介会导致在身份验证器中发现的可发现凭据以非模态 UI 的形式呈现给用户,并指示请求凭据的来源,而不是模态对话框。实际上,这意味着在登录表单中自动填充可用凭据。存储在可发现凭据中的元数据可以显示,以帮助用户在登录时选择凭据。要在登录表单中显示可发现凭据,您还需要在表单字段中包含autocomplete="webauthn"

重申一下,信赖方不会告诉身份验证器要向用户提供哪些凭据——相反,身份验证器会提供其可用的列表。一旦用户选择了凭据,身份验证器会使用它通过关联的私钥对挑战进行签名,然后浏览器将签名的挑战及其 credentialId 返回给 RP 服务器。

RP 服务器上的后续身份验证过程与不可发现凭据的相同。

注意: 您可以通过调用PublicKeyCredential.isConditionalMediationAvailable()方法来检查特定用户代理上是否提供条件中介。

通行密钥是可发现凭据的一个重要用例;有关实施细节,请参阅为无密码登录创建通行密钥通过表单自动填充使用通行密钥登录。有关可发现凭据的更多一般信息,请参阅可发现凭据深入探究

当使用条件中介进行身份验证时,无论其实际值如何,都会将阻止静默访问标志(请参阅CredentialsContainer.preventSilentAccess())视为 true:如果发现适用的凭据,条件行为始终涉及某种形式的用户中介。

注意: 如果没有发现凭据,则不会显示非模态对话框,并且用户代理可能会以取决于凭据类型的方式提示用户采取行动(例如,插入包含凭据的设备)。

可发现凭据同步方法

用户身份验证器中存储的关于可发现凭据的信息可能会与信赖方服务器不同步。当用户在 RP Web 应用程序上删除凭据或修改其用户/显示名称而未更新身份验证器时,可能会发生这种情况。

API 提供了方法,允许信赖方服务器向身份验证器发送更改信号,以便它可以更新其存储的凭据:

signalUnknownCredential()signalAllAcceptedCredentials() 的目的似乎相似,那么每种情况应该使用哪一个呢?

  • signalAllAcceptedCredentials() 应在每次成功登录后调用,以及当用户已登录且您希望更新其凭据状态时调用。它只能在用户经过身份验证时调用,因为它共享给定用户的所有 credentialId 列表。如果用户未经身份验证,这将导致隐私泄露。
  • signalUnknownCredential() 应在登录失败后调用,以向身份验证器发出信号,表明所选凭据的 credentialId 无法验证,应将其删除。该方法可以在用户未经身份验证时安全调用,因为它只将单个 credentialId(客户端刚刚尝试进行身份验证的那个)传递给身份验证器,并且不传递任何用户信息。

根据客户端功能定制工作流

注册和登录工作流可以根据 WebAuthn 客户端(浏览器)的功能进行定制。PublicKeyCredential.getClientCapabilities() 静态方法可用于查询这些功能;它返回一个对象,其中每个键引用一个 WebAuthn 功能或扩展,每个值都是一个布尔值,指示对该功能的支持。

这可以用于例如检查:

下面的代码演示了如何使用 getClientCapabilities() 来检查客户端是否支持提供生物识别用户验证的身份验证器。请注意,实际执行的操作取决于您的站点。对于要求生物识别身份验证的站点,您可能需要将登录 UI 替换为一条消息,指示需要生物识别身份验证,并且用户应尝试使用其他浏览器或设备。

js
async function checkIsUserVerifyingPlatformAuthenticatorAvailable() {
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  // Check the capability: userVerifyingPlatformAuthenticator
  if (capabilities.userVerifyingPlatformAuthenticator) {
    // Perform actions if biometric support is available
  } else {
    // Perform actions if biometric support is not available.
  }
}

控制 API 访问

WebAuthn 的可用性可以通过权限策略进行控制,具体指定两个指令:

这两个指令的默认允许列表值都是 "self",这意味着默认情况下这些方法可以在顶级文档上下文中使用。此外,get() 可以在从与最顶层文档相同的来源加载的嵌套浏览上下文中使用。如果publickey-credentials-getpublickey-credentials-create Permissions-Policy 指令允许,get()create() 可以在从与最顶层文档不同的来源加载的嵌套浏览上下文中使用(即在跨域