RFC 10017 用于基于浏览器的应用程序的 OAuth 2.0 2026 年 8 月
Parecki 等人 当前最佳实践 [页]
文档流:
互联网工程任务组 (IETF)
RFC:
10017
BCP:
212
类别:
当前最佳实践
发布于:
ISSN:
2070-1721
作者:
A. Parecki
Okta
P. De Ryck
Pragmatic Web Security
D. Waite
Ping Identity

RFC 10017

用于基于浏览器的应用程序的 OAuth 2.0

摘要

本规范详细说明了在开发使用 OAuth 2.0 的基于浏览器的应用程序时必须考虑的威胁、攻击后果、安全注意事项、 以及最佳实践。

本备忘录的状态

本备忘录记录了一项互联网当前最佳实践。

本文档是互联网工程任务组 (IETF) 的成果。它代表 IETF 社区的共识。本文档已接受 公开审查,并经互联网工程指导委员会 (IESG) 批准发布。有关 BCP 的更多信息 可参阅 RFC 7841 第 2 节。

有关本文档当前状态、任何 勘误以及反馈方式的信息,可从以下地址获取: https://www.rfc-editor.org/info/rfc10017。

▲

目录

1. 引言

本规范描述了在浏览器中执行的应用程序里实现 OAuth 2.0 客户端的不同架构模式。它还概述了基于浏览器的应用程序面临的安全挑战,并分析了 不同模式如何帮助解决其中一些 挑战。

本文档重点讨论充当 OAuth 客户端的 JavaScript 前端应用程序 (定义于 第 1.1 节,见 [RFC6749]),它们与 授权服务器(第 1.1 节,见 [RFC6749])交互,以 获取访问令牌以及可选的刷新令牌。客户端使用访问令牌访问资源服务器上的受保护 资源(第 1.1 节,见 [RFC6749])。 使用 OAuth 时,客户端、授权服务器和资源服务器均被视为相互独立的 各方,无论它们是否由同一实体所有或运营。

请注意,许多 Web 应用程序由运行在同一 域上的前端和 API 组成,因此可以采用不依赖 OAuth 2.0 的架构。有关详细说明, 请参阅第 7.1 节。此类场景可以依赖 OpenID Connect [OpenID] 进行联合用户 身份验证,之后应用程序会维护用户的身份验证状态。此类场景 (仅将 OAuth 2.0 用作 OpenID Connect 的底层规范)不在 本规范的范围内。

对于使用 OAuth 2.0 和 OpenID Connect 的原生应用程序开发者,IETF 发布了一项 BCP (当前最佳实践),用于指导这些技术的集成。 该文档正式称为 [RFC8252] 或 BCP 212,但通常根据 由 OpenID 基金会赞助、帮助开发者采用 这些实践的一组库而称为 "AppAuth"。[RFC8252] 针对 如何在原生应用程序中安全实现 OAuth 客户端 提出了具体建议,包括在需要时整合其他 OAuth 扩展。

本规范 "用于基于浏览器的应用程序的 OAuth 2.0" 强调了 基于浏览器的应用程序的安全属性与原生 应用程序存在巨大差异,并阐述了将 OAuth 客户端实现为原生 应用程序与基于浏览器的应用程序之间的相似之处。除 OpenID Connect 提供额外注意事项的情况外,本文档主要关注 OAuth。

其中许多建议源自 OAuth 2.0 安全 当前最佳实践 [RFC9700],因为基于浏览器的应用程序 同样应遵循这些建议。 本文档扩展并进一步限制了 [RFC9700] 中给出的多项建议。

2. 符号约定

本文档中的关键词 "必须"、"不得"、 "必需"、"应"、"不应 "、"应该"、"不应该"、"建议"、"不建议"、 "可以" 和 "可选" 应按照 BCP 14 [RFC2119] [RFC8174] 中的说明进行解释,但仅限于它们 如本文所示全部以大写字母出现时。

3. 术语

本规范使用 OAuth 2.0 [RFC6749] 所定义的术语 "访问令牌"、"授权端点"、 "授权许可"、"授权服务器"、"客户端"、"客户端标识符"(客户端 ID)、"受保护 资源"、"刷新令牌"、"资源服务器" 和 "令牌端点"。本规范使用 [RFC6750] 所定义的 "持有者令牌"。

除所引用规范中定义的术语外,本文档还使用 以下术语:

OAuth:

在本文档中,"OAuth" 指 "OAuth 2.0" [RFC6749] [RFC6750]。

基于浏览器的应用程序:

一种动态下载并在 Web 浏览器中执行的应用程序, 通常使用 JavaScript 编写。有时也称为 "单页应用程序" 或 "SPA"。

本文档讨论基于浏览器的应用程序的安全性,这些应用程序由浏览器在 运行时环境中执行。在大多数场景中,这些应用程序都是运行在 JavaScript 执行环境中的 JavaScript 应用程序。鉴于此场景十分普遍,本文档 使用术语 "JavaScript" 指代允许代码在浏览器中的应用程序 运行时内执行的所有机制。本文档中的建议和注意事项并非 仅与 JavaScript 语言或其运行时相关,也适用于浏览器中的其他语言和 运行时环境,例如 WebAssembly (WASM) [W3C.wasm-core-2]。

PKCE:

代码交换证明密钥 [RFC7636]。一种 可防止针对 OAuth 授权码的多种攻击的机制。

DPoP:

所有权证明演示 [RFC9449]。一种 OAuth 2.0 机制,它将访问 令牌限制为仅可由获发该令牌的客户端使用。

CORS:

跨源资源共享 [Fetch]。一种允许对 浏览器同源策略作出例外处理的机制。

CSP:

内容安全策略 [W3C.CSP3]。一种限制特定 Web 页面可以获取或执行哪些资源的机制。

4. 基于浏览器的应用程序中 OAuth 2.0 的历史

最初在 [RFC6749] 和 [RFC6750] 中规定 OAuth 2.0 时,基于浏览器的 JavaScript 应用程序需要一种 严格遵守同源策略的解决方案。OAuth 2.0 的常见部署方式是让 应用程序运行在与授权服务器不同的域上,因此过去无法 使用授权码授权类型(第 4.1 节,见 [RFC6749]),因为这需要发送跨源 POST 请求。此限制是定义隐式授权(第 4.2 节,见 [RFC6749])的动机之一,隐式授权通过 URL 的 片段部分在前端通道中返回访问令牌,从而无需发送跨源 POST 请求。

然而,隐式授权存在多项缺点,通常涉及 访问令牌在 URL 中暴露所造成的漏洞。有关这些攻击以及 在浏览器中使用隐式授权的缺点的分析,请参阅第 7.2 节。其他攻击和安全 注意事项可参见 [RFC9700]。

在现代 Web 开发中,跨源资源共享 (CORS) 的广泛采用 [Fetch](允许对同源 策略作出例外处理)使基于浏览器的应用程序能够使用 OAuth 2.0 授权码流程,并向 令牌端点发送 POST 请求,以便用授权码交换访问令牌。由于 授权码授权类型允许使用刷新令牌,因此基于浏览器的 客户端也采用了此行为,即使这些客户端仍属于公共客户端(定义于 第 2.1 节,见 [RFC6749]),对安全 存储的访问能力有限甚至完全没有。此外,在流程中加入代码交换证明密钥 (PKCE) [RFC7636] 可防止授权码注入,并 确保即使授权码被截获,攻击者也无法使用该授权码。

基于此原因以及其他经验教训,当前针对 基于浏览器的应用程序的最佳实践是使用带有 PKCE 的 OAuth 2.0 授权码授权类型。部署基于浏览器的应用程序时 可以采用多种架构模式,其中既有包含相应服务端组件的模式,也有 不包含该组件的模式。每种架构都有其特定的权衡和 注意事项,本文档将在后文进一步讨论。第一方同域 应用程序还需考虑其他注意事项。

5. 恶意 JavaScript 的威胁

恶意 JavaScript 会给基于浏览器的应用程序带来重大风险。跨站脚本 (XSS) 或 远程代码文件遭入侵等攻击 向量使攻击者能够在应用程序的执行上下文中运行任意代码。此类恶意代码不会 以任何方式与主应用程序的代码隔离。因此,恶意代码不仅可以 控制正在运行的执行上下文,还可以在应用程序的源内执行操作。 具体而言,这意味着恶意代码可以从当前页面窃取数据、与其他 同源浏览上下文交互、从应用程序的源内向后端发送请求,以及从 基于源的存储机制(例如 localStorage、IndexedDB)中窃取数据等。

首先也是最重要的一点是,必须主动采取措施,从一开始就防止攻击者 获得立足点。这些措施包括但不限于以下内容:

更多建议可参见开放全球应用程序安全项目 (OWASP) 速查表系列 [OWASPCheatSheet]。

遗憾的是,历史表明,即使应用了这些安全准则,仍然存在 攻击者可能找到方法触发恶意 JavaScript 执行的风险。在 恶意 JavaScript 存在的情况下分析基于浏览器的应用程序的安全性时, 至关重要的是要认识到,恶意 JavaScript 代码与 合法应用程序代码具有相同权限。所有 JavaScript 应用程序都在一定 程度上面临此风险。

应用程序可能会获取 OAuth 令牌,这些令牌授予其运行 所必需的权限。综合来看,这实际上使 遭入侵的代码能够将该权限用于恶意目的。 尽管无法避免攻击者滥用权限的风险,但仍有 一些方法可以限制遭入侵的应用程序滥用该 权限的程度。例如,可以通过限制可能获取的令牌类型或将 令牌绑定到浏览器,把此类访问限制在应用程序处于活跃使用状态时。

只要合法应用程序代码可以访问变量或调用函数, 恶意 JavaScript 代码就可以执行完全相同的操作。此外,恶意 JavaScript 代码还可以篡改 应用程序的正常执行流程以及任何应用程序级防御措施, 因为这些措施通常由应用程序内部控制。例如,攻击者可以移除或 覆盖事件监听器、修改内置函数的行为(原型污染),以及阻止 框架中的页面加载。

恶意 JavaScript 对基于浏览器的应用程序的影响是一个受到广泛研究且 已被充分理解的主题。然而,恶意 JavaScript 对充当 OAuth 客户端的基于浏览器的 应用程序所产生的具体影响十分独特,因为恶意 JavaScript 现在可以影响 OAuth 流程中的交互。本节探讨恶意 JavaScript 代码对 承担 OAuth 客户端职责的基于浏览器的应用程序所造成的威胁。第 5.1 节讨论了攻击者在 找到运行恶意 JavaScript 代码的方法后可以使用的几种场景。这些场景清晰地展示了 攻击者的真正能力,其远远超出了简单的令牌外传。第 5.2 节分析了这些攻击场景对 OAuth 客户端的影响。

本规范的其余部分将回顾这些攻击场景及其 后果,以分析不同架构模式的安全属性。

5.1. 攻击场景

本节介绍攻击者在找到允许执行恶意 JavaScript 代码的漏洞后 可以实施的几种攻击场景。这些 攻击场景包括简单场景(第 5.1.1 节)和复杂场景(第 5.1.3 节)。请注意,此 列举并不详尽,其范围仅限于 OAuth 特有功能,并且不分 先后顺序。

5.1.1. 单次执行令牌窃取

单次执行令牌窃取场景涵盖一种简单的令牌 外传攻击,在该攻击中,攻击者获取并外传客户端当前的 令牌。此场景包括以下步骤:

  • 执行恶意 JavaScript 代码

  • 从应用程序首选的存储 机制中获取令牌(第 8 节)

  • 将令牌发送到攻击者控制的服务器

  • 存储或滥用被盗令牌

为降低访问令牌遭入侵所带来的风险,建议采用的防御策略 是缩小令牌的范围并缩短其生命周期。对于 刷新令牌,使用刷新令牌轮换(定义于 第 4.14.2 节,见 [RFC9700])可提供 检测和纠正机制。发送者约束令牌(第 9.2 节)为防范 访问令牌被盗提供了额外的保护层。

请注意,此攻击场景非常简单,并且经常用于 说明恶意 JavaScript 的危险。在讨论 基于浏览器的应用程序的安全性时,至关重要的是不要把攻击者的能力 限定为本场景所讨论的攻击。

5.1.2. 持久性令牌 窃取

