Web 身份验证 API

安全上下文: 此功能仅在 安全上下文 (HTTPS) 中可用,在一些或所有 支持的浏览器 中可用。

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

注意: 密码钥匙 是 Web 身份验证的一个重要用例;请参阅 创建密码钥匙以进行无密码登录通过表单自动填充使用密码钥匙登录,以获取实施细节。另请参阅 Google Identity > 使用密码钥匙进行无密码登录

WebAuthn 概念和用法

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

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

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

  • 当使用 publicKey 选项调用 navigator.credentials.create() 时,用户代理通过身份验证器创建新的凭据 - 用于注册新帐户或将新的非对称密钥对与现有帐户关联。
    • 注册新帐户时,这些凭据存储在服务器(也称为服务或 依赖方)上,并可随后用于登录用户。
    • 非对称密钥对存储在身份验证器中,然后可用于使用依赖方对用户进行身份验证,例如在 MFA 期间。身份验证器可以嵌入到用户代理、操作系统(例如 Windows Hello)中,也可以是物理令牌(例如 USB 或蓝牙安全密钥)。
  • 当使用 publicKey 选项调用 navigator.credentials.get() 时,用户代理使用一组现有凭据对依赖方进行身份验证(作为主要登录或提供 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 履行时,它以 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. 服务器验证成功后,身份验证流程被认为成功。

控制对 API 的访问

可以使用 权限策略 控制 WebAuthn 的可用性,特别是指定两个指令

这两个指令的默认允许列表值为"self",这意味着默认情况下,这些方法可以在顶级文档上下文中使用。此外,get()可以在从与最顶层文档相同的来源加载的嵌套浏览上下文中使用。如果由publickey-credentials-getpublickey-credentials-create Permission-Policy 指令分别允许,get()create() 可以在从与最顶层文档不同的来源加载的嵌套浏览上下文中使用(例如,在跨源<iframes>中)。对于跨源create()调用,如果权限是由allow= 在 iframe 上授予的,则该框架还必须具有瞬时激活

注意: 如果策略禁止使用这些方法,它们返回的promise 将会以NotAllowedError DOMException 拒绝。

基本访问控制

如果您希望仅允许访问特定子域,可以像这样提供它

http
Permissions-Policy: publickey-credentials-get=("https://subdomain.example.com")
Permissions-Policy: publickey-credentials-create=("https://subdomain.example.com")

允许在<iframe>中嵌入createget() 调用

如果您希望在<iframe>中使用get()create() 进行身份验证,需要遵循几个步骤

  1. 嵌入依赖方网站的网站必须通过allow 属性提供权限
    • 如果使用get()
      html
      <iframe
        src="https://auth.provider.com"
        allow="publickey-credentials-get *">
      </iframe>
      
    • 如果使用create()
      html
      <iframe
        src="https://auth.provider.com"
        allow="publickey-credentials-create 'self' https://a.auth.provider.com https://b.auth.provider.com">
      </iframe>
      
      如果跨源调用create()<iframe> 还必须具有瞬时激活
  2. 依赖方网站必须通过Permissions-Policy 标头为上述访问提供权限
    http
    Permissions-Policy: publickey-credentials-get=*
    Permissions-Policy: publickey-credentials-create=*
    
    或者仅允许特定 URL 在<iframe>中嵌入依赖方网站
    http
    Permissions-Policy: publickey-credentials-get=("https://subdomain.example.com")
    Permissions-Policy: publickey-credentials-create=("https://*.auth.provider.com")
    

接口

AuthenticatorAssertionResponse

向服务提供证明,身份验证器拥有必要的密钥对,可以成功处理由CredentialsContainer.get() 调用启动的身份验证请求。在通过get() Promise 完成时获得的PublicKeyCredential 实例的response 属性中可用。

AuthenticatorAttestationResponse

WebAuthn 凭据注册的结果(即CredentialsContainer.create() 调用)。它包含服务器执行 WebAuthn 断言所需的信息,例如凭据 ID 和公钥。在通过create() Promise 完成时获得的PublicKeyCredential 实例的response 属性中可用。

AuthenticatorResponse

AuthenticatorAttestationResponseAuthenticatorAssertionResponse 的基本接口。

PublicKeyCredential

提供有关公钥/私钥对的信息,该密钥对是使用不可欺骗且数据泄露抵抗的非对称密钥对而不是密码登录服务的凭据。在通过create()get() 调用完成的Promise 返回时获得。

对其他接口的扩展

CredentialsContainer.create()publicKey 选项

使用publicKey 选项调用create() 将通过身份验证器启动新的非对称密钥凭据的创建,如上所述。

CredentialsContainer.get()publicKey 选项

使用publicKey 选项调用get() 将指示用户代理使用一组现有凭据对依赖方进行身份验证。

示例

演示网站

使用示例

注意: 出于安全原因,如果浏览器窗口在调用挂起时失去焦点,Web Authentication API 调用(create()get())将被取消。

js
// sample arguments for registration
const createCredentialDefaultArgs = {
  publicKey: {
    // Relying Party (a.k.a. - Service):
    rp: {
      name: "Acme",
    },
    // User:
    user: {
      id: new Uint8Array(16),
      name: "[email protected]",
      displayName: "Carina P. Anand",
    },
    pubKeyCredParams: [
      {
        type: "public-key",
        alg: -7,
      },
    ],
    attestation: "direct",
    timeout: 60000,
    challenge: new Uint8Array([
      // must be a cryptographically random number sent from a server
      0x8c, 0x0a, 0x26, 0xff, 0x22, 0x91, 0xc1, 0xe9, 0xb9, 0x4e, 0x2e, 0x17,
      0x1a, 0x98, 0x6a, 0x73, 0x71, 0x9d, 0x43, 0x48, 0xd5, 0xa7, 0x6a, 0x15,
      0x7e, 0x38, 0x94, 0x52, 0x77, 0x97, 0x0f, 0xef,
    ]).buffer,
  },
};

// sample arguments for login
const getCredentialDefaultArgs = {
  publicKey: {
    timeout: 60000,
    // allowCredentials: [newCredential] // see below
    challenge: new Uint8Array([
      // must be a cryptographically random number sent from a server
      0x79, 0x50, 0x68, 0x71, 0xda, 0xee, 0xee, 0xb9, 0x94, 0xc3, 0xc2, 0x15,
      0x67, 0x65, 0x26, 0x22, 0xe3, 0xf3, 0xab, 0x3b, 0x78, 0x2e, 0xd5, 0x6f,
      0x81, 0x26, 0xe2, 0xa6, 0x01, 0x7d, 0x74, 0x50,
    ]).buffer,
  },
};

// register / create a new credential
navigator.credentials
  .create(createCredentialDefaultArgs)
  .then((cred) => {
    console.log("NEW CREDENTIAL", cred);
    // normally the credential IDs available for an account would come from a server
    // but we can just copy them from above…
    const idList = [
      {
        id: cred.rawId,
        transports: ["usb", "nfc", "ble"],
        type: "public-key",
      },
    ];
    getCredentialDefaultArgs.publicKey.allowCredentials = idList;
    return navigator.credentials.get(getCredentialDefaultArgs);
  })
  .then((assertion) => {
    console.log("ASSERTION", assertion);
  })
  .catch((err) => {
    console.log("ERROR", err);
  });

规范

规范
Web Authentication: An API for accessing Public Key Credentials - Level 3
# iface-pkcredential

浏览器兼容性

BCD 表格仅在浏览器中加载