1. 简介
本节为非规范性内容。
许多网站需要在其身份验证流程中验证凭据(例如电话号码和电子邮件地址)。目前,它们依赖于将一次性密码(OTP)发送到这些通信渠道,以此作为所有权的证明。用户将一次性密码手动反馈给网络应用(通常通过复制/粘贴),这既繁琐又容易出错。
这是一个用于客户端的 JavaScript API 提案,允许网站请求一次性密码,以及一套传输特定的约定(我们从 SMS 开始,后续也可能添加其他方式),可用于与浏览器的协调。
1.1. 客户端 API
在这个提案中,网站能够调用浏览器 API 来请求来自特定传输方式(例如 SMS)的 OTP。
浏览器会中介接收 SMS 并将其传递给调用的网站(通常会询问用户的同意),因此该 API 会异步返回一个 promise。
1.2. 服务器端 API
一旦调用客户端 API,网站的服务器就可以通过请求的传输机制向客户端发送 OTP。对于每种传输机制,都会设置相应的服务器端约定,以确保 OTP 安全且可编程地发送。
例如,对于 SMS,服务器应向客户端发送 绑定到源的一次性代码消息。 [sms-one-time-codes]
在以下的 绑定到源的一次性代码消息中,主机为
"example.com"
,代码为 "123456"
,解释性文本为
"您的验证码是 123456。\n"
。
"您的验证码是 123456. @example.com #123456"
1.3. 特性检测
并非所有用户代理都需要在相同的时间点实现 WebOTP API,因此网站需要一种机制来检测 API 是否可用。
网站可以检查 OTPCredential 全局接口的存在:
1.4. Web 组件
大多数情况下,OTP 验证主要依赖于:
-
客户端的输入、表单和复制/粘贴,
-
服务器端用于发送 SMS 的第三方框架。
我们希望其中一些框架可以开发该 API 的声明性版本,以便于其客户现有代码的部署。
< script src = "sms-sdk.js" ></ script > < form > < input is = "one-time-code" required /> < input type = "submit" /> </ form >
以下是框架如何使用 Web 组件来实现它的示例:
customElements. define( "one-time-code" , class extends HTMLInputElement{ connectedCallback() { this . receive(); } async receive() { let { code, type} = await navigator. credentials. get({ otp: { transport: [ "sms" ] } }); this . value= otp; this . form. submit(); } }, { extends : "input" });
1.5. 中止 API
许多现代网站在客户端处理导航。因此,如果用户从 OTP 流程导航到另一个流程,则需要取消请求,以免用户收到与当前无关的权限提示。
为此,可以传递一个中止控制器来取消请求:
const abort= new AbortController(); setTimeout(() => { // 两分钟后中止 abort. abort(); }, 2 * 60 * 1000 ); let { code, type} = await navigator. credentials. get({ signal: abort. signal, otp: { transport: [ "sms" ] } });
2. 客户端 API
网站调用
navigator.credentials.get({otp:..., ...})
来获取 OTP。
navigator.credentials.get()
的算法会查找所有继承自 Credential
的接口,以实现 请求一个 Credential
的抽象操作。
在该操作中,找到继承自 Credential
的 OTPCredential
,
然后调用
OTPCredential.
来收集任何不需要 用户介入 的可用 凭据,如果找不到唯一的凭据,就调用
[[CollectFromCredentialStore]]()
OTPCredential.
让用户选择凭据源并完成请求。
[[DiscoverFromExternalSource]]()
由于本规范要求使用 授权手势 来创建 OTP 凭据,
OTPCredential.
内部方法继承了 [[CollectFromCredentialStore]]()
Credential.[[CollectFromCredentialStore]]()
的默认行为,即返回一个空集。
然后由
OTPCredential.
来提供 OTP。
[[DiscoverFromExternalSource]]()
2.1. OTPCredential 接口
OTPCredential
接口继承自 Credential
,
包含当获取新的一次性密码时返回给调用者的属性。
OTPCredential
的
接口对象继承自 Credential
的 [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
的实现,并定义了它自己的
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
实现。
[Exposed =Window ,SecureContext ]interface :
OTPCredential Credential {readonly attribute DOMString code ; };
id
-
此属性继承自
Credential
[[type]]
-
OTPCredential
接口对象的[[type]]
内部槽的值为字符串 "otp
"。 code
, 类型为 DOMString,只读-
检索到的一次性密码。
2.1.1.
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
方法
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
每次调用
navigator.credentials.get({otp:..., ...})
时都会调用该方法,该方法负责在请求时返回 OTP(即当传入
options.
时)。
otp
这个 内部方法接受三个参数:
origin
-
该参数是调用
get()
实现时由相关设置对象的 origin 确定的, 即CredentialsContainer
的 请求一个Credential
的抽象操作。 options
-
该参数是一个
CredentialRequestOptions
对象,其options.
成员包含一个otp
OTPCredentialRequestOptions
对象,用于指定要检索的 OTP 的所需属性。 sameOriginWithAncestors
-
该参数是一个布尔值,当且仅当调用者的 环境设置对象与其祖先同源时,该值为
true
。如果调用者是跨源的,则为false
。注意:调用此 内部方法 表明其已被 权限策略允许,该策略在 [CREDENTIAL-MANAGEMENT-1] 级别进行评估。 参见 § 2.5 权限策略集成。
注意: 该算法是同步的: Promise
的解决/拒绝由 navigator.credentials.get()
处理。
当调用此方法时,用户代理必须执行以下算法:
-
令 options 为
options.
的值。otp
-
令 callerOrigin 为
origin
。 如果 callerOrigin 是 不透明的源,则返回一个名称为 "NotAllowedError
" 的DOMException
, 并终止此算法。 -
令 effectiveDomain 为 callerOrigin 的 有效域。 如果 有效域 不是 有效域名,则返回一个名称为 "
SecurityError
" 的DOMException
, 并终止此算法。注意: 有效域 可能解析为一个 主机,其表示方式有多种, 例如 域名、IPv4 地址、IPv6 地址、不透明主机 或 空主机。 这里只允许使用主机的 域名 格式,这是为了简化操作,并认识到与基于 PKI 的安全性结合使用直接 IP 地址识别的各种问题。
-
如果
options.
是 存在的,且其 中止标志 已设置为signal
true
,则返回一个名称为 "AbortError
" 的DOMException
, 并终止此算法。 -
TODO(goto): 研究如何将传输算法与这里连接。
在上述过程中,用户代理应向用户显示一些 UI,以指导他们将 OTP 共享给源。
2.2. CredentialRequestOptions
为了支持通过 navigator.credentials.get()
获取 OTP,本文件扩展了 CredentialRequestOptions
字典,如下所示:
partial dictionary CredentialRequestOptions {OTPCredentialRequestOptions otp ; };
otp
, 类型为 OTPCredentialRequestOptions-
这个可选成员用于发起 WebOTP 请求。
2.3.
OTPCredentialRequestOptions
OTPCredentialRequestOptions
字典为 navigator.credentials.get()
提供了检索 OTP 所需的数据。
dictionary {
OTPCredentialRequestOptions sequence <OTPCredentialTransportType >transport = []; };
transport
, 类型为 sequence<OTPCredentialTransportType>,默认为[]
-
这个可选成员包含有关服务器如何接收 OTP 的提示。 值应为
OTPCredentialTransportType
的成员,但客户端平台必须忽略未知值。
2.4. OTPCredentialTransportType
enum {
OTPCredentialTransportType "sms" , };
sms
-
表示预期 OTP 将通过 SMS 到达。
2.5. 权限策略集成
本规范定义了一个由特征标识符
"otp-credentials
"
确定的 权限控制特征。它的 默认允许列表是 'self
'。 [Permissions-Policy]
Document
的 权限策略 决定了该 文档 中的任何内容是否被允许成功调用 WebOTP
API,即通过
navigator.credentials.get({otp: { transport: ["sms"]}})
。
如果在任何文档中禁用,文档中的所有内容将不被允许使用上述方法:尝试这样做将返回一个错误。
2.6. 在 iframe
元素中使用 WebOTP
当源匹配时,WebOTP API 可在内嵌框架中使用,但默认情况下在跨源
iframe
中被禁用。要覆盖此默认策略并指示允许跨源
iframe
调用 WebOTP API 的 [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
方法,请在
allow
属性中包含
otp-credentials
特征标识符。
在嵌入上下文中使用 WebOTP API 的依赖方应查看 § 4.4 嵌入使用的可见性考虑,以了解关于 UI 改造及其可能的缓解措施。
3. 传输方式
我们预计会有多种不同的传输机制来接收 OTP,最常见的是通过 SMS、电子邮件和硬件设备。
每种传输机制都需要自己的约定来向浏览器提供 OTP。
在此草案中,我们将 API 表面留作扩展,以适应任意数量的传输方式。
3.1. SMS
最常用的 OTP 传输机制之一是通过 SMS 消息,允许开发者验证电话号码。通常,它们嵌入在 SMS 消息中,然后由用户复制粘贴。
[sms-one-time-codes] 定义了 绑定到源的一次性代码消息,一种通过 SMS 发送 OTP 并将其与源关联的格式。
4. 安全性
从安全角度来看,此 API 有两个方面需要考虑:
-
篡改:防止基于消息修改的攻击。
4.1. 可用性
此 API 仅在以下情况下可用:
-
https(或用于开发目的的 localhost)
此 API 也仅通过 https 或 localhost(用于开发目的)可用。我们不完全采用可信 URL 的概念,因为它涵盖的方案(例如 data://123)比我们期望的要多(我们的初步直觉是:(a) https 和 localhost 覆盖了大多数情况,(b) 用户需要明确知道它发送 SMS 的公共实体)。
4.2. 寻址
每种传输机制都负责确保浏览器有足够的信息将 OTP 适当地路由到预期的源。
例如,绑定到源的一次性代码消息明确识别了可以使用 OTP 的源。
代理必须强制执行寻址方案,以确保它能够适当地路由。
4.3. 篡改
没有内置的加密保证,确保此 API 返回的 OTP 没有被篡改。例如,攻击者可以向用户的手机发送包含任意源的 绑定到源的一次性代码消息,而代理会将其传递给请求调用。
调用者有责任:
-
采取必要的检查措施以验证接收到的 OTP 是否有效,例如:
-
根据其已知格式期望(例如仅包含字母数字值)仔细解析,
-
将发送的 OTP 存储并检查服务器端数据库。
-
-
在收到无效 OTP 时优雅降级(例如重新请求一个)。
4.4. 嵌入使用的可见性考虑
在嵌入上下文中简单使用 WebOTP,例如在
iframe
中使用,如 § 2.6 在 iframe 元素中使用 WebOTP 所述,可能会使用户面临
UI 重定向 攻击,也称为 "点击劫持(Clickjacking)"。这是指攻击者在依赖方预期的 UI 之上叠加其自己的
UI,企图诱骗用户对依赖方执行意外的操作。例如,使用这些技术,攻击者可能能够诱骗用户购买商品、转账等。
5. 隐私
从隐私的角度来看,最显著的考虑是用户代理应强制执行用户与网站之间的信息自愿交换。
具体来说,此 API 允许程序化验证用户的个人身份属性,例如电子邮件地址和电话号码。
最常被提出的攻击向量是有针对性的攻击:网站试图在其全部用户中找到一个非常特定的用户。在这种攻击中,如果不加以注意,网站可以使用此 API 尝试找到拥有特定电话号码的特定用户,方法是向所有/部分用户(取决于置信度)发送 绑定到源的一次性代码消息,并检测到接收时的状态。
值得注意的是,此 API 并不帮助获取个人信息,而是帮助验证个人信息。也就是说,此 API 有助于验证用户是否拥有特定的电话号码,但不会帮助首次获取电话号码(它假设网站已经可以访问它)。
尽管如此,这些属性的所有权验证仍然是有关用户的额外信息,用户代理应负责地处理它,通常通过在将 OTP 交回网站之前显示权限提示来进行。
6. 致谢
特别感谢 Steven Soneff、Ayu Ishii、Reilly Grant、Eiji Kitamura、Alex Russell、Owen Campbell-Moore、Joshua Bell、Ricky Mondello 和 Mike West 协助制定此提案。
特别感谢 Tab Atkins, Jr. 创建和维护了用于编写本规范的工具 Bikeshed,并为其提供了撰写建议。