持久性令牌窃取攻击场景是单次执行令牌窃取场景(第 5.1.1 节)的一种更高级 变体。攻击者不会在恶意代码执行时立即窃取 令牌,而是设置必要的处理程序,以持续窃取应用程序的令牌。 此场景包括 以下步骤:

  • 执行恶意 JavaScript 代码

  • 设置持续令牌窃取机制(例如,每 10 秒执行一次)

    • 从应用程序的 首选存储机制中获取令牌(第 8 节)

    • 将令牌发送到 攻击者控制的服务器

    • 存储令牌

  • 等待适当时机,滥用被盗令牌的最新 版本

此场景的关键区别在于,攻击者始终 能够访问应用程序所使用的最新令牌。攻击场景中的这一细微变化 已足以对抗针对令牌窃取的典型防御措施,例如 较短的生命周期或刷新令牌轮换。

对于访问令牌,只要用户的浏览器处于在线状态,攻击者就能获取 最新的访问令牌。刷新令牌轮换不足以 防止刷新令牌遭到滥用。攻击者可以轻易确保应用程序 不会使用最新的刷新令牌。例如,攻击者可以在窃取 应用程序的令牌后将其清除,等待用户关闭应用程序,或者 等待用户的浏览器离线。由于应用程序不会使用 最新的刷新令牌,因此不会出现可检测的刷新令牌重用,从而使 攻击者完全控制被盗的刷新令牌。

5.1.3. 获取 和提取新令牌

在高级的获取和提取新令牌攻击 场景中,攻击者会完全忽略应用程序已经 获取的任何令牌。相反,攻击者会利用运行与应用程序的 源相关联的恶意代码的能力。凭借这种能力,攻击者可以 注入隐藏的 iframe 并启动静默授权码流程。此静默流程 将重用用户与授权服务器之间的现有会话,并导致 签发新的独立访问令牌(以及可选的刷新令牌)。此场景 包括以下步骤:

  • 执行恶意 JavaScript 代码

  • 设置处理程序,以从 iframe 获取授权码(例如,通过监控框架的 URL 或使用 Web 消息传递 [WebMessaging])

  • 在页面中插入隐藏的 iframe,并使用 授权请求对其进行初始化。iframe 中的授权请求将在 用户的会话内发生,并且如果会话仍然有效,将导致 签发授权码。请注意,此步骤依赖于 授权服务器支持基于框架的静默流程,如本场景的 最后一段所述。

  • 使用先前安装的处理程序从 iframe 中提取 授权码

  • 将授权码发送到 攻击者控制的服务器

  • 用授权码交换一组新的 令牌

  • 滥用被盗令牌

此场景最重要的结论是,它会运行一个新的 OAuth 流程,而不是专注于窃取现有令牌。实质上,即使 应用程序找到了能够将存储的令牌与 攻击者完全隔离的令牌存储机制,攻击者仍然能够请求一组新的 令牌。请注意,由于攻击者控制着浏览器中的应用程序, 攻击者的授权码流程与合法的授权 码流程无法区分。

由于基于浏览器的公共 OAuth 客户端的安全性完全 依赖重定向 URI 和应用程序的 源,因此此攻击场景是可行的。当攻击者在应用程序的 源中执行恶意 JavaScript 代码时,便能检查同源框架。因此, 在主执行上下文中运行的攻击者代码可以检查 同源框架中加载的重定向 URI,以提取授权码。

前端应用程序没有能够对抗此攻击场景的实用 安全机制。较短的访问令牌生命周期和刷新令牌 轮换均无效,因为攻击者拥有一组全新的独立令牌。 DPoP [RFC9449] 等高级安全机制同样无效,因为 攻击者可以使用自己的密钥对,为新获取的 令牌设置并使用 DPoP。要求用户与每次授权码流程进行交互,可以有效 阻止自动静默签发新令牌,但这会严重影响 已广泛采用的模式,例如在首次加载页面时引导应用程序 或在多个相关应用程序之间进行单点登录,因此并不是一项实用的 措施。

5.1.4. 通过用户的浏览器代理 请求

此攻击场景涉及攻击者直接从用户浏览器中运行的 OAuth 客户端应用程序内向 OAuth 资源服务器发送请求。 在此场景中,攻击者不需要滥用 应用程序来获取令牌,因为浏览器会在请求中包含自己的 Cookie 或令牌。 攻击者发送到资源服务器的请求与 合法应用程序发送的请求无法区分,因为攻击者 与合法应用程序在同一上下文中运行代码。此场景 包括以下步骤:

  • 执行恶意 JavaScript 代码

  • 向资源服务器发送请求并处理 响应

为了授权发送到资源服务器的请求,攻击者 只需模仿客户端应用程序的行为。例如,当客户端 应用程序以编程方式将访问令牌附加到传出请求时,攻击者 也会这样做。如果客户端应用程序依赖外部组件为 请求添加适当的访问令牌,那么该外部组件同样会为 攻击者的请求添加该令牌。

此攻击模式广为人知,也会出现在使用 HttpOnly 会话 Cookie 的服务端 Web 应用程序中。普遍认为 应用程序级安全措施无法阻止或防止 此场景。例如,DPoP [RFC9449] 明确认为此攻击场景 不在其范围内。

5.2. 攻击后果

成功实施攻击场景可能导致访问 令牌和刷新令牌被盗,或者使攻击者能够劫持在 用户浏览器中运行的客户端应用程序。这些后果都与基于浏览器的 OAuth 客户端相关。下文将 按严重程度从高到低依次讨论这些后果。

5.2.1. 利用 被盗的刷新令牌

当攻击者从基于浏览器的 OAuth 客户端获取有效的刷新令牌时, 可以通过与授权服务器运行刷新令牌授权来滥用该刷新令牌。 刷新令牌授权的响应中包含访问令牌, 这使攻击者能够访问受保护的资源(参见第 5.2.2 节)。实质上, 滥用被盗的刷新令牌可使攻击者在向资源服务器发送请求时长期冒充合法的 客户端应用程序。

只有当授权服务器因刷新令牌已过期或已轮换而拒绝该 刷新令牌,或者刷新令牌被撤销时,攻击才会停止。在 典型的基于浏览器的 OAuth 客户端中,刷新令牌保持 数小时甚至数天有效并不罕见。

5.2.2. 利用 被盗的访问令牌

如果攻击者获取有效的访问令牌,便能 在发送给资源服务器的请求中冒充合法的客户端应用程序。 具体而言,持有访问令牌使攻击者能够向 接受该有效访问令牌的任何资源服务器发送任意请求。实质上,滥用被盗的 访问令牌可使攻击者在资源服务器面前短期冒充合法的客户端应用程序。

当访问令牌过期或授权服务器撤销 该令牌时,攻击就会结束。在典型的基于浏览器的 OAuth 客户端中,访问 令牌的生命周期可能很短,介于数分钟到数小时之间。

请注意,持有访问令牌后,攻击者可以不受限制地 使用该令牌。攻击者可以使用任意 HTTP 方法、目标 URL、 标头值或正文,向资源服务器发送任意请求。

应用程序可以使用 DPoP,确保其访问令牌绑定到 由浏览器持有的不可导出密钥。在这种情况下,攻击者 滥用被盗访问令牌的难度将显著增加。更具体地说,使用 DPoP 时, 攻击者只能通过实施在线攻击来滥用被盗的应用程序令牌,在该攻击中, 证明是在用户的浏览器中计算的。第 11.4 节,见 [RFC9449] 对此攻击进行了详细说明。然而, 当攻击者获取新的访问令牌(以及可选的刷新令牌)时,如 第 5.1.3 节所述, 可以使用攻击者控制的密钥对为这些令牌设置 DPoP。在这种 情况下,攻击者又可以不受 限制地滥用这个新获取的访问令牌。

5.2.3. 客户端劫持

当窃取令牌不可行或不符合攻击者的目的时,攻击者还可以 选择劫持在用户浏览器中运行的 OAuth 客户端应用程序。这 实际上使攻击者能够执行合法客户端 应用程序可以执行的任何操作。例如,检查页面上的数据、修改 页面以及向后端系统发送请求。此外,攻击者还可以 滥用其对应用程序的访问权限发起其他攻击,例如通过会话固定等攻击 诱骗客户端代表攻击者执行操作 [SessionFixation]。

请注意,客户端劫持不如直接滥用 被盗的用户令牌强大。在客户端劫持场景中,攻击者无法直接控制 令牌,并且会受到客户端应用程序所执行的安全策略 限制。例如,可以为运行在 admin.example.org 上的资源服务器 配置 CORS 策略,拒绝来自运行在 web.example.org 上的客户端的请求。即使客户端使用的访问令牌 会被资源服务器接受,资源服务器严格的 CORS 配置也 不允许此类请求。不采用此类严格 CORS 策略的资源服务器仍然 可能遭到来自已入侵客户端应用程序的恶意请求。

6. 应用程序架构模式

构建依赖 OAuth 访问受保护资源的基于浏览器的 应用程序时,可以采用三种主要架构模式:

这些架构模式分别在安全性与 简单性之间提供不同的权衡。本节按安全性从高到低的顺序介绍这些模式。

6.1. 面向前端的后端 (BFF)

本节描述一种基于浏览器的应用程序架构,该应用程序 依赖后端组件处理所有 OAuth 职责和 API 交互。BFF 具有三项核心职责:

  1. BFF 作为机密 OAuth 客户端与授权服务器交互(定义于 第 2.1 节,见 [RFC6749])。

  2. BFF 在基于 Cookie 的会话上下文中管理 OAuth 访问令牌和刷新令牌, 避免将任何令牌直接暴露给基于浏览器的 应用程序。

  3. BFF 将所有请求转发到资源服务器,并在转发前 为请求附加正确的访问令牌。

在此架构中,BFF 作为服务端组件运行,但它是 前端应用程序的一个组件。为避免与 API 网关和反向代理等其他架构概念 混淆,必须牢记 BFF 会成为 前端应用程序的 OAuth 客户端。

如果攻击者能够在基于浏览器的 应用程序中执行恶意代码,此应用程序架构可以抵御前文讨论的大多数攻击场景。 由于令牌仅供 BFF 使用,因此浏览器中没有可供 提取的令牌(参见 "单次执行令牌窃取"(第 5.1.1 节)和 "持久性令牌窃取"(第 5.1.2 节))。BFF 是 机密客户端,这可以防止攻击者在浏览器中运行新流程 (参见 "获取和提取新令牌"(第 5.1.3 节))。由于恶意的浏览器端代码仍然 在应用程序的源内运行,攻击者能够从 用户的浏览器内向 BFF 发送请求(参见 "通过用户的浏览器代理请求"(第 5.1.4 节))。请注意,使用 HttpOnly Cookie 可防止攻击者直接访问会话状态, 从而防止攻击从客户端劫持升级为会话劫持。

6.1.1. 应用程序 架构

授权 令牌 资源 端点 端点 服务器 (F) (K) 面向 前端的 后端 (BFF) (D) (B,I) (C) (E) (G) (J) (L) (A,H) 静态 Web 主机 浏览器
图 1: OAuth 2.0 BFF 模式

在此架构中,浏览器代码(通常为 JavaScript)首先 从静态 Web 主机加载到浏览器中 (A),随后应用程序在 浏览器中运行。应用程序通过调用 "检查会话" API 端点 (B) 与 BFF 核对是否存在活动 会话。如果找到活动会话, 应用程序将恢复其已验证身份的状态,并直接跳至 (J)。

未找到活动会话时,基于浏览器的应用程序 会触发到 BFF 的导航 (C),以启动带有 PKCE 扩展的授权码流程(在第 6.1.3.1 节中说明),BFF 通过将浏览器重定向到 授权端点 (D) 作出响应。当用户被重定向回来时,浏览器将 授权码传递给 BFF (E),随后 BFF 可以在 令牌端点 (F) 使用其客户端凭据和 PKCE 代码验证器交换令牌。

BFF 将获取的令牌与用户的会话相关联(参见 第 6.1.2.3 节),并在 响应中设置 Cookie 以跟踪此会话 (G)。此时, 基于重定向的授权码流程已经完成,因此 BFF 可以将控制权 交还给前端应用程序。BFF 通过在响应 (G) 中包含重定向来实现这一点,从而触发浏览器从服务器获取前端 (H)。请注意,步骤 (H) 与步骤 (A) 相同,这很可能意味着所请求的资源可以 从浏览器缓存中加载。前端加载后,将与 BFF 核对是否存在 现有会话 (I),从而允许应用程序恢复其已验证身份的状态。

