1. 简介
本节为非规范性内容。
登录网站比应有的要困难得多。用户代理处于一个独特的位置,能够在多方面改善这种体验,大多数现代用户代理已经通过在浏览器中原生提供某种程度的 credential management 来认识到这一点。例如,用户可以为网站保存用户名和密码;那些 凭证 随后会自动填充到登录表单中,尽管成功率各不相同。
属性
autocomplete
提供了一种声明性机制,网站可以通过它与用户代理协作——通过将特定字段标记为 "username" 或
"password"——以提高后者检测并填写登录表单的能力;用户代理还实现了各种检测启发式方法,以便与那些没有花时间在标记中提供这些细节的网站配合。
虽然这种启发式与声明式检测的组合运行得相对较好,但现状仍然存在一些检测困难的重大空白。具有不常见登录机制的网站(例如通过 XMLHttpRequest
提交凭证)很难可靠检测,用户希望使用联合身份提供者进行认证的情况也越来越常见。允许网站更直接地与用户代理的 凭证管理器 交互,一方面可以使 凭证管理器 更加准确,另一方面也能帮助用户进行联合登录。
这些用例在 § 1.1 Use Cases 和 Credential Management: Use Cases and Requirements 中有更详细的探讨;本规范通过定义一个凭证管理器 API(Credential Manager API),尝试解决该文档概述的许多需求,网站可以使用该 API 为用户请求 凭证,并在用户成功登录时请求用户代理持久化这些凭证。
注意: 这里定义的 API 有意保持小而简单:它并不打算自身提供认证功能,而仅限于向现有用户代理实现的 凭证管理器 提供一个接口。该功能在 现在 就已很有价值,而不需要供应商或作者付出大量额外努力。当然,仍有许多可以做的事情。参见 § 9 Future Work,其中列出了一些我们暂时搁置、但可在未来 API 迭代中继续探讨的想法。
1.1. 使用场景
现代用户代理通常都为用户在登录网站时提供保存密码的能力,并且在用户再次访问网站时能全自动或半自动地填充这些密码。从网站的角度来看,这一行为完全不可见:网站不知道密码已被存储,也不会收到密码被填充的通知。这既有好处,也有不足。一方面,用户代理的密码管理器无论网站是否配合都能工作,这对用户来说非常好。另一方面,密码管理器的行为是一套脆弱且专有的启发式组合,旨在检测和填充登录表单、密码更改表单等。
现状中有几个突出问题值得关注:
-
用户代理在帮助用户使用联合身份提供者方面非常吃力。检测用户名/密码表单提交较为直接,但检测第三方登录则很难做到可靠。若网站能帮助用户代理理解联合登录相关的重定向意图,将会非常有益。
-
同样,用户代理在检测比普通用户名/密码表单更复杂的登录机制时也很困难。作者越来越多地通过
XMLHttpRequest或类似机制异步登录用户,以改善体验并提升展示控制。对用户来说这很好,但用户代理却难以将其集成到密码管理器中。若网站能帮助用户代理理解他们选择的登录机制,将会非常有益。 -
最后,如果网站能明确告知用户代理凭证已更改,密码更改的支持也会更好。
2. 核心 API
从开发者的角度来看,凭证是一个对象,允许开发者为特定操作做出认证决策。本节定义了一个通用且可扩展的 Credential
接口,作为本规范及其他文档中定义的凭证的基类,并提供一组挂载在 navigator.credentials.*
上的 API,供开发者获取凭证。
不同的 凭证类型 都以接口的形式呈现给 JavaScript,这些接口直接或间接地继承自 Credential
接口。本规范定义了两个此类接口:PasswordCredential
和 FederatedCredential。其他规范,如
[WEBAUTHN],定义了其他凭证类型。
某个 凭证对于特定 源(origin) 是“有效”的,指的是该凭证可被该源接受用于认证。即使某一时刻的凭证有效,UA 也不能假定未来该凭证仍然有效,原因有以下几点:
-
如果账户持有人更改了密码,密码凭证可能会变为无效。
-
通过短信收到的令牌生成的凭证通常只在一次使用时有效。
一次性 凭证由 凭证源生成,凭证源可能是私钥、访问联合账户、或某个手机号接收短信的能力等。凭证源不会暴露给 Javascript,也不会在本规范中明确表示。为统一模型,我们把密码视为一种凭证源,通过复制生成密码凭证。
虽然 UA 不能假定一个有效凭证再次使用时仍然有效,也不能假定生成过有效凭证的凭证源未来能够再次生成有效凭证,但后者的可能性更大。通过记录(使用 store())
过去哪些凭证是有效的,UA 能更好地在将来为用户提供有效的凭证源选择。
2.1. 基础设施
凭证管理器是一种应用程序、硬件设备或服务,用于存储、组织、管理并允许选择凭证。典型的凭证管理器包括数字钱包、密码管理器,以及passkey管理器。
用户代理必须在内部提供一个凭证存储,它是一种供应商特定的不透明存储机制,用于记录哪些凭证已被激活。它为凭证的访问和持久化提供以下功能:
-
检索凭证列表。此操作接受任意筛选条件,并返回与筛选条件匹配的一组凭证。
此外,凭证存储应为源维护一个prevent silent access
标志(除非另有说明,默认设为true)。如果该标志为true,则该源需要用户介入。
Note: 关于用户介入的重要性,详见§ 5 用户介入。
Note: 凭证存储是用户代理实现本文档所述 API 的内部实现细节,不会直接暴露给 Web。其他文档可能会为特定凭证类型规定更多能力。
本文档在其算法和正文中使用的一些基础概念依赖 Infra Standard [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 (); };
id, 类型为 USVString,只读-
凭据的标识符。每种 凭据 类型的标识符要求是不同的。例如,对于用户名/密码元组,它可能代表用户名。
type, 类型为 DOMString,只读isConditionalMediationAvailable()-
返回一个
Promise,当且仅当用户代理支持conditional方式进行 凭据请求调解 且针对 凭据类型 时,该 Promise 会 解析为true,否则为false。Credential的默认isConditionalMediationAvailable()实现如下:任何支持
conditional调解的 凭据类型 规范,必须显式重写该函数,使其 解析为true。注意:如果此函数不存在,
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
的默认实现,具体应依据对应的
凭据类型。
例如,§ 3.2 PasswordCredential 接口、
§ 4.1 FederatedCredential 接口 以及
[WEBAUTHN]。
2.2.1.1. [[CollectFromCredentialStore]] 内部方法
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
被调用时,传入一个 源 origin、一个 CredentialRequestOptions
对象,以及一个布尔值,仅当调用方的 环境设置对象与其 祖先同源时才为 true。
该算法会从用户代理的 凭据存储 中返回一组与所提供 options 匹配的 Credential
对象。
如果没有可用的匹配 Credential
对象,
返回的集合将为空。
Credential
的
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
默认实现如下:
-
返回空集合。
2.2.1.2. [[DiscoverFromExternalSource]] 内部方法
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
会并行调用,参数为一个 origin、一个 CredentialRequestOptions
对象,以及一个布尔值,仅当调用方的 环境设置对象与其
祖先同源时为 true。
如果能够根据所提供的 options 返回一个 Credential
,则返回该对象;若没有凭据可用,则返回 null;如果发现失败(例如 options 参数错误会产生一个 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 仅暴露在 安全上下文(Secure Context) 中。
[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上执行 请求Credential的结果。CredentialsContainer.get(options) 方法的参数。 参数 类型 可为 null 可选 说明 optionsCredentialRequestOptions✘ ✔ 控制请求范围的属性集。 store(credential)-
当调用
store()时,用户代理必须返回在credential上执行 存储Credential的结果。CredentialsContainer.store(credential) 方法的参数。 参数 类型 可为 null 可选 说明 credentialCredential✘ ✘ 要存储的凭据。 create(options)-
当调用
create()时,用户代理必须返回在options上执行 创建Credential的结果。CredentialsContainer.create(options) 方法的参数。 参数 类型 可为 null 可选 说明 optionsCredentialCreationOptions✘ ✔ 用于创建 Credential的参数。 preventSilentAccess()-
当调用
preventSilentAccess()时,用户代理必须返回在当前设置对象 上执行 禁止静默访问 的结果。注意: 这里的目的是让源能发出用户已退出登录的信号。即在用户点击“退出”按钮后,网站更新用户会话信息,并调用
navigator.credentials.preventSilentAccess()。这会设置 禁止静默访问标志(prevent silent access flag),意味着下次用户访问时不会自动将凭据返回给页面。注意: 此方法之前名为
requireUserMediation(),现已废弃。
Navigator
对象(navigator)时,用户代理必须使用 navigator 的 相关 Realm 创建一个
CredentialsContainer
对象,并将其与 navigator 关联。
2.3.1. CredentialRequestOptions 字典
为了通过 get()
获取 Credential
,调用者需要在一个 CredentialRequestOptions
对象中指定一些参数。
注意:CredentialRequestOptions
字典是一个扩展点。当引入需要参数的新凭据类型时,其字典类型将加入到该字典中,以便在请求时传递参数。详见 § 8.2 扩展点。
dictionary {CredentialRequestOptions CredentialMediationRequirement mediation = "optional";DOMString uiMode ;AbortSignal signal ; };
mediation, 类型为 CredentialMediationRequirement,默认值为"optional"-
该属性指定给定凭据请求的调解要求。每个枚举值的含义详见下方
CredentialMediationRequirement。 处理细节见§ 2.5.1 请求凭据。 uiMode, 类型为 DOMString-
该属性指定用户代理执行用户调解时,给定凭据请求的用户界面模式。各值含义详见
CredentialUiMode, 处理细节见 § 2.5.1 请求凭据。 signal, 类型为 AbortSignal-
该属性允许开发者中止正在进行的
get()操作。被中止的操作可能会正常完成(如在操作结束后才收到中止),也可能因 中止原因被拒绝。
unmediated 成员。将其设为 true 等价于将 mediation
设为
"silent",
设为 false 等价于将 mediation
设为 "optional"。
unmediated 应视为已废弃;新代码应只依赖 mediation。
CredentialCreationOptions
或 CredentialRequestOptions
(options),
相关凭据接口对象
是一组通过如下方式收集的 接口对象:
注意:该算法使用 凭据类型注册表。
-
令 settings 为 当前设置对象。
-
对于 options 的每个 optionKey → optionValue:
-
令 credentialInterfaceObject 为 合适的接口对象 (在 settings 的 全局对象上),其 Options Member Identifier 为 optionKey。
-
断言:credentialInterfaceObject 的
[[type]]槽等于 凭据类型 ,其 Options Member Identifier 为 optionKey。 -
将 credentialInterfaceObject 添加到 relevant interface objects。
-
-
返回 relevant interface objects。
CredentialRequestOptions
(options)如果以下步骤返回 true ,则
先验可匹配(matchable a priori):
-
对于 options 的每个 相关凭据接口对象 interface:
-
如果 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既不 resolve 也不 reject,页面不会因此收到错误。若用户在对话框中选取了凭据,则凭据返回给调用者。禁止静默访问标志 会被当作true处理,无论其实际值为何:conditional行为总会涉及用户调解(若发现可用凭据)。若未发现凭据,用户代理可根据凭据类型提示用户(如插入设备)。无论如何,
get()不得立即以null解决,以保护网站不知晓缺失凭据的事实。只有当所有请求的 凭据接口都已重写
isConditionalMediationAvailable()并返回以true解析的Promise时,网站才能将conditional作为参数传给get()。对于
create(), 若用户先前已同意创建凭据且用户代理最近调解过认证,则create()调用可无需额外模态操作。否则(未调解或无同意),则必须抛出 "NotAllowedError"DOMException。 required-
不经过用户调解 用户代理绝不会交付凭据,即便该源未设置禁止静默访问标志。
注意: 该要求用于如重新认证 或用户切换等场景。本要求仅影响当前操作,与此源的 禁止静默访问标志 无关。如需设置标志,开发者应调用
preventSilentAccess()。
2.3.3. 界面模式
开发者通过 get(options)
发起请求时,可以通过选择合适的枚举值 CredentialUiMode
指定期望的
用户调解模式。
UI 模式字段无默认值。当未指定时,
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) 方法必须仅根据
CredentialMediationRequirement
决定用户代理行为。
注意: 这让调用方可以区分不同的 用户调解形式。是否显示 UI 取决于
此请求的 CredentialMediationRequirement
值。
并非所有 CredentialUiMode
与 CredentialMediationRequirement
组合都有意义,具体交互行为需参考相关类型的
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors) 方法规定。
enum {CredentialUiMode "immediate" };
immediate-
对
get()的调用会使用户代理显示模态凭据选择对话框,或立即完成操作而不返回凭据。这样可让网站向用户展示简化的登录 UI;否则可转而显示本地登录页,或继续未登录流程。
若无凭据可在凭据选择对话框中显示,则会跳过用户调解 (如不会提示用户输入凭据)。
注意: 本规范未提供类似
isConditionalMediationAvailable()的静态方法来检测immediate是否可用。 若未来有更多凭据类型支持该调解模式,将需要一种更通用的特性检测机制,以表明某组合下哪些凭据类型受支持。
2.3.3.1. 示例
get()
,传入
mediation
字段值为
"silent"。
这样已选择“无需用户调解即登录”(见§ 5.2 要求用户调解)的用户会被直接登录,
其它用户则不会看到突兀的
凭据选择器:
window.addEventListener('load', async () => {
const credentials = await navigator.credentials.get({
...,
mediation: 'silent'
});
if (credentials) {
// Hooray! Let’s sign the user in using these credentials!
}
});
document.querySelector('#sign-in').addEventListener('click', async () => {
const credentials = await navigator.credentials.get({
...,
mediation: 'optional'
});
if (credentials) {
// Hooray! Let’s sign the user in using these credentials!
}
});
get()
的 mediation
字段设为
"required"
要求用户调解:
注意: 具体交互可能需用户进行认证(如输入主密码、指纹扫描等),具体取决于浏览器或凭据类型。
document.querySelector('#important-form').addEventListener('submit', async () => {
const credentials = await navigator.credentials.get({
...,
mediation: 'required'
});
if (credentials) {
// Verify that |credentials| enables access, and cancel the submission
// if it doesn’t.
} else {
e.preventDefault();
}
});
get()
的 mediation
字段设为
"required",
可确保点击“添加账户”按钮时不会自动返回凭据:
document.querySelector('#switch-button').addEventListener('click', e => {
var c = await navigator.credentials.get({
...,
mediation: 'required'
});
if (c) {
// Sign the user in using |c|.
}
});
2.4. CredentialCreationOptions 字典
为了通过 create()
创建 Credential
,调解者需要在 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
请求 Credential 算法
接收一个 CredentialRequestOptions
(options)参数,并返回一个
Promise
:能唯一获取凭据时 resolve 为 Credential,
否则 resolve 为 null。
-
令 settings 为 当前设置对象。
-
断言:settings 是 安全上下文。
-
令 document 为 settings 的 相关全局对象的 关联 Document。
-
如果 document 不是 完全激活,则返回 一个 rejected 的 promise, 原因为 "
InvalidStateError"DOMException。 -
如果
options.被 中止, 返回 一个 rejected 的 promise, 原因为signaloptions.的 中止原因。signal -
令 interfaces 为 options 的 相关凭据接口对象。
-
如果 interfaces 是 空集合, 则返回 一个 rejected 的 promise, 原因为 "
NotSupportedError"DOMException。 -
遍历 interfaces 中每个 interface:
-
如果 options.
mediation为conditional且 interface 不支持conditional用户调解, 返回 一个 rejected 的 promise, 原因为 "TypeError"DOMException。 -
如果 options.
uiMode为immediate且 interface 不支持immediate用户调解, 返回 一个 rejected 的 promise, 原因为 "TypeError"DOMException。 -
如果 settings 的 active credential types 包含 interface 的
[[type]], 返回 一个 rejected 的 promise, 原因为 "NotAllowedError"DOMException。 -
将 interface 的
[[type]]添加到 settings 的 active credential types。
-
-
令 origin 为 settings 的 origin。
-
令 sameOriginWithAncestors 为
true,如果 settings 与其 祖先同源,否则为false。 -
遍历 options 的 相关凭据接口对象 interface:
-
令 permission 为 interface 的
[[type]]Get Permissions Policy。 -
如果 permission 是 null,跳过。
-
如果 document 不被 允许使用 permission,则返回 一个 rejected 的 promise,原因为 "
NotAllowedError"DOMException。
-
-
令 p 为 新建的 promise。
-
在并行上下文中执行以下步骤:
-
令 credentials 为用 origin、options 和 sameOriginWithAncestors 从凭据存储收集 Credential 的结果。
-
若下列条件全部为真,则以 credentials[0] resolve p,并跳过后续步骤:
-
credentials 的 大小为 1
-
origin 不要求用户调解
-
options 是 先验可匹配
-
options.
mediation不为 "conditional"。
这也许不是最合适的模型。如果网站既支持用户名/密码又支持 webauthn,且前者用户不需要弹选择器而希望保持登录,该怎么处理?
-
-
如果 options 的
mediation为 "silent", resolve p 并传递null,并跳过余下步骤。 -
令 result 为 提示用户选择 Credential,参数为 options 和 credentials。
-
如果 result 是 接口对象:
-
用 origin、options 和 sameOriginWithAncestors 执行 result 的
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors), 并用其结果替换 result。如果该调用抛出 异常:
-
-
断言:result 为
null或Credential。 -
如果 result 是
Credential, 用 result resolve p。 -
如果 result 为
null且 options.mediation不为conditional, 用 result resolve p。注意: 若 options.
mediation为conditional且发现凭据为null,promise p 不被 resolve。
-
-
-
遍历 interfaces:
-
移除 interface 的
[[type]]从 settings 的 active credential types。
-
-
-
返回 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
,一旦对象被持久化到
凭据存储后即 resolve。
-
令 settings 为 当前设置对象。
-
断言:settings 是 安全上下文。
-
如果 settings 的 相关全局对象 的 关联 Document 非 完全激活, 返回 rejected 的 promise, 原因为 "
InvalidStateError"DOMException。 -
令 p 为 新建 promise。
-
如果 settings 的 活跃凭据类型 包含 credential 的
[[type]], 返回 rejected 的 promise, 原因为 "NotAllowedError"DOMException。 -
在并行环境中执行以下步骤:
-
React to p:
-
返回 p。
2.5.4. 创建
Credential
创建 Credential 算法
接收一个 CredentialCreationOptions
(options),并返回一个 Promise
:如果所提供参数可创建 Credential,
则 resolve 为该对象;否则 resolve 为 null。在异常情况下,
Promise
会 reject 合适的异常:
-
令 settings 为 当前设置对象。
-
断言:settings 是 安全上下文。
-
令 global 为 settings 的 全局对象。
-
令 document 为 相关全局对象 的 关联 Document。
-
如果 document 不是 完全激活,返回 rejected 的 promise, 原因为 "
InvalidStateError"DOMException。 -
若下列任一条件成立,返回 rejected 的 promise, 原因为
NotSupportedError: -
遍历 interfaces:
-
若 permission 为 null,跳过。
-
若 document 不被 允许使用 permission,返回 rejected 的 promise, 原因为 "
NotAllowedError"DOMException。
-
如果
options.被 中止, 返回 rejected 的 promise, 原因为signaloptions.的 中止原因。signal -
令 type 为 interfaces[0] 的
[[type]]。 -
若 settings 的 活跃凭据类型 包含 type, 返回 rejected 的 promise, 原因为 "
NotAllowedError"DOMException。 -
令 origin 为 settings 的 origin。
-
令 p 为 新建 promise。
-
在并行环境中执行以下步骤:
-
令 r 为执行 interfaces[0] 的
[[Create]](origin, options, sameOriginWithAncestors)内部方法后的结果,参数为 origin、options、sameOriginWithAncestors。如抛出 异常:
-
如果 r 是
Credential或null,则 resolve p 并传递 r,终止余下子步骤。 -
断言:r 是一个算法(如 § 2.2.1.4 [[Create]] 内部方法 中定义)。
-
排队一个任务到 global 的 DOM 操作任务源,执行下列子步骤:
-
resolve p,并传递 promise 调用 r(参数为 global)的结果。
-
-
-
React to p:
-
返回 p。
2.5.5. 防止静默访问
禁止静默访问算法
接收一个环境设置对象(settings),
返回一个 Promise
:一旦 prevent silent access
标志被持久化到 凭据存储 后即 resolve。
-
令 origin 为 settings 的 origin。
-
如果 settings 的 相关全局对象 的 关联 Document 不为 完全激活, 则返回 rejected 的 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); // 通知用户登录成功!执行所有需要登录的体验! // 或跳转到 /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 跨域凭据访问),
或允许用户临时创建新凭据。开发者可通过每次凭据被成功使用后(即便刚通过 get()
获取后)都调用
store()
,优雅处理这种不确定性:如果凭据尚未存储到该源,用户会有机会确认保存;如果已存储,用户则不会被提示。
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 标识 Provider。
protocol, 类型为 DOMString,只读,允许为 null-
凭据的联合身份提供者协议(例如 "
openidconnect")。如果该值为null, 可根据provider推断协议。 [[type]]-
FederatedCredential接口对象具有名为[[type]]的内部槽, 其值为 "federated"。 [[discovery]]-
FederatedCredential接口对象具有名为[[discovery]]的内部槽, 其值为 "credential store"。 FederatedCredential(data)-
该构造函数接受
FederatedCredentialInit(data),并执行下列步骤:-
令 r 为执行 “从 FederatedCredentialInit 创建 FederatedCredential” 的结果。如果抛出 异常,则重新抛出。
-
返回 r。
-
FederatedCredential
对象可以通过将 FederatedCredentialInit
字典
传递给 navigator.credentials.create()
来创建。
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 向 Web 暴露凭据信息可能会对用户隐私产生多种影响。因此,用户代理**必须**在多种场景下让用户参与,以确保用户明白发生了什么、他们的凭据将与谁共享。
我们称某个操作为用户调解,如果它是在获得用户明确同意后发生的。例如,同意可能通过用户在 凭据选择器界面上的直接交互来表达。通常,用户调解操作会涉及向用户展示某种界面,并请其做出决策。
未调解的(unmediated)操作则是在没有用户明确同意的情况下静默发生。例如,如果用户设置浏览器给予某个源持久的凭据访问权限,则可在不弹出决策界面的情况下提供凭据。
下面我们将阐述适用于所有凭据类型的一些通用要求,但请注意,用户代理具有较大自由度(它处于帮助用户的特权位置)。此外,特定凭据类型可能还有超出本文一般性要求的特殊规定。
5.1. 存储和更新凭证
凭据信息属于敏感数据,用户**必须**始终掌控这类信息的存储。无意间的凭据存储,例如,可能会让用户在某台设备上的本地资料与某个特定网络身份意外关联起来。为了降低此类意外风险:
-
未经用户调解,不应存储或更新凭据信息。 例如,用户代理可以在每次调用
store()时,弹出“是否保存此凭据?”对话框。如果用户代理希望提供持久授权(如“始终保存密码”选项),可推断用户已同意(不过我们建议用户代理把范围尽量收窄——比如“始终保存_生成的_密码”或“始终保存本站密码”)。
-
用户代理在存储凭据时应该通知用户,例如通过在地址栏或类似位置显示图标等形式。
-
用户代理**必须**允许用户手动移除已存储的凭据。该功能可作为设置页面提供,或通过如上所述的通知界面实现。
5.2. 强制用户介入
默认情况下,所有用户调解对
所有源都是必需的,因为 防止静默访问标志
在凭据存储中被设置为 true。用户可以选择为某个源授予凭据的持续访问权
(例如“保持登录本站”选项),此时会将该标志设为 false。这样,用户总会自动登录该站点,
从易用性和便捷性角度来看很理想,但也可能带来意外影响(例如,有的用户代理会在多设备间同步这个标志状态)。
为了避免这种意外风险:
-
用户代理必须允许用户为某一源或所有源要求用户调解。该功能可作为全局开关实现,覆盖每个源的 防止静默访问标志,让其都返回
false, 也可以为指定源(或特定源的具体凭据)做更细粒度的设置。 -
用户代理不得在没有用户调解的情况下,将某个源的防止静默访问标志 设为
false。例如,凭据选择器 (见§ 5.3 凭据选择)可带有一个复选框,允许用户勾选后为该源在无调解时开放凭据, 或用户代理可在其凭据管理器引导流程中让用户设置默认选项。 -
用户代理在为某个源提供凭据时必须通知用户,比如可以在地址栏或类似位置显示图标。
-
若用户清除某个源的浏览数据(如 cookies、localStorage 等),用户代理必须将该源的 防止静默访问标志 设为
true。
5.3. 凭证选择
当对某个需要
用户调解的来源调用 get()
时,用户代理**必须**征求用户同意后方可共享凭据信息。
这通常应以凭据选择器的形式呈现:向用户展示可用于该站点的凭据列表,
让他们选择要提供给网站的凭据,或者完全取消此次请求。
选择器 的用户界面应以某种方式易于与网站自身产生的界面区分。例如,选择器可以以用户代理界面重叠且不可伪造的方式显示。
选择器 的界面**必须**包含请求凭据的来源信息。
选择器
的界面应包含关联于请求凭据来源的所有 Credential
对象。
用户代理可以为每个 Credential
对象内部关联额外信息(超出本文档规定属性),
以提升选择器的实用性。例如,可以通过 Favicons 区分不同身份提供方等。但所有这种附加信息**不得**直接暴露给 Web。
选择器 的交互细节未在此规定:鼓励用户代理探索不同 UI 设计,以帮助用户理解认证选项,并引导用户流程做出凭据选择。 不过选择器的接口如下:
Credential,参数包括 CredentialRequestOptions
(options) 及从 凭据存储中发现的一组 Credential
对象(locally discovered
credentials)。
此算法返回值可以是 null(用户拒绝与站点共享凭据时),
也可以是用户所选的 Credential
对象,
或者如果用户选择了某类型凭据,则为相关 Credential
接口对象。
6. 安全注意事项
以下各节为各种安全和隐私注意事项的指导原则。不同凭证类型可能会强制执行更严格或更宽松的版本。
6.1. 跨域凭证访问
凭据属于敏感信息,用户代理在判断何时可以安全地与网站共享时必须格外谨慎。最安全的做法是仅允许凭据分享到其被保存时的确切来源(origin)。然而,这对于 Web 来说往往过于严格:比如功能被分在
example.com 和 admin.example.com 等子域名中的网站。
为在减少用户困扰和保障凭据安全之间取得平衡,用户代理:
-
不得在方案(scheme)降级的情况下在来源间共享凭据。也就是说,将
http://example.com/上保存的凭据提供给https://example.com/是合理的(以鼓励开发者迁移到安全传输),但反过来则危险。 -
可以利用公共后缀列表 [PSL] 通过比较凭据的 可注册域与
get()被调用的来源做比较,确定凭据的有效作用域。即:当从https://www.example.com/调用get()时,可以将https://admin.example.com/和https://example.com/上保存的凭据提供给用户,反之亦然。 -
当凭据的来源不是调用来源的精确匹配时,不得在没有用户调解的情况下, 响应
get()向该来源提供凭据。即,Credential对象在https://www.example.com上不会直接返回https://example.com的凭据,但可通过选择器界面供用户选择。
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生命周期内缓存。
这些图片**必须**使用如下 fetch 设置获取:credentials mode 设为 "omit",
service-workers mode 设为 "none",
client 设为 null,
initiator 设为空字符串,
destination 设为 "subresource"。
此外,如果用户代理允许用户更改凭据关联的名称或图标,则这种数据变动**不应**暴露给网站 (考虑某用户给同一来源的两个凭据命名为“我的小号”和“我的主号”等情况)。
7.3. 本地存储数据
该 API 允许某个 源将数据与用户的个人资料持久存储。由于大多数用户代理对凭证数据和“浏览数据”(如 cookie 等)处理方式不同,这可能导致用户在清除 cookie 时以为所有来源痕迹都被清除,实际仍有凭证数据保留而感到意外。
用户代理应提供 UI,清楚地向用户展示某个源已存储凭证数据,并应让用户易于移除这些数据。
8. 实现注意事项
本节为非规范内容。
8.1. 网站作者
此处应补充关于何时及如何使用该 API
的一些思考,尤其是关于 mediation
的相关内容。
[w3c/webappsec Issue #290]
说明通过
fetch() 携带 FormData
body 提交凭据时的编码限制。
为某一凭据类型做特性检测时,建议开发者验证相关 Credential
的具体实现是否存在,而不要只依赖
navigator.credentials 是否存在。后者只能判断 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 对象于 API。ExampleCredential的 internal 方法[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)被调用时,传入 origin(origin)、CredentialRequestOptions 对象(options)和布尔值(仅当调用上下文与祖先同源时为true)。 算法返回符合 options 的Credential对象集合。如无匹配Credential,返回空集合。-
断言:
options[example] 存在。 -
如果
options[example] 不为真,返回空集合。 -
遍历凭据存储中每个 credential:
-
...
-
-
-
定义
ExampleCredential的接口对象的[[discovery]]槽的值: -
用新凭据类型所需的选项扩展
CredentialRequestOptions, 以便能正确响应get(): -
用新凭据类型所需的数据扩展
CredentialCreationOptions, 以便可以创建响应create()的Credential对象: -
如果新凭据类型支持
conditional用户调解, 则定义ExampleCredential/isConditionalMediationAvailable()返回 resolved 的 promise,值为true。 -
按§ 2.1.2.1 注册项要求与更新流程中的流程, 为新 "example" 凭据类型 及其对应项加入凭据类型注册表:
-
CredentialCreationOptions、CredentialRequestOptions的 选项成员标识符(本例为 "example")以及,
注意:该凭据类型的 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。
循序渐进。
