Web Authentication API
Baseline 广泛可用 *
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 字节大小的随机信息缓冲区。
创建密钥对和注册用户
为了说明凭据创建过程的工作原理,让我们描述用户希望向信赖方注册凭据时发生的典型流程:
-
信赖方服务器使用适当的安全机制(例如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()调用返回的Promisefulfilled 时提供,形式为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。
-
一旦服务器验证通过,身份验证流程就被认为是成功的。
可发现凭据和有条件中介
可发现凭据是从身份验证器中检索(由浏览器发现)的,当用户登录信赖方 Web 应用程序时,将其作为登录选项提供。相比之下,不可发现凭据由信赖方服务器提供,供浏览器作为登录选项提供。
可发现凭据 ID 和相关元数据,例如用户名和显示名称,存储在客户端身份验证器中,例如浏览器密码管理器、身份验证应用程序或硬件解决方案(如 YubiKey)。身份验证器中提供此信息意味着用户可以方便地登录,而无需提供凭据,并且信赖方在断言时无需提供credentialId(尽管如果需要也可以这样做;如果凭据由 RP 断言,则遵循不可发现的工作流)。
可发现凭据通过带有指定residentKey的create()调用创建。新凭据的 credentialId、用户元数据和公钥由身份验证器存储,如上所述,但也会返回给 Web 应用程序并存储在 RP 服务器上。
为了进行身份验证,RP 服务器调用get(),并指定有条件中介,即mediation设置为conditional,一个空的allowCredentials列表(意味着只能显示可发现凭据)和一个挑战。
有条件中介会导致在身份验证器中发现的可发现凭据以非模态 UI 的形式呈现给用户,并指示请求凭据的来源,而不是模态对话框。实际上,这意味着在登录表单中自动填充可用凭据。存储在可发现凭据中的元数据可以显示,以帮助用户在登录时选择凭据。要在登录表单中显示可发现凭据,您还需要在表单字段中包含autocomplete="webauthn"。
重申一下,信赖方不会告诉身份验证器要向用户提供哪些凭据——相反,身份验证器会提供其可用的列表。一旦用户选择了凭据,身份验证器会使用它通过关联的私钥对挑战进行签名,然后浏览器将签名的挑战及其 credentialId 返回给 RP 服务器。
RP 服务器上的后续身份验证过程与不可发现凭据的相同。
注意: 您可以通过调用PublicKeyCredential.isConditionalMediationAvailable()方法来检查特定用户代理上是否提供条件中介。
通行密钥是可发现凭据的一个重要用例;有关实施细节,请参阅为无密码登录创建通行密钥和通过表单自动填充使用通行密钥登录。有关可发现凭据的更多一般信息,请参阅可发现凭据深入探究。
当使用条件中介进行身份验证时,无论其实际值如何,都会将阻止静默访问标志(请参阅CredentialsContainer.preventSilentAccess())视为 true:如果发现适用的凭据,条件行为始终涉及某种形式的用户中介。
注意: 如果没有发现凭据,则不会显示非模态对话框,并且用户代理可能会以取决于凭据类型的方式提示用户采取行动(例如,插入包含凭据的设备)。
可发现凭据同步方法
用户身份验证器中存储的关于可发现凭据的信息可能会与信赖方服务器不同步。当用户在 RP Web 应用程序上删除凭据或修改其用户/显示名称而未更新身份验证器时,可能会发生这种情况。
API 提供了方法,允许信赖方服务器向身份验证器发送更改信号,以便它可以更新其存储的凭据:
PublicKeyCredential.signalAllAcceptedCredentials():向身份验证器发送信号,告知 RP 服务器仍为特定用户持有的所有有效凭据 ID。PublicKeyCredential.signalCurrentUserDetails():向身份验证器发送信号,告知特定用户已在 RP 服务器上更新了其用户名和/或显示名称。PublicKeyCredential.signalUnknownCredential():向身份验证器发送信号,告知 RP 服务器未识别凭据 ID。
signalUnknownCredential() 和 signalAllAcceptedCredentials() 的目的似乎相似,那么每种情况应该使用哪一个呢?
signalAllAcceptedCredentials()应在每次成功登录后调用,以及当用户已登录且您希望更新其凭据状态时调用。它只能在用户经过身份验证时调用,因为它共享给定用户的所有credentialId列表。如果用户未经身份验证,这将导致隐私泄露。signalUnknownCredential()应在登录失败后调用,以向身份验证器发出信号,表明所选凭据的credentialId无法验证,应将其删除。该方法可以在用户未经身份验证时安全调用,因为它只将单个credentialId(客户端刚刚尝试进行身份验证的那个)传递给身份验证器,并且不传递任何用户信息。
根据客户端功能定制工作流
注册和登录工作流可以根据 WebAuthn 客户端(浏览器)的功能进行定制。PublicKeyCredential.getClientCapabilities() 静态方法可用于查询这些功能;它返回一个对象,其中每个键引用一个 WebAuthn 功能或扩展,每个值都是一个布尔值,指示对该功能的支持。
这可以用于例如检查:
- 客户端对各种身份验证器的支持,例如通行密钥或生物识别用户验证。
- 客户端是否支持使信赖方和身份验证器凭据保持同步的方法。
- 客户端是否允许在具有相同来源的不同网站上使用单个通行密钥。
下面的代码演示了如何使用 getClientCapabilities() 来检查客户端是否支持提供生物识别用户验证的身份验证器。请注意,实际执行的操作取决于您的站点。对于要求生物识别身份验证的站点,您可能需要将登录 UI 替换为一条消息,指示需要生物识别身份验证,并且用户应尝试使用其他浏览器或设备。
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 的可用性可以通过权限策略进行控制,具体指定两个指令:
publickey-credentials-create:控制带publicKey选项的navigator.credentials.create()的可用性。publickey-credentials-get:控制带publicKey选项的navigator.credentials.get()的可用性。
这两个指令的默认允许列表值都是 "self",这意味着默认情况下这些方法可以在顶级文档上下文中使用。此外,get() 可以在从与最顶层文档相同的来源加载的嵌套浏览上下文中使用。如果publickey-credentials-get 和publickey-credentials-create Permissions-Policy 指令允许,get() 和 create() 可以在从与最顶层文档不同的来源加载的嵌套浏览上下文中使用(即在跨域 中)。对于跨域 create() 调用,如果权限是由iframe 上的 allow= 授予的,则该框架还必须具有瞬时激活。
注意: 如果策略禁止使用这些方法,它们返回的promise将因 NotAllowedError DOMException 而拒绝。
基本访问控制
如果您希望仅允许访问特定子域,您可以这样提供:
Permissions-Policy: publickey-credentials-get=("https://subdomain.example.com")
Permissions-Policy: publickey-credentials-create=("https://subdomain.example.com")
允许在 中嵌入 create 和 get() 调用
如果您希望在 中使用 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()是跨域调用,还必须具有瞬时激活。
-
-
信赖方站点必须通过
Permissions-Policy头部提供上述访问权限:httpPermissions-Policy: publickey-credentials-get=* Permissions-Policy: publickey-credentials-create=*或者只允许特定 URL 在
中嵌入信赖方站点:httpPermissions-Policy: publickey-credentials-get=("https://subdomain.example.com") Permissions-Policy: publickey-credentials-create=("https://*.auth.provider.com")
接口
AuthenticatorAssertionResponse-
向服务提供证据,证明身份验证器拥有必要的密钥对,能够成功处理由
CredentialsContainer.get()调用发起的身份验证请求。在get()Promisefulfilled 时获得的PublicKeyCredential实例的response属性中可用。 AuthenticatorAttestationResponse-
WebAuthn 凭证注册的结果(即
CredentialsContainer.create()调用)。它包含服务器执行 WebAuthn 断言所需的凭证信息,例如凭证 ID 和公钥。在create()Promisefulfilled 时获得的PublicKeyCredential实例的response属性中可用。 AuthenticatorResponse-
是
AuthenticatorAttestationResponse和AuthenticatorAssertionResponse的基本接口。 PublicKeyCredential-
提供有关公钥/私钥对的信息,该密钥对是一种凭证,用于使用防网络钓鱼和防数据泄露的非对称密钥对(而非密码)登录服务。当通过
create()或get()调用返回的Promisefulfilled 时获得。
其他接口的扩展
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: "carina.p.anand@example.com",
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:访问公钥凭证的 API - 第 3 级 # iface-pkcredential |
浏览器兼容性
加载中…