Web 身份验证 API
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 字节的随机信息缓冲区。
创建密钥对并注册用户
为了说明凭据创建过程的工作原理,让我们描述一下用户希望向依赖方注册凭据时发生的典型流程
- 依赖方服务器使用适当的安全机制(例如 Fetch 或 XMLHttpRequest)将用户和依赖方信息以及“挑战”发送到处理注册过程的 Web 应用程序。
注意: 依赖方服务器和 Web 应用程序之间共享信息的格式取决于应用程序。建议的方法是交换 JSON 类型表示 对象,用于凭据和凭据选项。在
PublicKeyCredential
中创建了方便方法,用于将 JSON 表示形式转换为身份验证 API 所需的形式:parseCreationOptionsFromJSON()
、parseRequestOptionsFromJSON()
和PublicKeyCredential.toJSON()
。 - Web 应用程序代表依赖方通过调用
navigator.credentials.create()
来启动通过身份验证器生成新的凭据。此调用传递了一个publicKey
选项,指定了设备功能,例如设备是否提供其自己的用户身份验证(例如,使用生物识别技术)。典型的create()
调用可能如下所示jslet 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 哈希值将传递给身份验证器,以确保其未被篡改。 - 身份验证器在获得用户同意后,将生成密钥对并将公钥和可选的签名证明返回给 Web 应用程序。当
create()
调用返回的Promise
履行时,它以PublicKeyCredential
对象实例的形式提供(PublicKeyCredential.response
属性包含证明信息)。 - Web 应用程序使用适当的机制将
PublicKeyCredential
转发给服务器。 - 服务器存储公钥以及用户身份,以记住凭据以备将来进行身份验证。在此过程中,它会执行一系列检查以确保注册已完成且未被篡改。这些检查包括
- 验证挑战是否与发送的挑战相同。
- 确保源是预期的源。
- 验证签名和证明是否使用用于生成密钥对的特定身份验证器模型的正确证书链。
警告: 证明提供了一种方法,使依赖方能够确定身份验证器的来源。依赖方不应尝试维护身份验证器的允许列表。
对用户进行身份验证
用户使用 WebAuthn 注册后,他们可以使用该服务进行身份验证(即登录)。身份验证流程类似于注册流程,主要区别在于身份验证
- 不需要用户或依赖方信息
- 使用先前为该服务生成的密钥对而不是身份验证器的密钥对创建断言。
典型的身份验证流程如下
- 依赖方生成一个“挑战”,并使用适当的安全机制将其发送给用户代理,同时附带一个依赖方和用户凭据列表。它还可以指示在哪里查找凭据,例如,在本地内置身份验证器上,或在通过 USB、BLE 等的外部身份验证器上。
- 浏览器通过调用
navigator.credentials.get()
来请求身份验证器对挑战进行签名,该调用在publicKey
选项中传递了凭据。典型的get()
调用可能如下所示jslet 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()
调用的参数将传递给身份验证器以处理身份验证。 - 如果身份验证器包含给定的凭据之一并且能够成功签署挑战,则在获得用户同意后,它将签名的断言返回给 Web 应用程序。当
get()
调用返回的Promise
履行时,它以PublicKeyCredential
对象实例的形式提供(PublicKeyCredential.response
属性包含断言信息)。 - Web 应用程序将签名的断言转发给依赖方服务器,以供依赖方进行验证。验证检查包括
- 使用在注册请求期间存储的公钥来验证身份验证器签名的签名。
- 确保身份验证器签名的挑战与服务器生成的挑战匹配。
- 检查依赖方 ID 是否是此服务的预期 ID。
- 服务器验证成功后,身份验证流程被认为成功。
控制对 API 的访问
可以使用 权限策略 控制 WebAuthn 的可用性,特别是指定两个指令
publickey-credentials-create
: 控制使用publicKey
选项的navigator.credentials.create()
的可用性。publickey-credentials-get
: 控制使用publicKey
选项的navigator.credentials.get()
的可用性。
这两个指令的默认允许列表值为"self"
,这意味着默认情况下,这些方法可以在顶级文档上下文中使用。此外,get()
可以在从与最顶层文档相同的来源加载的嵌套浏览上下文中使用。如果由publickey-credentials-get
和publickey-credentials-create
Permission-Policy
指令分别允许,get()
和 create()
可以在从与最顶层文档不同的来源加载的嵌套浏览上下文中使用(例如,在跨源<iframes>
中)。对于跨源create()
调用,如果权限是由allow=
在 iframe 上授予的,则该框架还必须具有瞬时激活。
注意: 如果策略禁止使用这些方法,它们返回的promise 将会以NotAllowedError
DOMException
拒绝。
基本访问控制
如果您希望仅允许访问特定子域,可以像这样提供它
Permissions-Policy: publickey-credentials-get=("https://subdomain.example.com")
Permissions-Policy: publickey-credentials-create=("https://subdomain.example.com")
允许在<iframe>
中嵌入create
和 get()
调用
如果您希望在<iframe>
中使用get()
或 create()
进行身份验证,需要遵循几个步骤
- 嵌入依赖方网站的网站必须通过
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>
还必须具有瞬时激活。
- 如果使用
- 依赖方网站必须通过
Permissions-Policy
标头为上述访问提供权限或者仅允许特定 URL 在httpPermissions-Policy: publickey-credentials-get=* Permissions-Policy: publickey-credentials-create=*
<iframe>
中嵌入依赖方网站httpPermissions-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
-
AuthenticatorAttestationResponse
和AuthenticatorAssertionResponse
的基本接口。 PublicKeyCredential
-
提供有关公钥/私钥对的信息,该密钥对是使用不可欺骗且数据泄露抵抗的非对称密钥对而不是密码登录服务的凭据。在通过
create()
或get()
调用完成的Promise
返回时获得。
对其他接口的扩展
CredentialsContainer.create()
,publicKey
选项-
使用
publicKey
选项调用create()
将通过身份验证器启动新的非对称密钥凭据的创建,如上所述。 CredentialsContainer.get()
,publicKey
选项-
使用
publicKey
选项调用get()
将指示用户代理使用一组现有凭据对依赖方进行身份验证。
示例
演示网站
- Mozilla 演示 网站及其源代码。
- Google 演示 网站及其源代码。
- WebAuthn.io 演示 网站及其源代码。
- github.com/webauthn-open-source 及其客户端源代码 和服务器源代码
使用示例
// 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 表格仅在浏览器中加载