当浏览器中的应用程序想要向 资源服务器发出请求时,它会将请求发送到 BFF 上的相应端点 (J)。此 请求将包含在 (G) 中设置的 Cookie,使 BFF 能够获取适用于 此用户会话的令牌。BFF 从请求中移除 Cookie,并将 用户的访问令牌附加到请求,然后将其转发到实际的资源服务器 (K)。 随后,BFF 将响应转发回基于浏览器的应用程序 (L)。

6.1.2. 实现 细节

6.1.2.1. 会话和 OAuth 端点

BFF 提供一组对于 实现基于浏览器的应用程序与 BFF 之间的交互至关重要的端点。 本节将更详细地讨论这些端点,以阐明其用途 和使用场景。

"检查会话" 端点(图 1中的 (B) 和 (I))是由 基于浏览器的应用程序调用的 API 端点。会话信息可用时,请求会携带 该信息,使 BFF 能够检查活动 会话。响应应该向基于浏览器的应用程序表明 会话是否处于活动状态。此外,BFF 还可以包含其他信息,例如 已验证身份的用户的身份信息。

启动授权码流程的端点 (C) 由浏览器通过导航进行访问。当应用程序 在检查会话 (B) 后检测到未验证身份的状态时,可以将 浏览器导航至此端点。这样,BFF 就可以使用 重定向作出响应,将浏览器带到授权服务器。支持 在 Web 服务器上运行的机密客户端使用 OAuth 2.0 的库,通常会将启动此流程的 端点作为 "登录" 端点提供。请注意, BFF 也可以在检测到不存在活动会话时, 通过 (B) 启动授权码流程。在这种情况下,BFF 会在响应中返回授权 URI,并要求应用程序 使用此 URI 触发导航事件。然而,此场景需要 自定义实现,并使标准 OAuth 库更难使用。

接收授权码的端点 (E) 由浏览器内的导航事件调用。此时, 应用程序尚未加载,无法处理重定向。与 启动流程类似,处理重定向的端点由 标准 OAuth 库提供。BFF 可以使用重定向响应此请求, 从而触发浏览器加载应用程序。

最后,BFF 还可以向 应用程序提供 "注销" 端点,此端点未在图 1中描绘。注销 端点的确切行为取决于应用程序要求。请注意,标准 OAuth 库通常也会提供 "注销" 端点的实现。

6.1.2.2. 刷新令牌

按照 第 4.14 节,见 [RFC9700]中的说明使用刷新令牌时,BFF 会获取刷新令牌 (F) 并将其与用户的会话相关联。

如果 BFF 发现用户的访问令牌已经 过期并且 BFF 拥有刷新令牌,则可以使用刷新令牌获取 新的访问令牌。由于 BFF OAuth 客户端是机密客户端,它 将对刷新令牌请求使用客户端身份验证。通常,BFF 在处理来自前端的 API 调用时以内联方式执行这些步骤。在这种 情况下,这些未在图 1中明确显示的步骤,将 发生在 (J) 与 (K) 之间。在服务端保留所有令牌信息的 BFF 也可以在观察到令牌过期事件时请求新的访问令牌, 以提高 API 请求的性能。

刷新令牌过期后,如果不运行全新的授权码流程, 就无法获取有效的访问令牌。 因此,将 BFF 管理的基于 Cookie 的会话的生命周期 配置为与刷新令牌的最长生命周期相等是合理的。 此外,当 BFF 获悉活动会话的刷新令牌 不再有效时,使该会话失效也是合理的。

BFF 依赖浏览器 Cookie [RFC6265bis] 跟踪用户的 会话,该会话用于访问用户的令牌。服务端和客户端的 基于 Cookie 的会话都存在一些缺点。

服务端会话仅公开会话标识符,并将 所有数据保留在服务器上。这样做可以确保对 活动会话具有很高的控制程度,并且能够随时撤销任何会话。此 方法的缺点是会影响可扩展性,需要采用 "粘性会话" 或 "会话复制" 等解决方案。鉴于这些缺点,仅建议 在小规模场景中将服务端会话与 BFF 配合使用。

客户端会话会把所有数据放入浏览器中的 已签名且可以选择加密的对象内。此模式使服务器无需 跟踪任何会话数据,但会严重限制对活动 会话的控制,并使会话撤销难以处理。然而,当 客户端会话用于 BFF 上下文时,这些属性会发生 显著变化。由于基于 Cookie 的会话仅用于获取用户的 令牌,所有控制和撤销属性都源自访问 令牌和刷新令牌的使用。只需撤销用户的访问令牌和/或 刷新令牌即可阻止对受保护资源的持续访问,而无需 明确使基于 Cookie 的会话失效。

保护会话 Cookie 的最佳实践在 第 6.1.3.2 节中讨论。

6.1.2.4. 结合 OAuth 和 OpenID Connect

此应用程序架构所使用的 OAuth 流程可以 通过在授权请求 (C) 中包含必要的 OpenID Connect 范围 与 OpenID Connect 结合使用(至少包含 [OpenID] 第 3.1.2.1 节所定义的 openid 范围)。在这种情况下,BFF 将 在 (F) 中收到 ID 令牌。BFF 可以将 ID 令牌中的信息与用户的会话相关联,并在 (B) 或 (I) 中将其提供给应用程序。

需要时,BFF 可以使用与 用户会话相关联的访问令牌向 UserInfo 端点发出请求。

6.1.2.5. 实用部署策略

提供静态 JavaScript 代码与处理 OAuth 令牌和转发请求是相互独立的 职责。在图 1中, BFF 和静态 Web 主机显示为两个独立实体。在实际 部署中,这些组件可以部署为单个服务(即,由 BFF 提供静态 JavaScript 代码)、两个独立服务(即,内容 分发网络 (CDN) 和 BFF),或者单个服务中的两个组件 (即,云平台上的静态托管和无服务器函数)。

请注意,可以进一步自定义此 架构以支持特定场景。例如,同时依赖 内部和外部资源服务器的应用程序可以选择将内部 资源服务器与 BFF 托管在一起。在该场景中,对内部 资源服务器的请求直接在 BFF 上处理,无需通过网络 转发请求。从资源 服务器的角度来看,授权并未发生变化,因为用户的会话会在内部转换为 访问令牌及其声明。

6.1.3. 安全 注意事项

6.1.3.1. 授权码授权

使用 BFF 的主要好处是 BFF 能够 充当机密客户端。因此,BFF 必须 通过与授权服务器建立凭据来充当机密客户端。 此外,BFF 必须按照 第 2.1.1 节,见 [RFC9700]中的说明使用 OAuth 2.0 授权码授权来发起访问令牌 请求。

6.1.3.3. 跨站请求伪造防护

基于浏览器的应用程序与 BFF 之间的交互依赖 Cookie 进行身份验证和授权。与其他 基于 Cookie 的交互类似,BFF 必须考虑跨站请求 伪造 (CSRF) 攻击。成功的 CSRF 攻击可能使攻击者 发送到 BFF 的请求触发对受保护资源的传出调用。

BFF 必须实现适当的 CSRF 防御。具体采用哪种机制或机制组合取决于 BFF 部署所在的具体域,如下文所述。

6.1.3.3.2. 跨源资源共享

BFF 可以依赖 CORS 作为 CSRF 防御 机制。CORS 是由浏览器实现的一种安全机制,它会 限制跨源请求,除非服务器通过设置适当的 CORS 标头明确批准 此类请求。

浏览器通常会限制由脚本发起的跨源 HTTP 请求。如果目标服务器批准请求,CORS 可以解除此限制, 该批准会通过初始 "预检" 请求进行检查。除非预检响应明确批准 该请求,否则浏览器将拒绝发送完整请求。

由于此属性,BFF 可以依赖 CORS 作为 CSRF 防御。当攻击者尝试从用户的浏览器向 BFF 发起跨源 请求时,BFF 不会在 预检响应中批准该请求,导致浏览器阻止 实际请求。请注意,攻击者始终可以从自己的计算机 发起请求,但此时请求不会携带用户的 Cookie,因此攻击将会失败。

依赖 CORS 作为 CSRF 防御时, 必须认识到,某些请求无需 预检即可发出。对于被称为 "CORS 安全列出的请求" 的此类请求, 浏览器会直接发送请求;如果服务器未发送适当的 CORS 标头, 则阻止对响应的访问。对于可以通过 JavaScript 以外的其他方式触发的请求, 例如 GET 请求或基于表单的 POST 请求,浏览器会强制执行此行为。

此行为会导致资源服务器的某些 端点即使启用了 CORS 作为防御措施,也可能容易遭受 CSRF 攻击。 例如,如果资源服务器是一个公开 无正文 POST 请求端点的 API,就不会发出 预检请求,也不会有 CSRF 防御。

为了避免以这种方式绕过 CORS 策略, BFF 应该要求基于浏览器的 应用程序包含自定义请求标头。带有 自定义请求标头的跨源请求始终需要预检,这使 CORS 成为 有效的 CSRF 防御。使用此机制时,BFF 必须确保每个传入请求都携带 此静态标头。该标头的具体名称由 应用程序和 BFF 决定。示例配置可以是具有 静态值的请求标头,例如 My-Static-Header: 1。

还可以将基于浏览器的 应用程序部署在与 BFF 相同的源上。这可以确保前端 与 BFF 之间的合法交互不需要任何 预检,因此不会产生额外开销。

6.1.3.3.3. 使用防伪/双重提交 Cookie

某些技术栈和框架 内置了使用防伪 Cookie 的 CSRF 防护。此机制 依赖存储在 Cookie 中的会话专用秘密值, 只有在与该 Cookie 关联的域中运行的合法前端 才能读取它。前端应读取该 Cookie 并将其值插入请求,通常是通过添加自定义 请求标头。后端会将 Cookie 中的值与 前端提供的值进行核对,以识别合法请求。如果 针对所有会改变状态的请求正确实现,此机制 可以有效缓解 CSRF。

请注意,不一定 优先推荐此机制而不是 CORS 方法。然而,如果框架 内置了对此机制的支持,它可以作为一种低成本 的 CSRF 防护替代方案。

6.1.3.4. BFF 架构中的隐私注意事项

BFF 模式要求基于浏览器的 应用程序通过后端 BFF 组件将所有请求转发到资源服务器。因此,BFF 组件能够观察 应用程序与资源服务器之间的所有请求 和响应,这可能会对隐私产生 相当大的影响。

当基于浏览器的应用程序和 BFF 由 同一方构建和部署时,对隐私的影响可能很小。然而,当 此模式使用由第三方提供或托管的 BFF 组件 实现时,就需要考虑这种隐私影响。

6.1.3.5. 运维注意事项

由于 BFF 代表前端将所有请求转发到资源 服务器,因此应该确保资源 服务器了解此组件,并针对速率限制 和其他反滥用措施采用适当的策略。例如,如果 BFF 被部署为 单实例服务,而资源服务器根据 IP 地址限制请求速率,由于许多用户的 浏览器看起来都来自 BFF 的单一 IP 地址,服务器可能开始阻止请求。

6.1.3.6. 代理 限制

BFF 通过接受来自前端的请求 并将其转发到资源服务器来充当代理服务。传入请求 携带 Cookie,BFF 会在传出请求中将其转换为访问令牌。 (请注意,这使其更像应用层反向代理, 而不是 HTTP 代理。)除 CSRF 攻击外,攻击者还可能尝试 操纵 BFF,使其将请求转发到非预期主机。如果攻击者 成功利用此问题,就可能将 BFF 重定向到任意服务器, 从而可能暴露用户的访问令牌。

