1. 简介
本节为非规范性内容。
登录网站比应有的要困难。用户代理在许多方面都处于独特的位置,可以改善这一体验,大多数现代用户代理也认识到这一点,通过在浏览器中原生提供一定程度的凭证管理。例如,用户可以保存网站的用户名和密码;这些凭证稍后会自动填充到登录表单中,尽管成功率各有不同。
autocomplete
属性为网站提供了一种声明式机制,可以与用户代理协作,通过将特定字段标记为"username"或"password",提升用户代理检测和填充登录表单的能力。用户代理还实现了各种检测启发式方法,以适应那些未在标记中提供此类细节的网站。
虽然这种启发式与声明式结合的检测方式相对有效,但现状仍存在一些检测上的重大缺口。拥有非常规登录机制的网站(例如通过 XMLHttpRequest
[XMLHTTPREQUEST]
提交凭证)难以可靠检测,越来越多用户希望通过联合身份提供者进行认证也是如此。让网站能更直接地与用户代理的凭证管理器交互,一方面可以提升凭证管理器的准确性,另一方面也能帮助用户实现联合登录。
这些使用场景将在 § 1.1 使用场景 和 凭证管理:使用场景与需求 中进一步探讨;本规范试图通过定义一个凭证管理器 API 来满足上述文档中的诸多需求,网站可用其请求用户的凭证,并在用户成功登录时请求用户代理持久化这些凭证。
注意: 此处定义的 API 有意保持简洁,旨在为现有用户代理实现的凭证管理器提供接口,并不直接提供认证功能。这一功能在当前阶段非常有价值,且对厂商和作者的要求都不高。当然,还可以做很多其他提升,详见 § 9 未来工作,其中列举了一些暂未实现但可在未来 API 迭代中探索的思路。
1.1. 使用场景
现代用户代理通常都为用户在登录网站时提供保存密码的能力,并且在用户再次访问网站时能全自动或半自动地填充这些密码。从网站的角度来看,这一行为完全不可见:网站不知道密码已被存储,也不会收到密码被填充的通知。这既有好处,也有不足。一方面,用户代理的密码管理器无论网站是否配合都能工作,这对用户来说非常好。另一方面,密码管理器的行为是一套脆弱且专有的启发式组合,旨在检测和填充登录表单、密码更改表单等。
现状中有几个突出问题值得关注:
-
用户代理在帮助用户使用联合身份提供者方面非常吃力。检测用户名/密码表单提交较为直接,但检测第三方登录则很难做到可靠。若网站能帮助用户代理理解联合登录相关的重定向意图,将会非常有益。
-
同样,用户代理在检测比普通用户名/密码表单更复杂的登录机制时也很困难。作者越来越多地通过
XMLHttpRequest
或类似机制异步登录用户,以改善体验并提升展示控制。对用户来说这很好,但用户代理却难以将其集成到密码管理器中。若网站能帮助用户代理理解他们选择的登录机制,将会非常有益。 -
最后,如果网站能明确告知用户代理凭证已更改,密码更改的支持也会更好。
2. 核心 API
从开发者的角度来看,凭证是一个对象,允许开发者为特定操作做出认证决策。本节定义了一个通用且可扩展的 Credential
接口,作为本规范及其他文档中定义的凭证的基类,并提供一组挂载在 navigator.credentials.*
上的 API,供开发者获取凭证。
不同的 凭证类型 都以接口的形式呈现给 JavaScript,这些接口直接或间接地继承自 Credential
接口。本规范定义了两个此类接口:PasswordCredential
和 FederatedCredential
。其他规范,如
[WEBAUTHN],定义了其他凭证类型。
某个 凭证对于特定 源(origin) 是“有效”的,指的是该凭证可被该源接受用于认证。即使某一时刻的凭证有效,UA 也不能假定未来该凭证仍然有效,原因有以下几点:
-
如果账户持有人更改了密码,密码凭证可能会变为无效。
-
通过短信收到的令牌生成的凭证通常只在一次使用时有效。
一次性 凭证由 凭证源生成,凭证源可能是私钥、访问联合账户、或某个手机号接收短信的能力等。凭证源不会暴露给 Javascript,也不会在本规范中明确表示。为统一模型,我们把密码视为一种凭证源,通过复制生成密码凭证。
虽然 UA 不能假定一个有效凭证再次使用时仍然有效,也不能假定生成过有效凭证的凭证源未来能够再次生成有效凭证,但后者的可能性更大。通过记录(使用 store()
)
过去哪些凭证是有效的,UA 能更好地在将来为用户提供有效的凭证源选择。
2.1. 基础设施
用户代理必须在内部提供一个 凭证存储,即厂商自定义的、不透明的存储机制,用于记录哪些凭证是有效的。它为凭证的访问和持久化提供如下能力:
-
检索凭证列表。该操作接收一个任意过滤条件,返回符合条件的凭证集合。
此外,凭证存储应为每个 防止静默访问
标志(默认设为
true
,除非另有规定)进行维护。当某个源的标志为 true
时,该源 需要用户介入。
注意: 用户介入的重要性详见 § 5 用户介入。
注意: 凭证存储是用户代理实现本规范 API 的内部细节,不直接暴露给网页。其他文档可为特定凭证类型定义更多能力。
本规范依赖 Infra 标准,使用其算法和基础概念 [INFRA]。
每个环境设置对象都有一个 激活凭证类型集合(有序集合),初始为空。
2.1.1. 基础设施算法
2.1.1.1. 与其祖先同源
当 环境设置对象 (settings) 满足以下算法返回
true
时,
称其为
与其祖先同源:
-
如果 settings 的 相关全局对象没有 关联的 Document,返回
false
。 -
令 document 为 settings 的 相关全局对象的 关联的 Document。
-
如果 document 没有 浏览上下文,返回
false
。 -
令 origin 为 settings 的 源。
-
令 navigable 为 document 的 节点可导航对象。
-
当 navigable 有非空 父级 时:
-
返回
true
。
2.1.2. 凭证类型注册表
该注册表将凭证类型(即 [[type]]
的值)映射为与指定凭证类型相关的各种值。例如:选项成员标识符
(正式来说,是 字典成员的标识符),用于 CredentialCreationOptions
和 CredentialRequestOptions
(即“选项字典”)的规范。
注意: 此注册表被 相关凭证接口对象算法使用。
凭证类型 (按字母顺序) | 选项成员标识符 | 适用接口对象 | 获取权限策略 | 创建权限策略 | 规范 | 申请者联系方式 |
---|---|---|---|---|---|---|
digital-credential | digital | DigitalCredential
| digital-credentials-get | null | [DIGITAL-CREDENTIALS] | WICG |
federated | federated | FederatedCredential
| null | null | 本规范:§ 4 联合凭证 | W3C |
identity | identity | IdentityCredential
| identity-credentials-get | null | [FEDCM] | W3C |
otp | otp | OTPCredential
| otp-credentials | null | [WEB-OTP] | WICG |
password | password | PasswordCredential
| null | null | 本规范:§ 3 密码凭证 | W3C |
public-key | publicKey | PublicKeyCredential
| publickey-credentials-get | publickey-credentials-create | [WEBAUTHN] | W3C |
2.1.2.1. 注册项要求与更新流程
-
每一注册项必须声明凭证规范扩展
CredentialCreationOptions
和CredentialRequestOptions
时使用的字典成员标识符(在注册表中称为选项成员标识符)。 -
每一注册项的规范必须包含申请者的联系方式。
对本注册表的更新包括对某一凭证类型注册项的新增、修改或删除。任何人都可以通过向 webappsec-credential-management 仓库提交 pull request 的方式申请更新。 Web 应用安全工作组会将其列入即将召开的会议议程并通知申请者。 申请的审议和处理由 W3C Web 应用安全工作组协商决定。 主席随后会通知申请者结果并相应地更新注册表。
2.2.
Credential
接口
[Exposed =Window ,SecureContext ]interface {
Credential readonly attribute USVString id ;readonly attribute DOMString type ;static Promise <boolean >isConditionalMediationAvailable ();static Promise <undefined >willRequestConditionalCreation (); };
id
, 类型为 USVString,只读-
凭证的标识符。每种凭证类型对于标识符的要求各不相同。例如,对于用户名/密码对,标识符可能代表用户名。
type
, 类型为 DOMString,只读isConditionalMediationAvailable()
-
返回一个
Promise
, 如果且仅当用户代理支持针对该凭证类型的请求调解的conditional
方式时,promise 解析为true
,否则为false
。Credential
的默认实现isConditionalMediationAvailable()
:-
返回 一个解析为
false
的 promise。
支持
conditional
调解的凭证类型规范必须显式重写此函数,使其 解析为true
。注意: 如果未定义该方法,则该 凭证类型不支持
conditional
调解。 -
willRequestConditionalCreation()
-
返回一个
Promise
, 当用户代理注册了依赖方使用conditional
方式创建凭证的意图后,该 promise 解析。Credential
的默认实现willRequestConditionalCreation()
:-
返回 一个解析为
undefined
的 promise。
注意: 如果未定义该方法,则该 凭证类型不支持凭证创建的
conditional
调解。 -
[[type]]
-
Credential
接口对象有一个名为[[type]]
的内部槽, 该槽包含一个 字符串,代表凭证类型。除非另有规定,该槽的值为一个空字符串。各种 § 2.1.2 凭证类型注册表 中列出了所有凭证类型。注意: 对于实现同一接口的所有凭证,
[[type]]
槽的值都相同,因此开发者可以依赖obj.type
返回的字符串明确表示所处理的Credential
类型。 [[discovery]]
-
Credential
接口对象有一个名为[[discovery]]
的内部槽, 代表用户代理收集指定类型凭证的机制。其值为 "credential store
" 或 "remote
"。前者表示所有可用凭证信息都存储在用户代理的凭证存储中, 后者表示用户代理可以通过与外部设备或服务交互,发现凭证存储中未明确表示的凭证。
和 Tobie/Dominic 讨论一下 接口对象相关内容,这里以及 § 2.5.1 请求凭证 等部分。我不确定术语是否正确。也许是 接口原型对象?
部分 Credential
对象是 绑定源的:它们包含一个名为 [[origin]]
的内部槽,
用于存储该 Credential
可能在其上有效的 源。
2.2.1.
Credential
内部方法
Credential
接口对象包含多个内部方法,用于检索和存储 Credential
对象,默认实现为本节所述的“空操作”。
除非另有说明,为继承自 Credential
的接口创建的每个 接口对象,都必须为这些内部方法至少提供一种实现,并根据 凭证类型的需要,覆盖 Credential
的默认实现。例如,见 § 3.2 PasswordCredential 接口、§ 4.1 FederatedCredential 接口,以及 [WEBAUTHN]。
2.2.1.1. [[CollectFromCredentialStore]]
内部方法
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
被调用时需传入 origin、CredentialRequestOptions
和一个布尔值(仅当调用方的 环境设置对象与其祖先同源时为 true)。
该算法从用户代理的 凭证存储 中返回与给定选项匹配的 Credential
对象集合。如果没有匹配的 Credential
对象,则返回空集合。
Credential
的默认实现 [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
:
-
返回空集合。
2.2.1.2. [[DiscoverFromExternalSource]]
内部方法
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
以 并行方式被调用,参数为 origin、CredentialRequestOptions
对象,
及一个布尔值(仅当调用方的 环境设置对象与其祖先同源时为 true)。
若能根据给定选项返回凭证,则返回一个 Credential
;
若无凭证可用,则返回 null
;若发现失败(如选项不正确),则抛出错误(例如会抛出 TypeError
)。
若此类 Credential
只在一次使用或有限时间内有效,此方法负责通过 凭证源生成新的 凭证。
Credential
的默认实现 [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
:
-
返回
null
。
2.2.1.3. [[Store]]
内部方法
[[Store]](credential, sameOriginWithAncestors)
以 并行方式被调用,参数为 Credential
和一个布尔值(仅当调用方的 环境设置对象与其祖先同源时为 true)。
该算法在 Credential
持久化到 凭证存储后返回。
Credential
的默认实现 [[Store]](credential, sameOriginWithAncestors)
:
-
抛出
NotSupportedError
。
2.2.1.4. [[Create]]
内部方法
[[Create]](origin, options, sameOriginWithAncestors)
以 并行方式被调用,参数为 origin、CredentialCreationOptions
以及一个布尔值(仅当调用方的 环境设置对象与其祖先同源时为 true)。
该算法可以:
-
创建一个
Credential
,或 -
未创建凭证并返回
null
,或 -
若因异常情况创建失败则抛出错误(例如选项不正确会导致抛出
TypeError
)。
创建 Credential
时,将返回一个接受 全局对象并返回一个继承自 Credential
的 接口对象的算法。该算法必须由 任务调用。
注意: 此算法的步骤按每种 凭证类型分别定义。
Credential
的默认实现 [[Create]](origin, options, sameOriginWithAncestors)
:
-
返回
null
。
2.2.2.
CredentialUserData
混入
部分 Credential
对象包含旨在为用户在 凭证选择器中提供友好名称和图标,以便用户更易于区分的数据显示:
[SecureContext ]interface mixin {
CredentialUserData readonly attribute USVString name ;readonly attribute USVString iconURL ; };
2.3.
navigator.credentials
开发者通过挂载在 CredentialsContainer
接口上的方法检索 Credential
并与用户代理的 凭证存储交互,该接口作为 Navigator
对象上的 navigator.credentials
属性。
partial interface Navigator { [SecureContext ,SameObject ]readonly attribute CredentialsContainer credentials ; };
credentials
属性必须返回与 活动文档的 浏览上下文关联的 CredentialsContainer
。
注意:如 § 6.3 不安全的站点所述,凭证管理 API 仅在 安全上下文中暴露。
[Exposed =Window ,SecureContext ]interface {
CredentialsContainer Promise <Credential ?>get (optional CredentialRequestOptions options = {});Promise <undefined >store (Credential credential );Promise <Credential ?>create (optional CredentialCreationOptions options = {});Promise <undefined >preventSilentAccess (); };dictionary {
CredentialData required USVString ; };
id
get(options)
-
当调用
get()
时,用户代理必须返回在options
上执行 请求凭证 的结果。CredentialsContainer.get(options) 方法参数。 参数 类型 可为空 可选 描述 options
CredentialRequestOptions
✘ ✔ 控制请求范围的属性集合。 store(credential)
-
当调用
store()
时,用户代理必须返回在credential
上执行 存储凭证 的结果。CredentialsContainer.store(credential) 方法参数。 参数 类型 可为空 可选 描述 credential
Credential
✘ ✘ 要存储的凭证。 create(options)
-
当调用
create()
时,用户代理必须返回在options
上执行 创建凭证 的结果。CredentialsContainer.create(options) 方法参数。 参数 类型 可为空 可选 描述 options
CredentialCreationOptions
✘ ✔ 用于创建 Credential
的选项。 preventSilentAccess()
-
当调用
preventSilentAccess()
时,用户代理必须返回在 当前设置对象上执行 防止静默访问 的结果。注意:此处的目的是作为源发出的用户已注销的信号。也就是说,在用户点击“注销”按钮后,站点会更新用户会话信息,并调用
navigator.credentials.preventSilentAccess()
。这将设置防止静默访问标志,意味着下次用户访问时凭证不会自动返回给页面。注意:此函数以前称为
requireUserMediation()
,现应视为弃用。
Navigator
对象(navigator)时,用户代理必须使用 navigator 的 相关 Realm 创建一个新的 CredentialsContainer
对象,并将其与 navigator 关联。 2.3.1. CredentialRequestOptions
字典
为了通过 Credential
的 get()
方法检索凭证,
调用方需要在 CredentialRequestOptions
对象中指定一些参数。
注意: CredentialRequestOptions
字典是一个扩展点。当有新的凭证类型引入且需要选项时,这些字典类型将会添加到该字典中,以便可以传递给请求。参见 § 8.2
扩展点。
dictionary {
CredentialRequestOptions CredentialMediationRequirement mediation = "optional";AbortSignal signal ; };
mediation
, 类型为 CredentialMediationRequirement, 默认值为"optional"
-
该属性指定了特定凭证请求的调解要求。每个枚举值的含义如下文
CredentialMediationRequirement
所述。 处理细节见 § 2.5.1 请求凭证。 signal
, 类型为 AbortSignal-
该属性允许开发者中止正在进行的
get()
操作。 被中止的操作可能正常完成(通常是如果中止信号在操作完成后才收到),也可能因 中止原因而被拒绝。
unmediated
成员。设置为 true
等同于将 mediation
设置为
"silent
",
设置为 false
等同于将 mediation
设置为 "optional
"。
unmediated
应被视为弃用,新代码应使用 mediation
属性。
CredentialCreationOptions
或 CredentialRequestOptions
(options),其 相关凭证接口对象 是按如下方式收集的一组 接口对象:
注意:本算法使用 凭证类型注册表。
CredentialRequestOptions
(options),若以下步骤返回 true
,则称其 可先验匹配(matchable a priori):
-
遍历 options 的 相关凭证接口对象:
-
如果 interface 的
[[discovery]]
槽的值不是 "credential store
", 返回false
。
-
-
返回
true
。
注意:执行 get(options)
时,
仅当所提供的 CredentialRequestOptions
是 可先验匹配时,才会返回无需 用户介入的凭证。如果请求的凭证类型可能需要从外部服务发现(如 OAuth 令牌、安全密钥认证器等),则需要 用户介入来引导发现过程(如选择联合身份提供方、BTLE 设备等)。
2.3.2. 调解要求
通过 get(options)
或 create(options)
发起请求时,
开发者可通过选择合适的 CredentialMediationRequirement
枚举值,为每次请求设置 用户介入的要求。
注意:§ 5 用户介入章节有更多关于该概念的介绍,以及其对用户代理处理特定源请求的影响。
enum {
CredentialMediationRequirement "silent" ,"optional" ,"conditional" ,"required" };
silent
-
对该操作抑制用户介入。如果可以在无需用户参与的情况下完成操作,则非常好。如果需要用户参与,则操作会返回
null
,而不是让用户介入。注意:此用法旨在支持 “保持我登录此站点”场景,即开发者希望在用户应自动登录时静默获取凭证,只有当用户主动选择登录时才弹出登录提示,不打扰用户。
optional
-
如果可以在无需用户介入的情况下交付凭证,则会交付。如果需要用户介入,则用户代理会让用户参与决策。
注意:这是
get()
的默认行为,适用于开发者确信用户期望开始登录操作的场景。例如用户刚点击“登录”时,看到 凭证选择器不会感到意外或困惑。 conditional
-
对于
get()
, 发现的凭证会在一个非模态对话框中展示给用户,并标明请求凭证的 源。如果用户在对话框外操作,对话框会关闭且Promise
不会被解析或拒绝,也不会显示错误。如果用户选择了某个凭证,则返回该凭证。防止静默访问标志被视为true
,无论实际值为何:conditional
行为总会涉及某种 用户介入(如发现相关凭证)。如果未发现凭证,用户代理可根据凭证类型提示用户采取操作(如插入含凭证的设备)。无论哪种情况,
get()
方法不应立即以null
解析,以避免网站获知没有可用凭证。仅当所有 相关凭证接口都重写了
isConditionalMediationAvailable()
返回一个解析为true
的Promise
时,网站才能向get()
传递conditional
。对于
create()
, 如果用户先前已同意创建凭证且用户代理知晓近期已介入认证,则create()
可在无需额外模态交互的情况下解析。如果用户代理最近未介入认证或无创建凭证同意,则调用必须抛出 “NotAllowedError
”DOMException
。 required
-
即使某源未设置 防止静默访问标志,用户代理也不会在无 用户介入的情况下交付凭证。
注意:此要求用于支持 重新认证或切换用户场景,且仅影响本次操作,不影响源的 防止静默访问标志。如需设置该标志,开发者应调用
preventSilentAccess()
。
2.3.2.1. 示例
get()
,并传入
mediation
设置为
"silent
"。
这样可确保已选择无需用户介入的用户(见 § 5.2 需要用户介入)会自动登录,未选择该行为的用户不会被突然弹出的 凭证选择器打扰:
window.addEventListener('load', async () => { const credentials = await navigator.credentials.get({ ..., mediation: 'silent' }); if (credentials) { // 太棒了!用这些凭证让用户登录吧! } });
document.querySelector('#sign-in').addEventListener('click', async () => { const credentials = await navigator.credentials.get({ ..., mediation: 'optional' }); if (credentials) { // 太棒了!用这些凭证让用户登录吧! } });
get()
的 mediation
设置为
"required
"
强制要求用户介入:
注意:根据浏览器或凭证类型的安全模型,用户可能需要以某种方式进行认证,例如输入主密码、扫描指纹等,才能将凭证交给网站。
document.querySelector('#important-form').addEventListener('submit', async () => { const credentials = await navigator.credentials.get({ ..., mediation: 'required' }); if (credentials) { // 验证凭证是否允许访问,无效则取消提交。 } else { e.preventDefault(); } });
get()
的 mediation
设置为
"required
"
保证点击“添加账号”按钮不会自动返回凭证:
document.querySelector('#switch-button').addEventListener('click', e => { var c = await navigator.credentials.get({ ..., mediation: 'required' }); if (c) { // 用 |c| 登录用户。 } });
2.4. CredentialCreationOptions
字典
为了通过 Credential
的 create()
方法创建凭证,
调用方需要在 CredentialCreationOptions
对象中指定一些参数。
注意: CredentialCreationOptions
字典是一个扩展点。当有新的凭证类型引入时,它们会添加到该字典中,以便可以传递给创建方法。参见 § 8.2
扩展点,以及本文档中的扩展:§ 3.2 PasswordCredential 接口 和 § 4.1 FederatedCredential 接口。
dictionary {
CredentialCreationOptions CredentialMediationRequirement = "optional";
mediation AbortSignal signal ; };
signal
, 类型为 AbortSignal-
该属性允许开发者中止正在进行的
create()
操作。被中止的操作可能正常完成(通常是在操作完成后才收到中止信号),也可能因 中止原因而被拒绝。
2.5. 算法
2.5.1. 请求
Credential
请求凭证算法接受一个
CredentialRequestOptions
(options),并返回一个 Promise
,如果能明确获取凭证则解析为
Credential
,否则解析为
null
。
-
令 settings 为 当前设置对象。
-
断言:settings 是 安全上下文。
-
如果 document 不是 完全激活状态,则返回 一个被拒绝的 promise,原因为 "
InvalidStateError
"DOMException
。 -
如果
options.
已被 中止, 则返回 一个被拒绝的 promise, 原因为signal
options.
的 中止原因。signal
-
令 interfaces 为 options 的 相关凭证接口对象。
-
如果 interfaces 是 空集合,则返回 一个被拒绝的 promise,原因为 "
NotSupportedError
"DOMException
。 -
遍历 interfaces 的每个 interface:
-
如果 options.
mediation
为conditional
且 interface 不支持conditional
用户介入,则返回 一个被拒绝的 promise,原因为 "TypeError
"DOMException
。 -
如果 settings 的 激活凭证类型 包含 interface 的
[[type]]
, 则返回 一个被拒绝的 promise,原因为 "NotAllowedError
"DOMException
。
-
-
令 origin 为 settings 的 源。
-
令 sameOriginWithAncestors 为
true
,如果 settings 与其祖先同源,否则为false
。 -
遍历 options 的 相关凭证接口对象:
-
如果 permission 为 null,跳过。
-
如果 document 不允许 使用 permission,则返回 一个被拒绝的 promise,原因为 "
NotAllowedError
"DOMException
。
-
令 p 为 一个新的 promise。
-
并行运行以下步骤:
-
令 credentials 为 从凭证存储收集凭证的结果,参数为 origin、options 和 sameOriginWithAncestors。
-
如果以下所有条件成立,则解析 p 为 credentials[0] 并跳过剩余步骤:
-
credentials 的 长度为 1
-
origin 不 需要用户介入
-
options 可先验匹配
-
options.
mediation
不是 "conditional
"。
这也许不是最佳模型。如果一个网站希望同时接受用户名/密码和 webauthn 类型凭证,但不想强制使用选择器,对于只用前者且希望保持登录状态的用户会更友好。
-
-
令 result 为 请求用户选择凭证的结果,参数为 options 和 credentials。
-
如果 result 是 接口对象:
-
断言:result 为
null
,或为Credential
。 -
如果 result 是
Credential
, 解析 p 为 result。 -
如果 result 为
null
且 options.mediation
不是conditional
, 解析 p 为 result。注意:如果 options.
mediation
为conditional
且发现凭证为null
, promise p 不会被解析。
-
-
返回 p。
2.5.2. 从凭证存储收集
Credential
给定一个 origin(origin),
一个 CredentialRequestOptions
(options),以及一个仅当调用上下文 与其祖先同源时为 true
的布尔值
(sameOriginWithAncestors),用户代理可以 从凭证存储收集
Credential
,
返回一组本地存储且与 options 的筛选条件匹配的 Credential
对象。如果没有可用的 Credential
对象,返回集合为空:
-
令 possible matches 为一个空集合。
-
遍历 options 的 相关凭证接口对象 的每个 interface:
-
令 r 为执行 interface 的
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
内部方法,参数为 origin、options 和 sameOriginWithAncestors 的结果。如果抛出 异常,则重新抛出该异常。 -
断言:r 是一组 接口对象列表。
-
遍历 r 的每个 c:
-
将 c 添加到 possible matches。
-
-
-
返回 possible matches。
2.5.3. 存储
Credential
存储 Credential
算法接受一个
Credential
(credential),并返回一个 Promise
,该 Promise 在对象持久化到 凭证存储后解析。
-
令 settings 为 当前设置对象。
-
断言:settings 是 安全上下文。
-
如果 settings 的 相关全局对象的 关联文档不是 完全激活状态, 则返回 一个被拒绝的 promise,原因为 "
InvalidStateError
"DOMException
。 -
令 sameOriginWithAncestors 为
true
,如果 当前设置对象 与其祖先同源,否则为false
。 -
令 p 为 一个新的 promise。
-
如果 settings 的 激活凭证类型 包含 credential 的
[[type]]
, 则返回 一个被拒绝的 promise,原因为 "NotAllowedError
"DOMException
。 -
并行运行以下步骤:
-
返回 p。
2.5.4. 创建
Credential
创建 Credential
算法接受一个
CredentialCreationOptions
(options),并返回一个 Promise
,如果可以根据所提供的选项创建凭证,则解析为
Credential
,否则解析为
null
。在异常情况下,
Promise
可能会因相应异常而拒绝:
-
令 settings 为 当前设置对象。
-
断言:settings 是 安全上下文。
-
令 global 为 settings 的 全局对象。
-
如果 document 不是 完全激活状态,则返回 一个被拒绝的 promise,原因为 "
InvalidStateError
"DOMException
。 -
令 sameOriginWithAncestors 为
true
,如果 当前设置对象 与其祖先同源,否则为false
。 -
如果出现如下任一情况,则返回 一个被拒绝的 promise
NotSupportedError
: -
遍历 interfaces 的每个 interface:
-
如果 permission 为 null,跳过。
-
如果 document 不允许 使用 permission,则返回 一个被拒绝的 promise,原因为 "
NotAllowedError
"DOMException
。
-
如果
options.
已被 中止, 则返回 一个被拒绝的 promise, 原因为signal
options.
的 中止原因。signal
-
令 type 为 interfaces[0] 的
[[type]]
。 -
如果 settings 的 激活凭证类型 包含 type,则返回 一个被拒绝的 promise,原因为 "
NotAllowedError
"DOMException
。 -
令 origin 为 settings 的 源。
-
令 p 为 一个新的 promise。
-
并行运行以下步骤:
-
令 r 为执行 interfaces[0] 的
[[Create]](origin, options, sameOriginWithAncestors)
内部方法,参数为 origin、options 和 sameOriginWithAncestors 的结果。如果抛出 异常:
-
令 e 为抛出的 异常。
-
在 global 的 DOM 操作任务源 上排队一个任务以运行以下子步骤:
-
拒绝(Reject) p,原因为 e。
-
-
终止这些子步骤。
-
-
如果 r 是
Credential
或null
,解析 p,值为 r,并终止这些子步骤。 -
断言:r 为一个算法(见 § 2.2.1.4 [[Create]] 内部方法)。
-
在 global 的 DOM 操作任务源 上排队一个任务以运行以下子步骤:
-
解析(Resolve) p,其结果为在 global 上对 promise-calling r 的调用。
-
-
-
返回 p。
2.5.5. 防止静默访问
防止静默访问算法接受一个环境设置对象(settings),并返回一个 Promise
,该 Promise 在 防止静默访问
标志持久化到凭证存储后解析。
-
令 origin 为 settings 的 源。
-
如果 settings 的 相关全局对象的 关联文档不是 完全激活状态, 则返回 一个被拒绝的 promise,原因为 "
InvalidStateError
"DOMException
。 -
令 p 为 一个新的 promise。
-
并行运行以下步骤:
-
返回 p。
3. 密码凭证
无论好坏,许多网站都依赖用户名/密码对作为认证机制。
PasswordCredential
接口是一种凭证,用于实现该场景,既可以存储用户名和密码,也可以存储有助于用户在凭证选择器中选中正确账号的元数据。
3.1. 示例
3.1.1. 基于密码的登录
navigator.credentials.get()
从用户的凭证存储中获取用户名/密码对:
navigator.credentials .get({ 'password': true }) .then(credential => { if (!credential) { // 用户没有为此站点保存凭证,或拒绝分享。此处可以回退到普通登录表单。 return; } if (credential.type == 'password') { var form = new FormData(); form.append('username_field', credential.id); form.append('password_field', credential.password); var opt = { method: 'POST', body: form, credentials: 'include' // 发送 cookie。 }; fetch('https://example.com/loginEndpoint', opt) .then(function (response) { if (/* |response| 表示登录成功 */) { // 记录凭证有效。见下方说明。 navigator.credentials.store(credential); // 通知用户登录成功!执行登录后操作,比如跳转到登陆页面 location.href = '/signed-in-experience'。 } else { // 回退到普通登录表单。 } }); } });
或者,网站也可以直接将凭证数据复制到
form
,并对表单调用 submit()
:
navigator.credentials .get({ 'password': true }) .then(credential => { if (!credential) { return; // 同上... } if (credential.type === 'password') { document.querySelector('input[name=username_field]').value = credential.id; document.querySelector('input[name=password_field]').value = credential.password; document.getElementById('myform').submit(); } });
请注意,前一种方式更推荐,因为它明确调用了 store()
并保存了凭证。基于
form
的机制依赖表单提交,会导致浏览上下文跳转,难以确保登录成功后能调用 store()
。
注意:用户代理展示的凭证选择器可能允许用户选择实际上并未为当前源保存的凭证。例如,在登录
https://www.example.com
时,可能会展示 https://m.example.com
的凭证(见 § 6.1 跨域凭证访问),或者允许用户现场创建新凭证。开发者可以通过每次成功使用凭证后都调用 store()
,即使刚刚通过 get()
获取了凭证,也可以优雅处理这种不确定性:如果该凭证尚未为当前源存储,用户将有机会进行存储;如果已存储,则不会弹窗提示用户。
3.1.2. 登录后确认
为了确保用户在成功登录后能够保存新凭证,可以将凭证传递给 store()
。
fetch()
将凭证提交到登录端点,可以根据响应判断是否登录成功,并通知用户代理。假设有如下登录表单:
<form action="https://example.com/login" method="POST" id="theForm"> <label for="username">用户名</label> <input type="text" id="username" name="username" autocomplete="username"> <label for="password">密码</label> <input type="password" id="password" name="password" autocomplete="current-password"> <input type="submit"> </form>
然后开发者可以用如下处理函数来处理表单提交:
document.querySelector('#theForm').addEventListener('submit', e => { if (window.PasswordCredential) { e.preventDefault(); // 用触发 "submit" 事件的 PasswordCredential 构造一个新的 HTMLFormElement, // 会自动抓取带有 "username" 和 "current-password" autocomplete 属性的字段值: var c = new PasswordCredential(e.target); // fetch 表单的 action URL,并将新凭证对象作为 FormData 传递。如果响应表示登录成功,通知用户代理以便后续存储密码: var opt = { method: 'POST', body: new FormData(e.target), credentials: 'include' // 发送 cookie。 }; fetch(e.target.action, opt).then(r => { if (/* |r| 是 "成功" Response */) navigator.credentials.store(c); }); } });
3.1.3. 修改密码
同样的存储机制可用于“修改密码”,无需任何修改:当用户更改凭证时,网站可通知用户代理用户已用新凭证成功登录。用户代理随后可以更新其存储的凭证:
store()
并传入新信息来更新用户凭证。
假设有如下修改密码表单:
<form action="https://example.com/changePassword" method="POST" id="theForm"> <input type="hidden" name="username" autocomplete="username" value="user"> <label for="password">新密码</label> <input type="password" id="password" name="password" autocomplete="new-password"> <input type="submit"> </form>
开发者可以用如下代码处理表单提交:
document.querySelector('#theForm').addEventListener('submit', e => { if (window.PasswordCredential) { e.preventDefault(); // 用触发 "submit" 事件的 PasswordCredential 构造一个新的 HTMLFormElement, // 会自动抓取带有 "username" 和 "new-password" autocomplete 属性的字段值: var c = new PasswordCredential(e.target); // fetch 表单的 action URL,并将新凭证对象作为 FormData 传递。如果响应表示成功,通知用户代理以便后续存储密码: var opt = { method: 'POST', body: new FormData(e.target), credentials: 'include' // 发送 cookie。 }; fetch(e.target.action, opt).then(r => { if (/* |r| 是 "成功" Response */) navigator.credentials.store(c); }); } });
3.2.
PasswordCredential
接口
[Exposed =Window ,SecureContext ]interface :
PasswordCredential Credential {constructor (HTMLFormElement );
form constructor (PasswordCredentialData );
data readonly attribute USVString password ; };PasswordCredential includes CredentialUserData ;partial dictionary CredentialRequestOptions {boolean =
password false ; };
password
, 类型为 USVString,只读-
该属性表示凭证的密码。
[[type]]
-
PasswordCredential
接口对象有一个名为[[type]]
的内部槽, 其值为 "password
"。 [[discovery]]
-
PasswordCredential
接口对象有一个名为[[discovery]]
的内部槽, 其值为 "credential store
"。 PasswordCredential(form)
-
该构造器接受一个
HTMLFormElement
(form),并执行如下步骤:-
令 r 为执行 通过
HTMLFormElement
创建PasswordCredential
,参数为 form 和 origin 的结果。 -
否则返回 r。
PasswordCredential(data)
-
该构造器接受一个
PasswordCredentialData
(data),并执行如下步骤:-
令 r 为执行 通过 PasswordCredentialData 创建
PasswordCredential
,参数为 data 的结果。 -
否则返回 r。
-
PasswordCredential
对象可通过 navigator.credentials.create()
显式传入 PasswordCredentialData
字典或根据
HTMLFormElement
的可提交元素内容创建。
dictionary :
PasswordCredentialData CredentialData {USVString ;
name USVString ;
iconURL required USVString ;
origin required USVString ; };
password typedef (PasswordCredentialData or HTMLFormElement );
PasswordCredentialInit partial dictionary CredentialCreationOptions {PasswordCredentialInit ; };
password
PasswordCredential
对象是源绑定的。
PasswordCredential
的
接口对象继承了 Credential
的 [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
实现,并定义了自己的 [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
、
[[Create]](origin, options, sameOriginWithAncestors)
和 [[Store]](credential, sameOriginWithAncestors)
的实现。
3.3. 算法
3.3.1.
PasswordCredential
的
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
被调用时,参数为 origin(origin)、CredentialRequestOptions
(options),
以及一个仅当调用上下文与其祖先同源时为 true
的布尔值(sameOriginWithAncestors)。
该算法会从 凭证存储返回一组 Credential
对象。如果没有匹配的 Credential
对象可用,则返回集合为空。
如果 sameOriginWithAncestors 不是 true
,算法将抛出 NotAllowedError
。
-
如果 sameOriginWithAncestors 为
false
,则抛出 "NotAllowedError
"DOMException
。注意:此限制旨在解决 § 6.4 源混淆中提出的问题。
-
如果 options["
password
"] 不为true
,则返回空集合。 -
-
凭证的
[[origin]]
与 origin 同源。
3.3.2. PasswordCredential
的
[[Create]](origin, options, sameOriginWithAncestors)
[[Create]](origin, options, sameOriginWithAncestors)
被调用时,参数为 origin(origin)、CredentialCreationOptions
(options),以及一个仅当调用上下文 与其祖先同源时为 true
的布尔值(sameOriginWithAncestors)。
该算法在可以创建时返回一个 PasswordCredential
,否则返回
null
。
CredentialCreationOptions
字典必须有一个 password
成员,
其值要么为 HTMLFormElement
,
要么为 PasswordCredentialData
。
如果该成员的值无法用于创建 PasswordCredential
,
此算法将抛出 TypeError
异常。
-
如果 options["
password
"] 是HTMLFormElement
, 返回执行 通过HTMLFormElement
创建PasswordCredential
,参数为 options["password
"] 和 origin 的结果。 重新抛出任何异常。 -
如果 options["
password
"] 是PasswordCredentialData
, 返回执行 通过 PasswordCredentialData 创建PasswordCredential
,参数为 options["password
"] 的结果。 重新抛出任何异常。
3.3.3.
PasswordCredential
的 [[Store]](credential, sameOriginWithAncestors)
[[Store]](credential, sameOriginWithAncestors)
被调用时,传入一个 PasswordCredential
(credential),以及一个仅当调用上下文与其祖先同源时为 true
的布尔值
(sameOriginWithAncestors)。当 credential 持久化到 凭证存储后,该算法返回
undefined
。
如果 sameOriginWithAncestors 不是 true
,算法将返回 NotAllowedError
。
-
如果 sameOriginWithAncestors 为
false
,则抛出 "NotAllowedError
"DOMException
, 并且不修改用户代理的 凭证存储。注意:此限制旨在解决 § 6.4 源混淆中提出的问题。
-
如果用户代理的 凭证存储中已包含一个
PasswordCredential
(stored),其id
属性等于 credential 的id
, 且其[[origin]]
槽与 credential 的[[origin]]
同源,则:-
如果用户允许更新凭证(见 用户介入 的定义),则:
否则,如果用户允许存储凭证(见 用户介入 的定义),则:
-
在 凭证存储中存储一个
PasswordCredential
,其属性如下:id
-
credential 的
id
name
-
credential 的
name
iconURL
-
credential 的
iconURL
[[origin]]
-
credential 的
[[origin]]
password
-
credential 的
password
-
3.3.4. 通过 HTMLFormElement
创建 PasswordCredential
要通过
HTMLFormElement
创建 PasswordCredential
,传入一个 HTMLFormElement
(form)和一个 origin(origin),运行如下步骤。
注意: § 3.1.2 登录后确认 和 § 3.1.3 修改密码 提供了推荐用法示例。
-
令 data 为一个新的
PasswordCredentialData
字典。 -
设置 data 的
origin
成员值为 origin 的值。 -
令 formData 为执行
FormData
构造器,参数为 form 的结果。 -
令 newPasswordObserved 为
false
。 -
遍历 elements 的每个 field,执行如下步骤:
-
如果 field 没有
autocomplete
属性,则跳过至下一个 field。 -
令 name 为 field 的
name
属性值。 -
如果 formData 的
has()
方法在 name 上执行结果为false
,则跳过至下一个 field。 -
如果 field 的
autocomplete
属性值包含一个或多个 自动填充细节标记(tokens),则:-
遍历 tokens 的每个 token:
-
如果 token 是以下字符串之一的 ASCII 不区分大小写匹配,则执行相关步骤:
- "
new-password
" -
设置 data 的
password
成员值为执行 formData 的get()
方法,参数为 name 的结果,并将 newPasswordObserved 设为true
。 - "
current-password
" -
如果 newPasswordObserved 为
false
, 设置 data 的password
成员值为执行 formData 的get()
方法,参数为 name 的结果。注意:如果 newPasswordObserved 为
false
,则new-password
字段优先于current-password
字段。 - "
photo
" - "
name
"- "
nickname
" - "
- "
username
"
- "
-
-
-
-
令 c 为执行 通过 PasswordCredentialData 创建 PasswordCredential,参数为 data 的结果。如果抛出 异常,则重新抛出该异常。
-
断言:c 为
PasswordCredential
。 -
返回 c。
3.3.5. 通过
PasswordCredentialData
创建 PasswordCredential
要通过
PasswordCredentialData
创建 PasswordCredential
,给定一个 PasswordCredentialData
(data),运行如下步骤。
-
令 c 为一个新的
PasswordCredential
对象。 -
设置 c 的属性如下:
-
返回 c。
3.3.6.
CredentialRequestOptions
与 PasswordCredential
的匹配
给定一个 CredentialRequestOptions
(options),下述算法如果应将 PasswordCredential
作为 get()
请求的返回结果,则返回 "Matches
",否则返回 "Does Not Match
"。
-
如果 options 拥有
password
成员且其值为true
,则返回 "Matches
"。 -
返回 "
Does Not Match
"。
4. 联合凭证
4.1.
FederatedCredential
接口
[Exposed =Window ,SecureContext ]interface :
FederatedCredential Credential {constructor (FederatedCredentialInit );
data readonly attribute USVString provider ;readonly attribute DOMString ?protocol ; };FederatedCredential includes CredentialUserData ;dictionary {
FederatedCredentialRequestOptions sequence <USVString >;
providers sequence <DOMString >; };
protocols partial dictionary CredentialRequestOptions {FederatedCredentialRequestOptions ; };
federated
provider
, 类型为 USVString,只读-
凭证的联合身份提供者。具体格式见 § 4.1.1 身份提供者标识。
protocol
, 类型为 DOMString,只读,可为空-
凭证的联合身份提供者协议(如 "
openidconnect
")。如果值为null
,则协议可由provider
推断。 [[type]]
-
FederatedCredential
接口对象有一个名为[[type]]
的内部槽, 其值为 "federated
"。 [[discovery]]
-
FederatedCredential
接口对象有一个名为[[discovery]]
的内部槽, 其值为 "credential store
"。 FederatedCredential(data)
-
该构造器接受一个
FederatedCredentialInit
(data),执行如下步骤:-
令 r 为执行 通过 FederatedCredentialInit 创建 FederatedCredential, 参数为 data 的结果。如果抛出 异常,则重新抛出该异常。
-
返回 r。
-
FederatedCredential
对象可通过向 navigator.credentials.create()
传递一个 FederatedCredentialInit
字典进行创建。
dictionary :
FederatedCredentialInit CredentialData {USVString ;
name USVString ;
iconURL required USVString ;
origin required USVString ;
provider DOMString ; };
protocol partial dictionary CredentialCreationOptions {FederatedCredentialInit ; };
federated
FederatedCredential
对象是源绑定的。
FederatedCredential
的接口对象继承了 Credential
的 [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
实现,并定义了自己的 [[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
、
[[Create]](origin, options, sameOriginWithAncestors)
和 [[Store]](credential, sameOriginWithAncestors)
的实现。
注意:如果未来我们让用户代理为用户获取身份认证令牌,可以通过实现
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
来实现。
4.1.1. 身份提供者标识
每个网站在引用某个特定联合身份提供者时应使用相同的标识符。例如,Facebook Login 不应被称为 "Facebook"、"Facebook Login"、"FB"、"FBL"、"Facebook.com" 等等。应有一个规范标识符供所有人使用,因为一致的标识有助于用户代理提供帮助。
为保持一致,传递到本文档定义的 API(如 FederatedCredentialRequestOptions
的
providers
数组或 FederatedCredential
的
provider
属性)必须用该提供者用于登录的 origin 的 ASCII 序列化来标识。也就是说,Facebook 用
https://www.facebook.com
,Google 用 https://accounts.google.com
。
这种 origin 的序列化不包括结尾的 U+002F
斜杠("/
"),但用户代理应默默接受它:https://accounts.google.com/
显然与
https://accounts.google.com
相同。
4.2. 算法
4.2.1.
FederatedCredential
的
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
被调用时,参数为 origin(origin)、CredentialRequestOptions
(options),
以及一个仅当调用上下文与其祖先同源时为 true
的布尔值(sameOriginWithAncestors)。
该算法会从 凭证存储返回一组 Credential
对象。如果没有匹配的 Credential
对象可用,则返回集合为空。
-
如果 sameOriginWithAncestors 为
false
,则抛出 "NotAllowedError
"DOMException
。注意:此限制旨在解决 § 6.4 源混淆中提出的问题。
-
如果 options["
federated
"] 不为true
,则返回空集合。
4.2.2. FederatedCredential
的
[[Create]](origin, options, sameOriginWithAncestors)
[[Create]](origin, options, sameOriginWithAncestors)
被调用时,参数为 origin(origin)、CredentialCreationOptions
(options),以及一个仅当调用上下文与其祖先同源时为 true
的布尔值(sameOriginWithAncestors)。
该算法在可以创建时返回一个 FederatedCredential
,否则返回
null
,异常情况下抛出 异常:
-
返回执行 通过 FederatedCredentialInit 创建 FederatedCredential, 参数为 options["
federated
"] 的结果。如果抛出 异常,则重新抛出该异常。
4.2.3.
FederatedCredential
的 [[Store]](credential, sameOriginWithAncestors)
[[Store]](credential, sameOriginWithAncestors)
被调用时,传入一个 FederatedCredential
(credential),以及一个仅当调用上下文与其祖先同源时为 true
的布尔值
(sameOriginWithAncestors)。当 credential 持久化到 凭证存储后,该算法返回
undefined
。
如果 sameOriginWithAncestors 不是 true
,算法将返回 NotAllowedError
。
-
如果 sameOriginWithAncestors 为
false
,则抛出 "NotAllowedError
"DOMException
, 并且不修改用户代理的 凭证存储。注意:此限制旨在解决 § 6.4 源混淆中提出的问题。
-
如果用户代理的 凭证存储中已包含一个
FederatedCredential
, 其id
属性等于 credential 的id
, 且其[[origin]]
槽与 credential 的[[origin]]
同源, 且其provider
等于 credential 的provider
, 则直接返回。 -
如果用户允许存储凭证(见 用户介入 的定义),则在 凭证存储中存储一个
FederatedCredential
, 其属性如下:
4.2.4. 通过
FederatedCredentialInit
创建 FederatedCredential
要 通过
FederatedCredentialInit
创建 FederatedCredential
,给定一个 FederatedCredentialInit
(init),运行如下步骤。
5. 用户介入
通过 API 向网页暴露凭证信息会对用户隐私产生诸多潜在影响。因此,用户代理必须在多个情景下让用户介入,以确保用户清楚地了解发生了什么,以及自己的凭证将与谁共享。
如果某个动作是在获得用户明确同意后发生的,我们称之为用户介入。同意可以通过用户直接与凭证选择器界面交互等方式表达。一般来说,用户介入动作会涉及向用户展示某种界面,要求他们做出决定。
如果某个动作是静默发生的,没有获得用户明确同意,则称为“无介入”。例如,如果用户将浏览器配置为对特定源持久授予凭证访问权限,则凭证可能会在未向用户展示请求界面的情况下被提供。
这里我们列出所有凭证类型通用的部分要求,但请注意,用户代理还有较大的自由度(它处于帮助用户的特权位置)。此外,特定的凭证类型可能有超出这里通用要求的额外要求。
5.1. 存储和更新凭证
凭证信息是敏感数据,用户必须能一直掌控这些信息的存储。意外存储凭证可能会导致用户本地设备上的个人资料意外地与某个在线身份关联。为降低意外风险:
-
凭证信息不应在没有用户介入的情况下被存储或更新。例如,用户代理可以在每次调用
store()
时弹出 “保存此凭证?” 对话框。如果用户代理选择以 “始终保存密码” 的方式为用户提供持久授权,则可以推断用户同意(不过我们建议用户代理应谨慎,可以只提供“始终保存生成的密码”或“始终保存此网站的密码”等更窄范围的选项)。
-
用户代理应在凭证被存储时通知用户。通知方式可以是地址栏中的图标或类似的位置。
-
用户代理必须允许用户手动移除已存储的凭证。此功能可以通过设置页面实现,也可以通过与上述通知交互实现。
5.2. 强制用户介入
默认情况下,所有用户介入都要求对所有源进行,因为防止静默访问标志在凭证存储中被设为
true
。用户可以选择授予某个源对凭证的持久访问权限(例如“保持登录此网站”的选项),这会将该标志设为
false
。这样,用户就会一直保持登录状态,这对可用性和便利性很有利,但也可能带来意外影响(比如用户代理在设备间同步该标志状态)。
为降低意外风险:
-
用户代理必须允许用户对某个源或所有源强制用户介入。此功能可以作为全局开关实现,覆盖每个源的防止静默访问标志,使其返回
false
,或者对特定源(或特定源上的特定凭证)实现更细粒度的设置。 -
用户代理不得在没有源的用户介入的情况下将其防止静默访问标志设为
false
。例如,凭证选择器(见 § 5.3 凭证选择)可以有一个复选框,用户可以切换以将凭证标记为该源无需介入即可使用,或者用户代理在凭证管理器的初始设置流程中询问用户默认设置。 -
用户代理必须在凭证被提供给某个源时通知用户。通知方式可以是地址栏中的图标或类似的位置。
-
如果用户清除某个源的浏览数据(cookies、localStorage 等),用户代理必须将该源的防止静默访问标志设为
true
。
5.3. 凭证选择
当对某个需要用户介入的源调用 get()
时,用户代理必须向用户请求共享凭证信息的权限。这应该以凭证选择器的形式出现,向用户展示可在该网站使用的凭证列表,允许用户选择一个提供给网站,也可以取消请求。
选择器的用户界面应以网站无法伪造的方式与网页 UI 区分开。例如,选择器可以覆盖用户代理的 UI,防止伪造。
选择器的用户界面必须包含请求凭证的源的标识。
选择器的用户界面应包含所有与请求凭证的源相关联的 Credential
对象。
用户代理可以在每个 Credential
对象上内部关联本文件未指定的附加信息,以增强选择器的实用性。例如,favicon 可帮助区分身份提供者等。任何额外信息都不得直接暴露给网页。
选择器的行为未在此定义:鼓励用户代理在用户教育和引导选择凭证方面进行 UI 创新。不过,选择器的接口如下:
Credential
,传入一个 CredentialRequestOptions
(options)和一组来自凭证存储的 Credential
对象(本地发现的凭证)。
该算法返回 null
(如果用户拒绝共享凭证),返回一个 Credential
(如果用户选择了具体凭证),或者返回一个
Credential
接口对象(如果用户选择了凭证类型)。
6. 安全注意事项
以下各节为各种安全和隐私注意事项的指导原则。不同凭证类型可能会强制执行更严格或更宽松的版本。
6.1. 跨域凭证访问
凭证属于敏感信息,用户代理在确定何时可以安全地与网站共享时需谨慎。最安全的做法是仅与保存凭证的精确源共享凭证。然而,这对 Web 来说可能过于严格:请考虑将功能分布在子域名上的网站,如
example.com
和 admin.example.com
。
为在减少用户不便与增强凭证安全之间折中,用户代理:
-
不得在 scheme 降级时在不同源之间共享凭证。例如,允许将
http://example.com/
上保存的凭证用于https://example.com/
(鼓励开发者迁移到安全传输),但反过来则很危险。 -
可以使用 Public Suffix List [PSL],通过比较凭证的 可注册域名和调用
get()
的源,确定凭证的有效作用范围。即:在https://www.example.com/
调用get()
时,可以向用户提供https://admin.example.com/
和https://example.com/
上保存的凭证,反之亦然。 -
在凭证的源与调用源不完全匹配时,不得在响应
get()
时直接提供凭证,除非经过用户介入。即Credential
对象仅在源精确匹配时直接返回,否则通过选择器提供给用户。
6.2. 凭证泄露
建议开发者采取措施避免跨站脚本攻击导致对用户账户的持久访问,比如设置合理的 Content Security Policy [CSP],限制数据可发送的端点。尤其应确保以下指令在页面策略中显式或隐式设置:
-
[=
script-src
=] 和 [=object-src
=] 都限制页面脚本执行,从而降低跨站脚本攻击成功概率。如果页面有填充form
元素,还应设置 [=form-action
=] 指令。 -
[=
connect-src
=] 限制fetch()
可提交数据的源(可防止凭证被泄露到evil.com
)。 -
[=
child-src
=] 限制页面可嵌入的嵌套浏览上下文,防止恶意postMessage()
目标注入。[HTML]
当然,开发者还应正确转义输入输出,并考虑使用其他防护措施,比如 Subresource Integrity [SRI] 进一步降低风险。
定义具体凭证类型时,应充分考虑凭证数据的传输方式。例如,可以定义仅限同源端点的传输机制。
6.3. 不安全站点
用户代理不得向非安全上下文环境暴露此处定义的 API。用户代理可能实现自动填充机制,在 非可信 URL 上存储凭证并填充登录表单,但这些站点不能直接与凭证管理器交互,也不得访问在 安全上下文中保存的凭证。
6.4. 源混淆
如果被嵌入的页面能够访问本规范定义的 API,可能会导致用户被误导,将凭证授权给非顶级浏览上下文的源,而用户只能合理理解顶级上下文的安全源。
本规范将凭证管理 API 暴露给这些上下文,因为某些凭证类型在用户代理给予足够 UI 支持和上下文提示后,可以安全地暴露。
但某些具体凭证类型很难在这些上下文安全暴露,因此通过在 [[Create]](origin, options, sameOriginWithAncestors)
、
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
、
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
和 [[Store]](credential, sameOriginWithAncestors)
方法中进行检查来加以限制。
例如,PasswordCredential
的
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
方法如果在 Worker
或非 顶级浏览上下文中调用,会立即返回空集合。
6.5. 注销
如 § 5.2 强制用户介入 所述,如果用户选择自动登录网站,则用户代理会在网站请求时提供凭证。网站可通过调用 CredentialsContainer
的
preventSilentAccess()
方法,关闭某源的自动登录行为。
用户代理依赖网站正确操作;若网站疏忽或恶意,未调用此方法,用户代理仍会继续提供凭证,这比网站在用户点击“注销”时不清理凭证更糟,因为用户代理也参与了认证过程。
用户必须能够控制此行为。如 § 5.2 强制用户介入 所述,清除某源的 Cookie 也会将该源在 凭证存储中的防止静默访问标志重置为
true
。此外,用户代理应为禁用某源自动登录提供 UI 控件,比如与凭证已被提供通知绑定的选项。
7. 隐私注意事项
7.1. 计时攻击
如果用户没有针对某个源的凭证,调用 get()
时会非常快速地返回结果。恶意网站可能据此区分“没有凭证”与“有凭证但用户选择不分享”的用户。
用户代理也应对凭证请求进行速率限制。短时间内多次请求凭证几乎肯定属于滥用行为。
7.2. 选择器泄露
如果用户代理的凭证选择器显示由源提供的图片(例如 Credential
显示网站 favicon),则这些图片的请求不得与实例化选择器直接关联,以避免泄露选择器的使用。可以选择在保存或更新 Credential
时后台获取图片,并在该 Credential
生命周期内缓存它们。
这些图片必须以 credentials mode 为 "omit
"、service-workers mode 为 "none
"、client 为 null
、initiator 为空字符串、destination 为 "subresource
" 进行获取。
此外,如果用户代理允许用户更改凭证关联的名称或图标,则对数据的更改不应暴露给网站(例如用户将两个凭证命名为“我的假账号”和“我的真账号”)。
7.3. 本地存储数据
该 API 允许某个 源将数据与用户的个人资料持久存储。由于大多数用户代理对凭证数据和“浏览数据”(如 cookie 等)处理方式不同,这可能导致用户在清除 cookie 时以为所有来源痕迹都被清除,实际仍有凭证数据保留而感到意外。
用户代理应提供 UI,清楚地向用户展示某个源已存储凭证数据,并应让用户易于移除这些数据。
8. 实现注意事项
本节为非规范内容。
8.1. 网站作者
这里可以补充关于 API
何时如何使用的建议,尤其是 mediation
相关内容。
[Issue #w3c/webappsec#290]
描述通过 fetch()
提交含 FormData
的凭证时的编码限制。
在为某个凭证类型进行特性检测时,建议开发者验证相关 Credential
的具体实现是否存在,而不是仅检查 navigator.credentals
是否存在。后者只能验证 API 本身是否可用,却不能确保特定类型凭证是否被支持。例如某网站要求密码,则检查
if (window.PasswordCredential)
是最有效的支持验证方式。
8.2. 扩展点
本规范定义了通用的高层 API,目的是将其扩展为满足具体认证需求的凭证类型。扩展方式应尽量简洁:
-
定义一个继承自
Credential
的新接口: -
在
ExampleCredential
的接口对象上定义适当的[[Create]](origin, options, sameOriginWithAncestors)
、[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
、[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
、 和[[Store]](credential, sameOriginWithAncestors)
方法。[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
适用于永久有效的凭证(可直接从凭证存储复制),而[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
适用于需从凭证来源重新生成的凭证类型。像
PublicKeyCredential
的[[Create]](origin, options, sameOriginWithAncestors)
和[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
等长时操作,建议使用options.signal
让开发者可中止操作。详见 DOM § 3.3 使用 AbortController 和 AbortSignal 对象。ExampleCredential
的[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
内部方法被调用时,参数为 origin(origin
)、CredentialRequestOptions(options
)、以及仅当调用上下文与其祖先同源时为true
的布尔值。该算法返回与所提供 options 匹配的Credential
集合。如果没有匹配,则集合为空。-
断言:
options
[example
] 存在。 -
如果
options
[example
] 不为真,则返回空集合。 -
遍历 凭证存储中的每个 credential:
-
...
-
-
-
定义
ExampleCredential
接口对象的[[discovery]]
槽值: -
扩展
CredentialRequestOptions
,添加新凭证类型需要响应get()
的选项: -
扩展
CredentialCreationOptions
,添加新凭证类型需要响应create()
的数据: -
如果新凭证类型支持
conditional
用户介入,则定义ExampleCredential/isConditionalMediationAvailable()
,其返回 a promise resolved withtrue
。 -
按 § 2.1.2.1 注册条目要求和更新流程,为新的 "example" 凭证类型及其对应项添加注册表条目:
-
CredentialCreationOptions
和CredentialRequestOptions
的options 成员标识符(此处为 "example"),以及
注意:凭证类型 options 字典的options 成员标识符必须在
CredentialCreationOptions
和CredentialRequestOptions
中一致,并且应与Credential
接口对象的[[type]]
槽值相同。 -
你可能还会发现需要新的原语。例如,如需在复杂的多因素登录流程中一次返回多个 Credential
对象,可以为 CredentialsContainer
添加一个 getAll()
方法,其返回 sequence<Credential>
,并定义合理机制以处理不同类型凭证请求。
如需扩展,建议联系 public-webappsec@ 进行咨询与评审。
8.3. 浏览器扩展
理想情况下,支持扩展系统的用户代理会允许第三方钩入这些 API 端点,以改善第三方凭证管理软件的行为,就像用户代理通过命令式方式改善自身一样。
9. 未来工作
本节为非规范内容。
此处定义的 API 只是最基础地将用户代理的凭证管理器暴露给 Web,并允许 Web 协助这些凭证管理器识别何时使用了联合身份提供者。下一步逻辑将参考 [WEB-LOGIN] 等文档(以及一定程度上的 Mozilla 的 BrowserID [BROWSERID])的思路。
用户代理处于一个独特位置,可以有效调解用户、身份提供者和网站之间的关系。如果用户代理能减少典型认证流程中的风险和困惑,用户的处境将比现在大为改善。
一种自然的信息暴露方式可能是扩展 FederatedCredential
接口,增加如认证令牌等属性,或添加某种声明提供者支持认证类型的 manifest 格式属性。
此处描述的 API 设计为足够可扩展,以支持需要用户交互的用例,甚至可能涉及除了请求凭证的网站之外的其他网站。我们希望我们采用的基于 Promise
的系统,足够扩展以支持此类异步流程,这些流程可能需要多个浏览上下文之间的某种交互(例如 idp.com
上的调解活动可解析传回给 rp.com
的
Promise),或者未来支持设备与用户代理之间的交互(例如 [WEBAUTHN]),而无需从零重新设计
API。
循序渐进。