为了缓解此风险,BFF 必须通过在转发请求前 验证目标主机,对传出请求实施严格控制。这要求 维护获批准资源服务器的明确允许列表,以确保 请求仅代理到预定义的后端(例如, /bff/orders/create 仅映射到 https://order-api.example.com/create)。如果需要基于 路径(例如 /bff/orders/{id})进行动态路由,BFF 必须应用严格验证,以确保只能 访问获授权的目标。此外,按端点限制允许的 HTTP 方法可以进一步减少攻击向量。

实现可动态配置的代理时, BFF 必须确保其仅允许向 明确获准的主机和路径发出请求。未强制实施这些限制可能 导致未经授权的访问和访问令牌泄漏。

6.1.3.7. 高级 安全性

在 BFF 模式中,所有 OAuth 职责都已 转移到作为机密客户端的服务端组件 BFF。由于 服务端应用程序运行在比基于浏览器的 应用程序更可控的环境中,因此更容易采用高级 OAuth 安全实践。 示例包括基于密钥的客户端身份验证和发送者约束 令牌。

6.1.4. 威胁分析

本节重新审视第 5 节中的攻击场景和后果,并讨论可能的 其他防御措施。

6.1.4.1. 攻击 场景和后果

如果攻击者能够在应用程序的执行上下文中执行恶意代码 (例如 JavaScript 或 WASM),则以下 攻击场景与此相关:

请注意,此攻击场景会导致以下 后果:

请注意,客户端劫持是基于浏览器的应用程序 本质上固有的攻击场景。因此,除了首先阻止恶意 代码执行外,没有任何措施能够 防止此类攻击。有助于实现这一目标的技术包括遵循 安全编码准则、进行代码分析,以及部署内容安全策略 [W3C.CSP3] 等纵深防御 机制。

在此架构中,BFF 是处理 各种安全特定职责和基于代理的行为的关键组件。尽管讨论 基于代理的应用程序的安全实现不在本文档的范围内, 但必须注意,BFF 中的安全漏洞 可能会对应用程序产生重大影响。

最后,BFF 处于能够观察 基于浏览器的应用程序与资源服务器之间所有流量的独特位置。如果 高安全性应用程序希望实施异常检测或速率 限制,BFF 将是实施这些措施的理想位置。此类限制可以 进一步帮助缓解客户端劫持的后果。

6.1.4.2. 已缓解的攻击场景

BFF 应用程序架构可以有效 缓解下面列出的其他攻击场景:

BFF 通过不向基于浏览器的应用程序 暴露任何令牌来对抗前两个攻击场景。即使攻击者 完全控制了应用程序,也根本没有可供 窃取的令牌。

在第三个场景中,攻击者通过运行静默流程获取新的 访问令牌(以及可选的刷新令牌)。将 BFF 设置为机密客户端可以 缓解此攻击。即使攻击者 成功获取授权码,也会因缺少客户端凭据而无法交换该 授权码。此外,使用 PKCE 可防止针对授权码的其他攻击。

由于刷新令牌和访问令牌由 BFF 管理, 而且不会暴露给浏览器,因此潜在攻击造成的以下两种后果 不再相关:

6.1.4.3. 总结

BFF 架构比仅使用浏览器的应用程序 复杂得多。它需要部署和运维 服务端 BFF 组件。此外,此模式要求 应用程序与资源服务器之间的所有交互均由 BFF 代理。根据部署模式,这种代理行为可能会给 服务端组件带来沉重负担。如果 BFF 充当资源 服务器,请参阅第 6.2.2.6 节中的其他说明。

然而,由于 BFF 架构 模式的性质,它可以提供有力的安全保障。使用 BFF 还能确保 应用程序的攻击面不会因使用 OAuth 而增加。唯一可行的 攻击模式是劫持用户浏览器中的客户端应用程序——这是 Web 应用程序固有的问题。

强烈建议业务 应用程序、敏感应用程序以及处理个人 数据的应用程序采用此架构。

6.2. 令牌中介 后端

本节描述一种基于浏览器的应用程序架构,该应用程序 依赖后端组件作为机密客户端处理获取令牌所涉及的 OAuth 职责(定义于 第 2.1 节, 见 [RFC6749])。随后,后端组件 向应用程序提供访问令牌,使其能够直接与资源服务器交互。

令牌中介后端模式比 BFF 模式更加轻量 (参见第 6.1 节),因为它不需要 代理应用程序与资源 服务器之间的所有请求和响应。从安全角度来看,令牌中介后端不如 BFF 安全, 但与直接在 浏览器中运行 OAuth 客户端应用程序相比,仍具有显著优势。

如果攻击者能够在应用程序中执行恶意代码,此 应用程序架构可以防止攻击者滥用刷新令牌(参见 "单次执行令牌窃取"(第 5.1.1 节)和 "持久性令牌窃取"(第 5.1.2 节))或 获取一组新令牌(参见 "获取和提取新令牌"(第 5.1.3 节))。然而,由于 访问令牌直接暴露给应用程序,攻击者可以从 客户端存储中窃取该令牌(参见 "单次执行令牌窃取"(第 5.1.1 节)和 "持久性令牌窃取"(第 5.1.2 节)),或者向 令牌中介后端请求新令牌(参见 "通过用户的浏览器代理请求" (第 5.1.4 节))。请注意,使用 HttpOnly Cookie 可防止攻击者直接访问会话状态, 从而防止攻击从访问令牌窃取升级为会话劫持。

6.2.1. 应用程序 架构

授权 令牌 资源 端点 端点 服务器 (F) 令牌中介 后端 (J) (D) (B,I) (C) (E) (G) (A,H) 静态 Web 主机 浏览器
图 2: OAuth 2.0 令牌中介后端模式

在此架构中,基于浏览器的代码(例如 JavaScript 或 WASM)首先从静态 Web 主机加载到浏览器中 (A),随后应用程序 在浏览器中运行。应用程序与令牌中介后端核对 是否存在活动会话 (B)。如果找到活动会话,应用程序将收到 相应的访问令牌,恢复其已验证身份的状态,并直接跳至 (J)。

未找到活动会话时,应用程序会触发 到令牌中介后端的导航 (C),以启动 带有 PKCE 扩展的授权码流程(在第 6.2.3.1 节中说明),令牌中介 后端通过将浏览器重定向到授权端点 (D) 作出响应。当 用户被重定向回来时,浏览器将授权码传递给 令牌中介后端 (E),随后令牌中介后端可以在 令牌端点 (F) 使用其客户端凭据和 PKCE 代码验证器交换令牌。

令牌中介后端将获取的令牌与 用户的会话相关联(参见第 6.2.2.4 节),并在响应中设置 Cookie 以跟踪此会话 (G)。 发送给浏览器的此响应还会触发应用程序重新加载 (H)。 应用程序重新加载后,将与令牌中介后端核对是否存在 现有会话 (I),从而允许应用程序恢复其已验证身份的状态,并 从令牌中介后端获取访问令牌。

浏览器中的应用程序可以使用在 (I) 中获取的访问令牌直接向资源服务器发出请求 (J)。

6.2.2. 实现 细节

6.2.2.1. 会话和 OAuth 端点

令牌中介后端的大多数端点实现 都与为 BFF 描述的端点类似。

  • "检查会话" 端点(图 2中的 (B) 和 (I))是由基于浏览器的应用程序调用的 API 端点。 会话信息可用时,请求会携带 该信息,使后端能够检查活动会话。响应应该向 基于浏览器的应用程序表明会话是否处于活动状态。如果 找到活动会话,后端会在 响应中包含访问令牌。此外,后端还可以包含其他信息,例如 已验证身份的用户的身份信息。

  • 启动授权 码流程的端点 (C) 与为 BFF 架构描述的端点相同。有关详细信息,请参阅第 6.1.2.1 节。

  • 接收授权 码的端点 (E) 与为 BFF 架构描述的端点相同。有关详细信息,请参阅第 6.1.2.1 节。

  • 支持注销的端点与 为 BFF 架构描述的端点相同。有关详细信息,请参阅第 6.1.2.1 节。

6.2.2.2. 刷新 令牌

按照 第 4.14 节,见 [RFC9700]中的说明使用刷新令牌时,令牌中介后端 会在 (F) 中获取刷新令牌,并将其与用户的会话相关联。

如果资源服务器拒绝访问令牌, 应用程序可以联系令牌中介后端,请求新的访问 令牌。令牌中介后端依赖与此 请求关联的 Cookie 查找用户的刷新令牌,并使用 刷新令牌发出令牌请求。这些步骤未在图 2中显示。请注意,此刷新令牌 请求来自作为机密客户端的后端,因此需要进行客户端 身份验证。

刷新令牌过期后,如果不启动全新的授权码授权, 就无法获取有效的访问令牌。 因此,如果预先知道此类信息,将基于 Cookie 的会话的生命周期 配置为与刷新令牌的最长生命周期相等是合理的。 此外,当令牌中介后端获悉 活动会话的刷新令牌不再有效时,使该 会话失效也是合理的。

6.2.2.3. 访问令牌 范围

根据所访问的资源服务器以及 授权服务器上的范围配置,应用程序可能希望 请求具有不同范围配置的访问令牌。此行为将 使应用程序能够遵循使用最小范围 访问令牌的最佳实践。

应用程序可以在检查活动会话((A) 和 (I))时, 将所需范围告知令牌中介后端。由 令牌中介后端决定先前获取的访问令牌 是否符合所需的范围标准。

应该注意,如果不加区别地应用,令牌中介后端的 此访问令牌缓存机制可能造成范围提升风险。 如果缓存的访问令牌所具有的范围是 前端所请求范围的超集,令牌中介后端不应该将其返回给前端;相反,它应该使用刷新令牌向授权服务器请求 具有更小范围集合的访问令牌。请注意,是否支持 此类访问令牌范围缩减机制由 授权服务器决定。

依赖 [RFC8707] 中所述扩展为 特定资源服务器获取访问令牌时,令牌中介后端可以使用与 范围缩减机制类似的机制。

与 BFF 类似,令牌中介后端依赖 浏览器 Cookie 跟踪用户的会话。基于 Cookie 的会话的实现 准则和安全注意事项与 BFF 相同,如第 6.1.2.3 节所述。

6.2.2.5. 结合 OAuth 和 OpenID Connect

与 BFF 类似,令牌中介后端可以选择 在单个流程中结合 OAuth 和 OpenID Connect。有关 详细信息,请参阅第 6.1.2.4 节。

6.2.2.6. 实用部署场景

托管静态 JavaScript 或 WASM 代码与处理 同授权服务器的交互是相互独立的职责。在图 2中, 令牌中介后端和静态 Web 主机显示为两个独立实体。 在实际部署场景中,这些组件可以部署为单个 服务(即,由令牌中介后端提供静态代码)、两个 独立服务(即,CDN 和令牌中介后端),或者单个服务中的两个 组件(即,云平台上的静态托管和无服务器函数)。 这些部署差异不会影响此模式中所述的关系, 但可能会影响其他实际事项,例如是否需要 正确配置 CORS 以启用跨源通信。

6.2.3. 安全 注意事项

6.2.3.1. 授权码授权

使用令牌中介后端的主要好处是 后端能够充当机密客户端。因此, 令牌中介后端必须充当机密 客户端。此外,令牌中介后端必须 按照 第 2.1.1 节,见 [RFC9700]中的说明使用 OAuth 2.0 授权码授权, 以发起访问令牌 请求。

6.2.3.3. 跨站请求伪造防护

基于浏览器的应用程序与 令牌中介后端之间的交互依赖 Cookie 进行身份验证和授权。 与 BFF 一样,令牌中介后端必须考虑 CSRF 攻击。

第 6.1.3.3 节概述了 各种 CSRF 攻击缓解策略的细微差别。具体对于 令牌中介后端,这些 CSRF 防御仅适用于 应用程序可获取访问令牌的一个或多个端点。

6.2.3.4. 高级 OAuth 安全性

令牌中介后端是作为 服务端组件运行的机密客户端。令牌中介后端可以采用 机密客户端的安全最佳实践,例如基于密钥的客户端 身份验证。

6.2.4. 威胁分析

本节重新审视第 5 节中的攻击场景和后果,并讨论可能的 其他防御措施。

6.2.4.1. 攻击场景和后果

如果攻击者能够在 应用程序的执行上下文中执行恶意代码,则以下攻击场景与此 相关:

请注意,这些攻击场景会导致以下 后果:

将访问令牌暴露给基于浏览器的应用程序 是令牌中介后端架构模式的核心思想。 因此,访问令牌容易遭到恶意 浏览器端代码的令牌窃取。

6.2.4.2. 已缓解的攻击场景

令牌中介后端可以有效 缓解下面列出的其他攻击场景:

令牌中介后端通过不向基于浏览器的应用程序 暴露刷新令牌来对抗前两个攻击 场景。即使攻击者完全控制了应用程序,也根本 没有可供窃取的刷新令牌。

在第三个场景中,攻击者通过运行静默流程获取新的 访问令牌(以及可选的刷新令牌)。将 令牌中介后端设置为机密客户端可以缓解此攻击。即使 攻击者成功获取授权码,也会因缺少客户端凭据而无法 交换该授权码。此外, 使用 PKCE 可防止针对授权码的其他攻击。

由于令牌中介后端的性质, 潜在攻击造成的以下后果不再相关:

6.2.4.3. 其他 防御措施

尽管此架构本身会暴露访问令牌, 但仍有一些其他防御措施可帮助提升应用程序的 安全态势。

6.2.4.3.1. 安全令牌存储

鉴于令牌中介后端 模式的性质,浏览器中无需使用持久性令牌存储。 需要时,应用程序始终可以使用其基于 Cookie 的会话 从令牌中介后端获取访问令牌。第 8 节 更详细地介绍了浏览器中各种存储 机制的安全属性。

请注意,即使访问令牌 存储在恶意浏览器端代码无法触及的位置,恶意代码 仍然可以模仿合法应用程序,并向 令牌中介后端发送请求以获取最新访问令牌。

6.2.4.3.2. 使用发送者约束令牌

在此架构中使用发送者约束访问令牌并非 易事。令牌中介后端负责 用授权码或刷新令牌交换访问 令牌,但访问令牌将由应用程序使用。使用 DPoP [RFC9449] 等机制需要在 两方之间拆分职责,而规范并未定义此类场景。 在此类场景中使用 DPoP 不在 本文档的范围内。

6.2.4.4. 总结

令牌中介后端的架构比仅使用浏览器的应用程序 更复杂,但没有运行代理 BFF 那么复杂。同样,令牌中介后端提供的 安全属性也处于使用 BFF 与运行 仅使用浏览器的应用程序之间。

令牌中介后端可以应对通常会 使攻击者获得长期权限、代表用户进行访问的场景。然而,由于 访问令牌窃取所造成的后果,攻击者仍然能够 直接访问资源服务器。

考虑采用令牌中介后端架构时, 强烈建议评估采用第 6.1 节所述完整 BFF 是否是可行的 替代方案。只有当使用场景或系统要求会阻碍 代理 BFF 的使用时,才应该考虑采用令牌中介后端而不是 完整 BFF。

6.3. 基于浏览器的 OAuth 2.0 客户端

本节描述一种充当 OAuth 客户端的基于浏览器的应用程序 架构,该应用程序在浏览器中处理所有 OAuth 职责。因此, 基于浏览器的应用程序无需后端组件参与,便可从授权服务器获取 令牌。

如果攻击者能够在浏览器中执行恶意代码,此 应用程序架构容易遭受前文讨论的所有攻击场景(第 5.1 节)。实质上, 攻击者将能够从授权服务器获取访问令牌和刷新令牌, 从而可能代表用户长期访问受保护资源。

6.3.1. 应用程序 架构

授权 资源 服务器 服务器 (B) (C) (D) (E) (A) 静态 Web 主机 浏览器
图 3: 基于浏览器的 OAuth 2.0 客户端模式

在此架构中,代码首先从静态 Web 主机加载到 浏览器中 (A),随后应用程序在浏览器中运行。在此场景中, 基于浏览器的应用程序被视为公共 客户端,它不具备用于向授权 服务器验证身份的客户端凭据。

应用程序通过启动带有 PKCE 扩展的授权码流程来获取授权码 (B) (在第 6.3.2.1 节中说明)。应用程序使用浏览器 API (例如 [Fetch])向令牌端点发出 POST 请求 (C),以用授权码交换令牌。

随后,应用程序负责按照第 8 节中的说明,使用适当的 浏览器 API 尽可能安全地存储访问令牌和可选的刷新令牌。

当浏览器中的应用程序想要向 资源服务器发出请求时, 可以直接与资源服务器交互。应用程序在请求中包含访问 令牌 (D), 并接收资源服务器的响应 (E)。

6.3.2. 实现 细节

属于公共客户端(第 2.1 节,见 [RFC6749])并使用 第 4.1 节,见 [RFC6749]中所述授权码授权类型的基于浏览器的应用程序必须同时遵循本节所述的其他要求。

6.3.2.1. 授权码授权

属于公共客户端的基于浏览器的应用程序在 获取访问令牌时必须实现 PKCE [RFC7636] 扩展,授权服务器也必须为此类客户端支持并强制使用 PKCE。

PKCE 扩展可防止授权码被截获并 由恶意客户端用来交换访问令牌的攻击,因为它为 授权服务器提供了一种方法,用于验证交换 授权码的客户端实例与启动流程的实例 是否相同。

6.3.2.2. 跨站请求伪造防护

基于浏览器的应用程序必须 防止针对其重定向 URI 的 CSRF 攻击。可以通过 以下任何一种方式实现:

  • 配置授权服务器, 要求此客户端使用 PKCE

  • 为 OAuth state 参数使用并验证唯一值,以携带 CSRF 令牌

  • 如果应用程序同时 使用 OpenID Connect,则按照 [OpenID]中的说明使用并验证 OpenID Connect nonce 参数

有关为授权码流程 选择适当 CSRF 防御措施的更多详细信息,请参阅 第 2.1 节,见 [RFC9700]。

6.3.2.3. 刷新 令牌

对于基于浏览器的客户端,除非应用程序明确 使用 DPoP [RFC9449],否则刷新令牌通常是持有者令牌。因此, 刷新令牌泄漏所造成的风险 大于访问令牌泄漏,因为攻击者可能能够 继续使用被盗的刷新令牌获取新的访问令牌,并且可能 不会被 授权服务器发现。

授权服务器可以选择是否向基于浏览器的 应用程序签发刷新令牌。然而,鉴于第三方 Cookie 阻止 机制的影响,使用刷新令牌已经变得更具吸引力。 [RFC9700] 在 [RFC6749] 的建议基础上,说明了 与刷新令牌有关的一些其他要求。 如果向基于浏览器的应用程序签发刷新令牌,则符合此 BCP 的应用程序和授权 服务器必须同时遵循 [RFC9700] 中有关刷新令牌的建议。

具体而言,授权服务器:

  • 必须在每次使用时轮换 刷新令牌,或者按照 第 4.14.2 节,见 [RFC9700]中的说明使用发送者约束刷新令牌

  • 必须为 刷新令牌设置最长生命周期,或者在刷新令牌 一段时间内未被使用时使其过期

  • 如果刷新令牌具有预先设定的过期时间,则在 签发轮换后的刷新令牌时不得将新刷新 令牌的生命周期延长至超过初始刷新令牌的生命周期

将刷新令牌的总生命周期限制为 初始刷新令牌的生命周期,可以确保被盗的刷新令牌无法 被无限期使用。

例如:

  • 用户授权一个应用程序,签发 有效期为 10 分钟的访问令牌和有效期为 8 小时的刷新令牌。

  • 10 分钟后,初始访问令牌 过期,因此应用程序使用刷新令牌获取新的访问 令牌。

  • 授权服务器返回一个有效期为 10 分钟的新访问 令牌,以及一个有效期为 7 小时 50 分钟的新刷新令牌。

  • 此过程将持续到初始授权后的 8 小时结束。

  • 此时,当应用程序在 8 小时后尝试 使用刷新令牌时,请求将失败,并且 应用程序必须重新启动依赖用户身份验证或先前已建立会话的 授权码流程。

授权服务器应该 将刷新令牌的生命周期与用户在 授权服务器上的已验证身份会话关联起来。这样可以确保用户注销时,先前 向基于浏览器的应用程序签发的刷新令牌失效,从而模拟 单点注销场景。与其他公共客户端相比,授权服务器可以为 基于浏览器的应用程序设置不同的刷新令牌签发、生命周期和过期 策略。

6.3.3. 安全 注意事项

6.3.3.1. 客户端 身份验证

由于基于浏览器的应用程序源代码会 交付到最终用户的 浏览器,因此不适合在其中包含预置的秘密值。因此, 基于浏览器的应用程序通常部署为 第 2.1 节,见 [RFC6749]所定义的公共客户端。

作为分发给 多个用户的应用程序的一部分静态包含的秘密值不应被视为机密秘密值,因为任何 用户都可以检查自己的副本并获知共享秘密值。出于此 原因以及 第 5.3.1 节,见 [RFC6819]中所述的原因,授权 服务器不得要求使用共享秘密值对基于浏览器的 应用程序进行客户端身份验证,因为这除了 标识客户端外没有任何价值,而 client_id 参数已经提供了客户端标识。

仍要求 SPA 客户端使用静态包含的 共享秘密值的授权服务器必须将该客户端视为 公共客户端,并且不得接受此秘密值作为客户端身份的证明。如果不采取 其他措施,此类客户端将遭受客户端冒充 (参见第 6.3.3.2 节)。

6.3.3.2. 客户端 冒充

如 第 10.2 节,见 [RFC6749]中所述,除非授权 服务器能够确认客户端应用程序的身份,否则授权服务器不应该在 没有用户同意或交互的情况下自动 处理授权请求。

如果授权服务器将重定向 URI 限制为一组固定的 绝对 HTTPS URI,从而禁止使用通配符域、通配符路径或通配符 查询字符串组件, 则授权服务器可以接受已注册绝对 HTTPS URI 的这种精确匹配, 将其作为客户端身份证明,以便在 先前针对 client_id 的请求已经获批时,决定是否自动处理授权请求。

6.3.3.2.1. 授权码重定向

客户端必须向 授权服务器注册一个或多个重定向 URI,并在授权请求中仅使用 精确注册的重定向 URI。

授权服务器必须按照 第 4.1.1 节,见 [RFC9700]中的说明,要求已注册 重定向 URI 精确匹配。这有助于防止 以授权码为目标的攻击。

6.3.3.3. 浏览器内通信流程的安全性

在基于浏览器的应用程序中,通常会在 弹出窗口或 iframe 等辅助窗口中执行 OAuth 流程,而不是 重定向主窗口。 在这些流程中,基于浏览器的应用程序会保持对主窗口的控制,例如 避免页面刷新或静默运行基于框架的流程。

如果基于浏览器的应用程序和授权服务器在 不同框架中调用,则必须使用 postMessage API(又称 [WebMessaging])等浏览器内通信 技术,而不是顶层 重定向。 为了保证消息的机密性和真实性,postMessage 的发起方源 和接收方源都必须使用 postMessage API 原生提供的机制进行验证 ([WebMessaging] 第 9.3.2 节)。

第 4.17 节,见 [RFC9700]提供了有关 浏览器内通信流程安全性的更多详细信息,以及基于浏览器的应用程序和授权服务器为抵御这些攻击而必须采取的 对策。

6.3.3.4. 跨源 请求

在此场景中,应用程序使用浏览器 API 向 授权服务器和资源服务器发送请求。鉴于 OAuth 2.0 的性质,这些请求通常是跨源请求, 因而受到浏览器对跨源通信实施的限制。 授权服务器和资源服务器必须 发送必要的 CORS 标头(定义于 [Fetch]),使应用程序能够发出 必要的跨源请求。请注意,在基于浏览器的 OAuth 客户端 与授权服务器或资源服务器运行于同一源的特殊场景中, 无需 CORS 策略即可启用 必要的交互。

对于授权服务器,CORS 配置与 令牌端点有关,基于浏览器的应用程序会在该端点 用授权码交换令牌。此外,如果授权服务器 向应用程序提供其他端点,例如发现元数据 URL、JSON Web 密钥集、动态客户端注册、撤销、内省 或用户信息端点,基于浏览器的应用程序也可能访问 这些端点。因此,授权服务器负责 在这些端点上支持 CORS。

本规范不包含用于决定 具体 CORS 策略实现的准则,该策略可以使用 通配符源或限制更严格的配置。请注意,CORS 有两种 具有不同安全属性的运行模式。第一种模式适用于 CORS 安全列出的请求(以前称为简单请求),在此模式中,浏览器 发送请求,并使用 CORS 响应标头决定是否可以将响应 暴露给客户端执行上下文。对于不在 CORS 安全列表中的 请求,例如带有自定义请求标头的请求,浏览器会先 使用预检检查 CORS 策略。只有当服务器在预检响应中 表示批准时,浏览器才会发送实际 请求。

请注意,由于授权服务器的特定 配置,对预检的 CORS 响应可能与 对实际请求的 CORS 响应不同。在预检期间, 授权服务器只能验证所提供的源,但在实际 请求期间,授权服务器拥有完整的请求数据,例如客户端 ID。因此,授权服务器可以在 预检期间批准已知源,但在将该源与 此特定客户端的预注册源列表比较后拒绝实际请求。

6.3.4. 威胁分析

本节重新审视第 5 节中的攻击场景和后果,并讨论可能的 其他防御措施。

6.3.4.1. 攻击场景和后果

如果攻击者能够在 应用程序的执行上下文中执行恶意代码,则以下攻击场景与此 相关:

最危险的攻击场景是获取和 提取新令牌。在此攻击场景中,攻击者仅与 授权服务器交互,这使客户端中 OAuth 功能的实际实现细节 变得无关紧要。即使合法客户端 应用程序找到了将令牌与攻击者完全隔离的方法, 攻击者仍然能够从授权服务器获取令牌。

请注意,这些攻击场景会导致以下 后果:

6.3.4.2. 其他 防御措施

尽管此架构本身容易受到 恶意浏览器端代码的攻击,但仍有一些其他防御措施可帮助 提升应用程序的安全态势。请注意,这些 防御措施均无法解决或修复允许攻击者运行 新流程来获取令牌这一根本问题。

6.3.4.2.1. 安全令牌存储

直接处理令牌时,应用程序 可以选择不同的存储机制来存储访问令牌和 刷新令牌。本地 存储 [WebStorage] 等普遍可访问的存储区域,比 Web Worker [WebWorker] 等隔离程度更高的存储区域更容易被恶意 JavaScript 访问。第 8 节 更详细地讨论了不同的存储机制及其 权衡。

一种实用的实现模式可以使用 Web Worker [WebWorker] 隔离刷新 令牌,并向应用程序提供用于向 资源服务器发出请求的访问令牌。这可以防止攻击者使用 应用程序的刷新令牌获取新令牌。

然而,即使令牌存储机制能够 将令牌与攻击者完全隔离,也无法阻止 攻击者运行新流程获取一组新令牌(参见第 5.1.3 节)。

6.3.4.2.2. 使用发送者约束令牌

基于浏览器的 OAuth 客户端可以实现 DPoP [RFC9449], 从持有者访问令牌和持有者刷新令牌过渡到 发送者约束令牌。在此类实现中,用于 签署 DPoP 证明的私钥由浏览器处理(不可提取的 [CryptoKeyPair] 使用 [W3C.IndexedDB] 存储)。因此, 使用 DPoP 可以有效防止 XSS 攻击者 外传应用程序令牌的场景(参见第 5.1.1 节 和第 5.1.2 节)。

请注意,使用 DPoP 无法阻止 攻击者运行新流程获取新的访问令牌(以及 可选的刷新令牌);参见第 5.1.3 节。即使强制使用 DPoP, 攻击者也可以将这组新令牌绑定到自己控制的密钥对, 从而外传发送者约束 令牌,并依靠攻击者控制的密钥计算 必要的 DPoP 证明,以便使用这些令牌。

6.3.4.2.3. 限制对授权 服务器的访问

攻击者获取新的 访问令牌(以及可选的刷新令牌)的场景(第 5.1.3 节)依赖于 从浏览器内直接与 授权服务器交互的能力。理论上,防止攻击者静默 与授权服务器交互的防御措施可以解决最危险的 攻击场景。然而在实践中,此类防御措施无效或 不切实际。

为保持完整性,此 BCP 在下文列出了几种 方案。请注意,不建议采用其中任何防御措施,因为它们无法 提供实际可用的安全收益。

授权服务器可以阻止 源自 iframe 内部的授权请求。尽管这可以 防止第 5.1.3 节中的确切场景,但无法防范 该攻击场景的细微变体。例如,攻击者可以 在弹出窗口或隐匿式弹出窗口中启动静默流程。 此外,仅使用浏览器的 OAuth 客户端通常依赖基于隐藏 iframe 的流程来引导用户的身份验证状态,因此这种 方法会严重影响用户体验。

授权服务器可以选择在每次授权码流程中 强制要求用户同意(如 第 10.2 节,见 [RFC6749]所述),从而要求用户 在签发授权码前进行交互。此方法将 增加攻击者运行静默流程获取一组新 令牌的难度。然而,它也会因为持续要求用户同意而严重影响 用户体验。因此,此方法 会造成 "同意疲劳",使用户很可能 盲目批准同意请求,即使该请求与攻击者启动的流程 相关联。

6.3.4.3. 总结

总而言之,基于浏览器的 OAuth 客户端应用程序架构十分直接,但会显著扩大 应用程序的攻击面。攻击者不仅能够劫持 客户端,还能从基于浏览器的应用程序中提取一整套功能完备的 令牌。

不建议业务 应用程序、敏感应用程序以及处理个人 数据的应用程序采用此架构。

7. 不推荐和已弃用的架构模式

过去二十年间,客户端应用程序和后端应用程序发生了显著演变, 威胁、攻击者模型以及对现代应用程序安全性的总体认识也随之演变。 因此,过去在业界普遍接受以及由 OAuth 工作组发布的建议 如今往往已不再受到推荐,而提出的解决方案也常常无法 满足预期的安全要求。

本节讨论几种替代架构模式,不建议 在现代基于浏览器的 OAuth 应用程序中使用这些模式。本节将讨论每种 模式,并在相关情况下通过威胁分析研究其攻击场景和 后果。

7.1. 单域 基于浏览器的应用程序(不使用 OAuth)

简单应用程序经常因为使用 OAuth 取代会话管理概念而变得不必要地复杂。一个典型示例是现代形式的 服务端模型-视图-控制器 (MVC) 应用程序,它现在由基于浏览器的 前端和为其提供支持的服务端 API 组成。

在此类应用程序中,使用 OpenID Connect 将用户 身份验证交由专用提供者处理,可以显著简化应用程序的架构 和开发。然而,通常不需要使用 OAuth 来管理前端与 后端之间的访问。应用程序可以不使用访问令牌,而是依赖 服务端基于 Cookie 的会话状态来跟踪用户的身份验证状态。 用于保护会话 Cookie 的安全准则在第 6.1.3.2 节中讨论。

尽管不使用 OAuth 的建议在本文档中似乎不合时宜,但 必须注意,OAuth 最初是为第三方或联合访问 API 而创建的, 因此它在单一同域部署中可能不是最佳解决方案。尽管如此,即使在同域架构中, 使用 OAuth 仍然具有一些优势,例如:

  • 为未来提供更多灵活性。例如,如果以后 向系统添加新域,由于已经部署了 OAuth,新 域便不需要额外重新设计架构。

  • 能够利用现有的库支持,而 无需为集成编写定制代码。

  • 在 OAuth 服务器上集中提供登录和多因素身份验证支持、 账户管理及恢复功能,而不是使其成为 应用程序逻辑的一部分。

  • 在用户身份验证与 提供资源之间划分职责。

在第一方同域场景中,为基于浏览器的应用程序使用 OAuth 可以提供这些优势,并且可以通过上述任何架构模式 实现。

7.1.1. 威胁分析

由于未使用 OAuth,此架构模式只 容易遭受以下攻击场景:通过用户的浏览器代理请求 (第 5.1.4 节)。因此, 此模式可能导致以下后果:客户端劫持(第 5.2.3 节)。

7.2. OAuth 隐式授权

OAuth 2.0 隐式授权类型(定义于 第 4.2 节, 见 [RFC6749]) 的工作方式是由授权服务器在 授权响应(前端通道)中签发访问令牌,而不执行授权码交换步骤。在这种 情况下,访问 令牌在重定向 URI 的片段部分中返回,这为攻击者 提供了多种截获和窃取访问令牌的机会。

隐式授权类型的安全属性使其不再是 推荐的最佳实践。为了有效防止使用此流程,授权服务器 不得在授权响应中签发访问令牌,并且必须仅从令牌端点 签发访问令牌。基于浏览器的客户端必须使用授权码授权类型,并且不得 使用隐式授权类型获取访问令牌。

7.2.1. 历史说明

过去,隐式授权类型为基于浏览器的应用程序提供了优势,因为 JavaScript 始终可以任意读取和操作 URL 的片段部分, 而不会触发页面重新加载。为了在应用程序获取 访问令牌后将其从 URL 中移除,这一点是必需的。此外,在 CORS 尚未获得浏览器广泛支持时,隐式授权类型 提供了一种不要求浏览器或服务器支持 CORS 的替代流程。

现代浏览器现在提供会话历史 API(在 [HTML] 的 "导航和会话历史" 中说明),该 API 提供一种 无需触发页面重新加载便可修改 URL 路径和查询字符串 组成部分的机制。此外,CORS 已获得广泛 支持,并且单页应用程序通常会将其用于多种用途。这意味着 现代基于浏览器的应用程序可以 使用带有 PKCE 的 OAuth 2.0 授权码授权类型,因为借助 会话历史 API,它们能够在不触发页面重新加载的情况下 从查询字符串中移除授权码,而令牌端点对 CORS 的支持意味着, 即使授权服务器位于其他域,应用程序也可以获取令牌。

7.2.2. 威胁分析

本节讨论的架构模式容易遭受 以下攻击场景:

因此,此模式可能导致以下后果:

7.2.3. 针对隐式授权的其他 攻击

除已经讨论的攻击场景和后果外, 还有几种其他攻击进一步支持弃用 隐式授权类型。[RFC6819] 和 第 4.1.2 节,见 [RFC9700] 所述的许多针对隐式授权类型的攻击都没有充分的缓解策略。以下各节将介绍 在继续使用隐式授权类型时无法缓解的具体 攻击。

7.2.3.1. 操纵重定向 URI

如果攻击者能够使授权响应 发送到其控制的 URI, 就会直接获得包括访问令牌在内的授权响应。 [RFC9700] 详细介绍了实施此攻击的几种方法。

7.2.3.2. 浏览器历史记录中的访问 令牌泄漏

攻击者可以从 浏览器历史记录中获取访问令牌。 [RFC6819] 建议的对策仅限于为令牌使用较短的 过期 时间,并指明浏览器不应缓存响应。 这两种措施都无法完全防止此攻击;它们只能减少潜在 损害。

此外,许多浏览器现在还会将浏览器历史记录 同步到云服务和 多台设备,从而形成更广泛的攻击面,使攻击者能够从 URL 中提取访问 令牌。

第 4.3.2 节,见 [RFC9700]对此进行了更详细的讨论。

7.2.3.3. 操纵脚本

攻击者可以通过各种 方式修改页面或向浏览器中注入脚本,包括浏览器的 HTTPS 连接被 企业网络等 截获时。尽管针对 TLS 层的攻击通常不在 基本安全建议需要防范的范围 内,但对于基于浏览器的 应用程序而言,实施此类攻击要容易得多。注入的脚本可以使攻击者访问 页面上的所有内容。

当应用程序使用已知的标准方式获取访问令牌时, 页面上运行恶意脚本的风险可能会 增大;也就是说,攻击者始终可以查看 window.location 变量来寻找访问 令牌。此威胁特征 不同于攻击者通过了解由授权码流程获取的访问令牌可能 在何处或以何种方式 存储,从而专门针对某个应用程序的情况。

7.2.3.4. 访问 令牌泄漏给第三方脚本

在基于浏览器的应用程序中使用第三方脚本相当常见,例如 分析工具、崩溃报告,甚至社交媒体的 "点赞" 按钮。 在这些情况下,应用程序的作者可能无法完全 了解应用程序中运行的全部代码。访问令牌在 片段中返回时,页面上的任何第三方脚本都可以看到它。

7.2.4. 隐式授权 的缺点

与使用推荐的授权码授权类型相比,隐式授权类型还存在 其他几项缺点。

  • OAuth 2.0 未提供任何机制,使客户端能够验证 特定访问令牌是否 原本就是为该客户端签发的。如果恶意方将通过其他方式 获取的访问令牌 交给客户端,这可能导致误用以及潜在的冒充 攻击。

  • 在前端通道重定向中返回访问令牌 无法使授权 服务器确信访问令牌最终确实会到达 应用程序,因为此重定向可能以多种方式失败或被 截获。

  • 支持隐式授权类型需要额外的 代码、更多维护工作以及对相关安全注意事项的更多 了解。将授权服务器限制为仅支持授权码授权类型,可以缩小 实现的攻击面。

  • 如果基于浏览器的应用程序被封装为 原生应用程序,那么 [RFC8252] 同样要求使用带有 PKCE 的授权码授权类型。

7.3. 资源所有者 密码凭据授权

按照 第 2.4 节, 见 [RFC9700]中的说明,不得 使用资源所有者密码凭据授权。应该改用授权 码授权类型并将用户重定向到授权服务器, 这使授权服务器有机会提示用户选择 安全且不易遭受网络钓鱼的身份验证选项、利用单点登录会话, 或使用第三方身份提供者。相比之下,资源所有者密码凭据 授权并未 为这些功能提供任何内置机制,而需要通过自定义 协议进行扩展。

为了符合此最佳实践,使用 OAuth 或 OpenID Connect 的基于浏览器的应用程序必须按照本文档中的说明,使用基于重定向的流程(例如 OAuth 授权 码授权类型)。

7.4. 在 Service Worker 中处理 OAuth 流程

为限制攻击者提取现有令牌或 获取一组新令牌的能力,过去曾有人提出一种使用 Service Worker [W3C.service-workers] 的 模式。在此模式中,应用程序加载后的第一个操作是 注册 Service Worker。Service Worker 负责执行 授权码流程以获取令牌,并使用适当的访问令牌增强发送给资源服务器的传出请求。 此外,Service Worker 还会阻止客户端应用程序的 代码直接调用授权服务器的端点。此限制旨在 针对 "获取和提取新令牌" 攻击场景(第 5.1.3 节)。

下面的序列图说明了 客户端、Service Worker、授权服务器和资源服务器之间的交互。

Service 资源 授权 用户 应用程序 Worker 服务器 服务器 浏览 /authorize 重定向 携带 授权 码 < - - - - - - - - - - - - - - - - - 令牌 请求 携带 授权 码 /token < - - - - - - - - - - - - - - - - - 资源 请求 资源 请求 携带 访问 令牌 用户 应用程序 Service 资源 授权 Worker 服务器 服务器
图 4: OAuth 2.0 Service Worker 模式

请注意,此模式绝不会将令牌暴露给在 浏览器中运行的应用程序。由于 Service Worker 在隔离的执行环境中运行,因此 不存在共享内存,客户端应用程序也无法影响 Service Worker 的执行。

7.4.1. 威胁分析

本节讨论的架构模式容易遭受 以下攻击场景:

因此,此模式可能导致以下后果:

7.4.1.1. 攻击 Service Worker

使用 Service Worker 看似很有前景的安全优势, 值得更详细地讨论其安全限制。 为了针对相关攻击场景(第 5.1 节)全面保护应用程序, Service Worker 需要满足两项安全要求:

  1. 防止攻击者外传 令牌。

  2. 防止攻击者获取一组新的 令牌。

注册后,Service Worker 会运行授权码 流程并获取令牌。由于 Service Worker 在其隔离的执行环境中 跟踪令牌,任何 应用程序代码(包括可能具有恶意的代码)都无法触及这些令牌。因此, Service Worker 满足防止令牌外传的第一项要求。 这实际上消除了第 5.1 节中讨论的前两个攻击场景。

为了满足第二项安全要求,Service Worker 必须能够保证,控制合法 应用程序的攻击者无法执行新的授权码授权;此攻击在第 5.1.3 节中讨论。由于 Service Worker 的性质,已注册的 Service Worker 能够阻止所有启动此类新 流程的传出请求,即使这些请求发生在框架或新窗口中。

然而,在应用程序内部运行的恶意代码 可以注销此 Service Worker。注销 Service Worker 可能会对 应用程序功能产生重大影响,因此浏览器不会轻易 处理此操作。因此,已注销的 Service Worker 会被标记为 已注销,但当前正在运行的所有实例都将保持活动状态,直到其 对应的浏览上下文终止(例如,通过关闭标签页或 窗口)。所以,即使攻击者注销了 Service Worker,它仍然 处于活动状态,并且能够阻止攻击者访问授权 服务器。

注销 Service Worker 的后果之一是, 打开新的浏览上下文时,它将不再存在。因此,当 攻击者先注销 Service Worker,随后在 框架中启动新流程时,该框架的浏览上下文将没有关联的 Service Worker。 因此,攻击者能够运行自己的新 授权码授权,从框架的 URL 中提取授权码, 并用其交换令牌。实质上,Service Worker 无法满足 第二项安全要求,因而容易遭受攻击者 获取一组新令牌的场景(第 5.1.3 节)。

由于这些缺点,再加上注册和维护 Service Worker 所带来的显著复杂性,因此不建议 使用此模式。

最后,请注意,使用 Service Worker 本身 不会扩大应用程序的攻击面。在实践中,Service Worker 通常用于改造旧式应用程序,使其支持 在传出请求中包含 OAuth 访问令牌。这些 场景中的 Service Worker 不会改变应用程序的安全属性,而只是 简化应用程序的开发和维护。

8. 浏览器中的令牌存储

当基于浏览器的应用程序直接处理 OAuth 访问令牌或刷新令牌时, 它便负责临时或持久地存储这些令牌。因此, 应用程序需要决定如何管理令牌(例如,内存存储还是持久存储),以及应采取哪些 措施来进一步将令牌与主应用程序代码隔离。本节讨论几种 不同的存储机制及其属性。这些建议考虑了 OAuth 令牌的独特 属性,其中一些可能与通用浏览器安全建议重叠。

在讨论基于浏览器的令牌存储解决方案的安全属性时, 务必要了解攻击者攻陷基于浏览器的应用程序后所具备的能力。 与前面的讨论类似,应该考虑两种主要攻击场景:

  1. 攻击者从存储中获取令牌。

  2. 攻击者从提供方(例如授权 服务器或令牌中介后端)获取令牌。

由于攻击者的代码会变得与合法应用程序的 代码无法区分,因此攻击者始终能够以与合法应用程序代码 完全相同的方式向提供方请求令牌。因此,即使是完全隔离的令牌存储解决方案, 也无法消除第二种威胁所带来的危险,即攻击者向提供方请求令牌。

尽管如此,基于浏览器的不同存储解决方案所具有的安全属性, 会影响攻击者从存储中获取现有令牌的能力。

8.1. Cookie

浏览器 Cookie 既是一种存储机制,也是一种传输机制。 浏览器通过相应的请求和响应 标头自动支持这两种机制,从而将 Cookie 存储在浏览器中,并在 传出请求与 Cookie 的域、路径或其他 属性匹配时自动附带 Cookie。

除了通过标头控制 Cookie 之外,浏览器还提供用于获取和设置 Cookie 的 JavaScript Cookie API。此 Cookie API 经常被误认为是一种在浏览器中存储 数据的简便方式。在这种场景中,JavaScript 代码将令牌存储在 Cookie 中, 以便稍后取回该令牌并将其加入 API 调用的 Authorization 标头。 然而,由于 Cookie 与基于浏览器的应用程序所在的域相关联, 浏览器向该域上运行的服务器发出请求时,也会发送包含令牌的 Cookie。 此类请求的一个示例是,用户先前访问过应用程序后,浏览器再次加载该应用程序 (图 3 中的 (A))。

由于使用 Cookie 进行 基于 JavaScript 的存储会产生这些非预期副作用,因此不推荐这种做法。

请注意,这种做法不同于在 BFF 中使用 Cookie (见第 6.1.3.2 节);在后者中,JavaScript 无法访问 Cookie,并且 Cookie 本来就应发送到 后端。

8.2. 在 Service Worker 中存储令牌

Service Worker [W3C.service-workers] 提供了一个完全隔离的环境来 管理令牌。客户端应用程序无法访问这些令牌,从而有效地 防止令牌外泄。为了保证这些令牌的安全,Service Worker 不能与应用程序共享这些令牌。因此,每当应用程序 希望使用令牌执行操作时,都必须请求 Service Worker 执行该 操作并返回结果。

当目标是将令牌与应用程序的执行上下文隔离时, Service Worker 不得将令牌存储在任何 与主窗口共享的持久存储 API 中。例如,目前 IndexedDB 存储由 浏览上下文和 Service Worker 共享,因此不适合由 Service Worker 用来持久保存应保持主窗口无法访问的数据。因此, Service Worker 目前无法访问隔离的持久存储区域。

如前所述,使用 Service Worker 并不能阻止攻击者 获取一组新令牌。同样,如果应用程序负责从 授权服务器获取令牌并将其传递给 Service Worker 以便进一步 管理,攻击者也可以执行与合法应用程序相同的操作来获取 这些令牌。

8.3. 在 Web Worker 中存储令牌

应用程序可以使用 Web Worker [WebWorker],所形成的场景与前述依赖 Service Worker 的场景 几乎完全相同。Service Worker 与 Web Worker 之间的区别在于访问级别和运行时属性。Service Worker 可以拦截 并修改传出请求,而 Web Worker 只是一种运行后台任务的方式。Web Worker 是临时的,会在浏览上下文关闭时消失;而 Service Worker 是注册在浏览器中的持久服务。

使用 Web Worker 的安全属性与使用 Service Worker 相同。令牌一旦暴露给应用程序,就会变得易受攻击。需要使用令牌时, 依赖这些令牌的操作必须由 Web Worker 执行。

隔离刷新令牌的一种常见方法是使用 Web Worker。在这种 场景中,应用程序从 Web Worker 启动授权码流程。 重定向返回的授权码会被转发给 Web Worker,后者再使用该授权码交换 令牌。Web Worker 将刷新令牌保存在内存中,并将访问令牌发送给主 应用程序。主应用程序按需使用访问令牌。当应用程序需要 运行刷新令牌流程时,它会请求 Web Worker 执行该流程,随后应用程序 便可获得新的访问令牌。

在此场景中,应用程序自身的刷新令牌实际上受到了 防外泄保护,但访问令牌没有。此外,没有任何措施能够阻止 攻击者通过运行新的授权码流程来获取自己的令牌(第 5.1.3 节)。

8.4. 内存令牌 存储

另一种选择是不使用任何持久 存储,而将令牌保存在内存中。这样做会将令牌的暴露范围限制在当前执行上下文内, 但缺点是无法在页面加载之间持久保存令牌。

在 JavaScript 执行环境中,可以使用闭包变量进一步增强内存令牌存储的安全性, 从而有效屏蔽对令牌的直接 访问。通过使用闭包,只有闭包内部预定义的函数才能访问令牌, 例如向资源服务器发出请求的函数。

虽然闭包在简单、隔离的环境中效果良好,但在浏览器执行环境这样的复杂环境中, 要确保其安全并不容易。例如,闭包 依赖各种外部函数来执行操作,例如 toString 或网络 API。攻击者可以使用原型污染,将 这些函数替换为恶意版本,导致闭包之后的操作使用这些 恶意版本。在恶意函数内部,攻击者可以访问函数 参数,这可能会将闭包内的令牌暴露给攻击者。

8.5. 持久令牌 存储

截至本文撰写时,浏览器中可用的持久存储 API 包括 localStorage [WebStorage]、 sessionStorage [WebStorage] 和 IndexedDB [W3C.IndexedDB]。

localStorage 可跨页面重新加载持久保存,并由所有标签页共享。此 存储可供整个源访问,并能长期持久保存。localStorage 无法 防止恶意 JavaScript 进行未经授权的访问,因为攻击者会在 同一个源中运行代码,因此能够读取 localStorage 的内容。此外,localStorage 是同步 API,会阻塞其他 JavaScript,直到 操作完成。

sessionStorage 与 localStorage 类似,但 sessionStorage 的生命周期与浏览器标签页的生命周期相关联。此外,sessionStorage 不会 在打开同一源页面的多个标签页之间共享,这略微缩小了 sessionStorage 中令牌的暴露范围。

IndexedDB 与 localStorage 一样是一种持久存储机制,但它既会 在多个标签页之间共享,也会在浏览上下文与 Service Worker 之间共享。 此外,IndexedDB 是异步 API,优于同步的 localStorage API。

请注意,这些模式之间的主要区别在于数据的 暴露范围;但当攻击者能够在应用程序的执行环境中 执行恶意代码时,这些选择均无法完全缓解令牌外泄。

8.6. 浏览器存储 API 的文件系统 注意事项

在所有情况下,截至本文撰写时,都无法保证浏览器存储中的数据 已进行静态加密。这种行为可能会将令牌暴露给有能力 读取磁盘文件的攻击者。虽然此类攻击依赖的能力远远超出 基于浏览器的应用程序的范围,但这个问题凸显了针对 现代应用程序的一种重要攻击向量。越来越多的恶意软件专门用于遍历用户的计算机, 查找浏览器配置文件,以获取高价值令牌和会话 Cookie,进而导致 账户接管攻击。

虽然基于浏览器的应用程序无法缓解此类攻击, 但可以通过使用加密确保数据机密性来减轻此类攻击的后果。 [W3C.WebCryptoAPI] 提供了一种供 JavaScript 代码 生成秘密密钥的机制,并允许将该密钥设置为不可导出。JavaScript 应用程序随后可以使用此 API 在存储令牌之前对其进行加密,并在使用时解密。然而, [W3C.WebCryptoAPI] 规范仅确保密钥无法导出到浏览器代码,但并未 对操作系统如何存储该密钥本身提出任何要求。因此, 不能依靠不可导出密钥来防止密钥从 底层文件系统外泄。

为了防止令牌从文件系统外泄, 加密密钥需要存储在文件系统之外的其他位置,例如远程 服务器。这会给纯粹基于浏览器的应用程序带来新的复杂性,并且超出 本文档的范围。

9. 安全注意事项

9.1. 降低令牌的 权限

OAuth 领域的一项通用安全最佳实践是尽可能减小 与访问令牌关联的权限。此最佳实践适用于本规范讨论的所有 架构。具体而言,以下注意事项有助于 尽可能减小访问令牌的权限:

  • 缩短访问令牌的生命周期,并依靠刷新令牌 续期访问令牌

  • 减少与访问 令牌关联的作用域或权限

  • 使用 [RFC8707] 中 描述的扩展,将访问令牌限制为只能用于单个资源

使用 OpenID Connect 时,务必要避免通过 ID Token 中的声明 泄露敏感信息。授权服务器不应该包含客户端未使用的任何 ID Token 声明。

9.2. 发送者约束 令牌

如本文档通篇所述,使用发送者约束令牌 并不能解决纯浏览器 OAuth 客户端的安全局限。然而,当 令牌中介后端(第 6.2 节)或纯浏览器 OAuth 客户端(第 6.3 节)提供的安全级别足以满足 当前用例时,可以使用发送者约束令牌来增强访问 令牌和刷新令牌的安全性。一种能以适用于基于浏览器的应用程序的方式 实现发送者约束令牌的方法是 DPoP [RFC9449]。

使用发送者约束令牌时,OAuth 客户端必须证明自己持有 私钥,才能使用该令牌,因此令牌本身无法单独使用。如果 发送者约束令牌被盗,攻击者无法直接使用该令牌; 还需要窃取私钥。本质上,可以说使用 发送者约束令牌,将安全存储令牌的难题转变成了安全存储 私钥的难题。理想情况下,应用程序应该使用不可导出的私钥,例如 使用 [W3C.WebCryptoAPI] 生成私钥。如果浏览器 存储中未加密的令牌受不可导出私钥保护,XSS 攻击将无法提取 该密钥,因此攻击者无法使用该令牌。

如果应用程序无法使用能生成不可导出 密钥的 API,则应该采取措施将私钥与自身的执行 上下文隔离。实现这种隔离的技术类似于使用安全令牌存储机制,相关内容 见第 8 节。

虽然不可导出密钥可免遭 JavaScript 上下文内的 外泄,但底层私钥从文件系统外泄仍是 一种潜在攻击向量。截至本文撰写时,[W3C.WebCryptoAPI] 并不保证 不可导出密钥实际上受可信平台模块(TPM)保护,或以 加密形式存储在磁盘上。如果攻击者能够访问用户计算机的文件系统, 例如通过恶意软件,仍有可能从底层文件系统 外泄不可导出密钥。这实际上使该潜在攻击向量等同于 会话劫持攻击。

9.3. 授权服务器 混淆攻击缓解措施

授权服务器混淆攻击对任何支持 至少两个授权服务器的客户端都构成严重威胁。第 4.4 节,见 [RFC9700],其中提供了有关 混淆攻击 以及上述对策的更多详细信息。

9.4. 使用源隔离应用程序

Web 的许多安全机制都依赖源,源被定义为 三元组 <scheme, hostname, port>。例如,浏览器会自动 隔离具有不同源的浏览上下文、将资源限制到特定源,并对 传出的跨源请求应用 CORS 限制。

因此,避免在单个源中部署多个 应用程序被视为一项最佳实践。每个源只部署一个应用程序的 架构可以利用这些浏览器限制来提高应用程序的安全性。 此外,每个应用程序使用一个独立源,可以更轻松地配置和部署 CORS、CSP 等安全措施。

10. IANA 注意事项

本文档不要求执行任何 IANA 操作。

11. 参考文献

11.1. 规范性参考文献

[Fetch]
WHATWG,“Fetch”,WHATWG 现行标准,<https://fetch.spec.whatwg.org/>。 提交快照:<https://fetch.spec.whatwg.org/commit-snapshots/d9c7b6d31067a7eb13110950ef67db54c6362cfa/>。
[RFC2119]
Bradner, S.,“用于在 RFC 中指示要求级别的关键词”,BCP 14, RFC 2119,DOI 10.17487/RFC2119,, <https://www.rfc-editor.org/info/rfc2119>。
[RFC6749]
Hardt, D.,编,“OAuth 2.0 授权框架”,RFC 6749,DOI 10.17487/RFC6749,,<https://www.rfc-editor.org/info/rfc6749>。
[RFC6750]
Jones, M. 和 D. Hardt,“OAuth 2.0 授权框架:不记名令牌用法”, RFC 6750,DOI 10.17487/RFC6750,, <https://www.rfc-editor.org/info/rfc6750>。
[RFC7636]
Sakimura, N.,编,Bradley, J. 和 N. Agarwal,“OAuth 公共客户端的代码 交换证明密钥”,RFC 7636,DOI 10.17487/RFC7636,,<https://www.rfc-editor.org/info/rfc7636>。
[RFC8174]
Leiba, B.,“RFC 2119 关键词中大写与 小写的歧义”,BCP 14,RFC 8174,DOI 10.17487/RFC8174,, <https://www.rfc-editor.org/info/rfc8174>。
[RFC8252]
Denniss, W. 和 J. Bradley, “适用于原生应用的 OAuth 2.0”,BCP 212,RFC 8252,DOI 10.17487/RFC8252,, <https://www.rfc-editor.org/info/rfc8252>。
[RFC8707]
Campbell, B.、Bradley, J. 和 H. Tschofenig,“OAuth 2.0 的资源指示符”,RFC 8707,DOI 10.17487/RFC8707,,<https://www.rfc-editor.org/info/rfc8707>。
[RFC9449]
Fett, D.、Campbell, B.、Bradley, J.、Lodderstedt, T.、 Jones, M. 和 D. Waite,“OAuth 2.0 持有证明演示(DPoP)”,RFC 9449,DOI 10.17487/RFC9449,, <https://www.rfc-editor.org/info/rfc9449>。
[RFC9700]
Lodderstedt, T.、Bradley, J.、 Labunets, A. 和 D. Fett, “OAuth 2.0 安全最佳当前实践”,BCP 240,RFC 9700,DOI 10.17487/RFC9700,,<https://www.rfc-editor.org/info/rfc9700>。
[W3C.service-workers]
Chintala, M.,编 和 Y. Yanagisawa, 编,“Service Workers 每夜版”,W3C 推荐标准,,<https://www.w3.org/TR/2026/CRD-service-workers-20260126/>。 最新版本可在以下地址获取:<https://www.w3.org/TR/service-workers/>。
[WebMessaging]
WHATWG,“HTML - 跨文档 消息传递”,WHATWG 现行标准,<https://html.spec.whatwg.org/#web-messaging>。 提交快照:<https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/#web-messaging>。

11.2. 资料性参考文献

[COOKIES]
van Kesteren, A. 和 J. Hofmann,“Cookie:HTTP 状态管理 机制”,进行中的工作,互联网草案,draft-ietf-httpbis-layered-cookies-02,,<https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-layered-cookies-02>。
[CryptoKeyPair]
Mozilla 开发者网络,“CryptoKeyPair”,<https://developer.mozilla.org/en-US/docs/Web/API/CryptoKeyPair>。
[HTML]
WHATWG,“HTML”,WHATWG 现行标准,<https://html.spec.whatwg.org/>。 提交快照:<https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/>。
[OpenID]
Sakimura, N.、Bradley, J.、 Jones, M.、de Medeiros, B. 和 C. Mortimore,“OpenID Connect Core 1.0(纳入勘误集 2)”,,<https://openid.net/specs/openid-connect-core-1_0-errata2.html>。
[OWASPCheatSheet]
“OWASP 速查表”,<https://cheatsheetseries.owasp.org/>。
[RFC6265bis]
Bingler, S.,编,West, M., 编 和 J. Wilander,编,“Cookie:HTTP 状态管理机制”,进行中的工作,互联网草案, draft-ietf-httpbis-rfc6265bis-22,,<https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-22>。
[RFC6819]
Lodderstedt, T.,编,McGloin, M. 和 P. Hunt,“OAuth 2.0 威胁模型和安全注意事项”,RFC 6819,DOI 10.17487/RFC6819,,<https://www.rfc-editor.org/info/rfc6819>。
[SessionFixation]
“会话固定”,<https://owasp.org/www-community/attacks/Session_fixation>。
[Site]
Mozilla 开发者网络,“站点”,<https://developer.mozilla.org/en-US/docs/Glossary/Site>。
[W3C.CSP3]
West, M.,编 和 A. Sartori, 编,“内容安全策略第 3 级”,W3C 工作草案,,<https://www.w3.org/TR/2026/WD-CSP3-20260505/>。 最新版本可在以下地址获取:<https://www.w3.org/TR/CSP3/>。
[W3C.IndexedDB]
Becker, S.,编,“Indexed Database API 3.0”,W3C 工作草案,,<https://www.w3.org/TR/2025/WD-IndexedDB-3-20250813/>。 最新版本可在以下地址获取:<https://www.w3.org/TR/IndexedDB/>。
[W3C.SRI]
Braun, F.,编,“子资源 完整性”,W3C 工作草案,,<https://www.w3.org/TR/2025/WD-sri-2-20250710/>。 最新版本可在以下地址获取:<https://www.w3.org/TR/sri-2/>。
[W3C.wasm-core-2]
Rossberg, A.,编,“WebAssembly 核心 规范”,W3C 候选推荐 草案,, <https://www.w3.org/TR/2026/CRD-wasm-core-2-20260210/>。 最新版本可在以下地址获取:<https://www.w3.org/TR/wasm-core-2/>。
[W3C.WebCryptoAPI]
Huigens, D.,编,“Web 加密 第 2 级”,W3C 首次公开工作草案,,<https://www.w3.org/TR/2025/WD-webcrypto-2-20250422/>。 最新版本可在以下地址获取:<https://www.w3.org/TR/webcrypto-2/>。
[WebStorage]
WHATWG,“HTML - Web 存储”, WHATWG 现行标准,<https://html.spec.whatwg.org/#webstorage>。 <https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/#webstorage>。
[WebWorker]
WHATWG,“HTML - Web Worker”, WHATWG 现行标准,<https://html.spec.whatwg.org/#workers>。 提交快照:<https://html.spec.whatwg.org/commit-snapshots/b4b94b1db80aa55a143e1f74f6ad9d4a185f2333/#workers>。

致谢

作者谨对 William Denniss 和 John Bradley 的工作表示感谢, 他们针对原生应用程序提出的建议为基于浏览器的应用程序的许多最佳实践 提供了参考。作者还要感谢 Hannes Tschofenig 和 Torsten Lodderstedt、最初提出此 BCP 的 第 27 届互联网身份研讨会会议 的与会者,以及以下为最终规范的塑造和形成 贡献了构想、反馈和措辞的个人:

Andy Barlow、Andy Newton、Annabelle Backman、Brian Campbell、Brock Allen、Christian Mainka、Damien Bowden、 Daniel Fett、Deb Cooley、Elar Lang、Emmanuel Gautier、Erik Kline、Éric Vyncke、Eva Sarafianou、 Filip Skokan、George Fletcher、Hannes Tschofenig、Janak Amarasena、 John Bradley、Joseph Heenan、 Justin Richer、Karl McGuinness、 Karsten Meyer zu Selhausen、Leo Tohill、Louis Jannett、 Marc Blanchet、Martin Thomson、Matthew Bocci、Mike Bishop、Mike Jones、Mohamed Boucadair、Orie Steele、Qin Wu、 Rifaat Shekh-Yusef、Roman Danyliw、 Sean Kelleher、Thomas Broyer、Thomas Fossati、Tomek Stojecki、 Torsten Lodderstedt、Vittorio Bertocci、Watson Ladd、William Duncan 以及 Yannick Majoros。

作者地址

Aaron Parecki
Okta
Philippe De Ryck
Pragmatic Web Security
David Waite
Ping Identity