单页应用程序(也称为基于浏览器的应用程序)在从网页加载 JavaScript 和 HTML 源代码后完全在浏览器中运行。由于浏览器可以使用整个源代码,因此它们无法维护客户端机密的机密性,因此这些应用程序不使用机密。因为他们不能使用客户端密码,所以最好的选择是使用 PKCE 扩展来保护重定向中的授权代码。这类似于也不能使用客户端密码的移动应用程序的解决方案。
单页应用程序的一个常见历史模式是使用隐式流程在重定向中接收访问令牌,而无需中间授权代码交换步骤。这有许多安全问题,如隐式流程所述,不应再使用。请参阅https://oauth.net/2/browser-based-apps/ 了解更多详情。
下图说明了一个示例,其中用户与浏览器交互,浏览器直接向服务发出 API 请求。首先从客户端下载 Javascript 和 HTML 源代码后,浏览器会直接向服务发出 API 请求。在这种情况下,应用程序的服务器永远不会向服务发出 API 请求,因为一切都直接在浏览器中处理。
授权代码是一个临时代码,客户端将用它来交换访问令牌。代码本身是从授权服务器获得的,用户可以在授权服务器上看到客户端请求的信息,并批准或拒绝该请求。
Web 流程的第一步是向用户请求授权。这是通过创建授权请求链接供用户单击来实现的。
授权 URL 通常采用以下格式:
https://authorization-server.com/oauth/authorize
?client_id=a17c21ed
&response_type=code
&state=5ca75bd30
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
用户访问授权页面后,服务向用户显示请求的解释,包括应用程序名称、范围等。如果用户单击“批准”,服务器将重定向回网站,并提供授权代码和URL 查询字符串中的状态值。
以下参数用于发出授权请求。
response_type
response_type
设置为code
指示您需要授权代码作为响应。
client_id
是client_id
您的应用程序的标识符。首次向该服务注册您的应用程序时,您将收到一个 client_id。
redirect_uri
(可选)redirect_uri
在规范中是可选的,但某些服务需要它。这是您希望在授权完成后将用户重定向到的 URL。这必须与您之前在服务中注册的重定向 URL 相匹配。
scope
(可选)包含一个或多个范围值以请求额外的访问级别。这些值将取决于特定的服务。
state
(推荐)该state
参数有两个功能。当用户被重定向回您的应用程序时,您作为状态包含的任何值也将包含在重定向中。这使您的应用程序有机会在用户被定向到授权服务器和再次返回之间持久保存数据,例如使用状态参数作为会话密钥。这可能用于指示授权完成后在应用程序中执行的操作,例如,指示在授权后重定向到您的应用程序的哪些页面。这也作为 CSRF 保护机制。
请注意,不使用客户端密码意味着使用状态参数对于单页应用程序更为重要。
以下分步示例说明了如何为单页应用程序使用授权授予类型。
该应用程序通过制作一个包含 ID 以及可选范围和状态的 URL 来启动流程。该应用程序可以将其放入<a href="">
标签中。
<a href="https://authorization-server.com/authorize?response_type=code
&client_id=mRkZGFjM&state=TY2OTZhZGFk">Connect Your Account</a>
在被定向到 auth 服务器后,用户会看到授权请求。
用户被带到服务并看到请求后,他们将允许或拒绝该请求。如果他们允许请求,他们将被重定向回指定的重定向 URL 以及查询字符串中的授权代码。然后,应用程序需要将此授权码交换为访问令牌。
https://example-app.com/cb?code=Yzk5ZDczMzRlNDEwY&state=TY2OTZhZGFk
如果您在初始授权 URL 中包含“state”参数,该服务将在用户授权您的应用程序后将其返回给您。您的应用应该将状态与其在初始请求中创建的状态进行比较。这有助于确保您只交换您请求的授权码,防止者使用任意或窃取的授权码重定向到您的回调 URL。
为了交换访问令牌的授权代码,应用程序向服务的令牌端点发出 POST 请求。该请求将具有以下参数。
grant_type
(必需的)该grant_type
参数必须设置为“ authorization_code
”。
code
(必需的)此参数用于从授权服务器接收到的授权代码,该代码将包含在该请求的查询字符串参数“code”中。
redirect_uri
(可选)如果重定向 URL 包含在初始授权请求中,则它也必须包含在令牌请求中,并且必须相同。有些服务支持注册多个重定向 URL,有些服务需要在每个请求中指定重定向 URL。查看服务的文档以了解详细信息。
尽管此流程中未使用客户端密码,但请求需要发送客户端 ID 以识别发出请求的应用程序。这意味着客户端必须将客户端 ID 作为 POST 主体参数包含在内,而不是像在包含客户端机密时那样使用 HTTP 基本身份验证。
一些服务对单页应用程序使用替代的隐式流程,而不是允许应用程序使用没有秘密的授权代码流程。
隐式流程绕过代码交换步骤,取而代之的是访问令牌在查询字符串片段中立即返回给客户端。
实际上,只有非常有限的情况需要这样做。几个主要的实现(Keycloak、Deutsche Telekom、Smart Health IT)选择完全避免隐式流程,而是使用授权代码流程。
为了让单页应用程序使用授权代码流,它必须能够向授权服务器发出 POST 请求。这意味着如果授权服务器在不同的域中,服务器将需要支持适当的 CORS 标头。如果支持 CORS 标头不是一个选项,则该服务可能会改用隐式流。
在任何情况下,对于隐式流程和没有秘密的授权代码流程,服务器必须要求注册重定向 URL 以维护流程的安全性。
没有客户端机密的授权代码授予是安全的唯一方法是使用“state”参数并将重定向 URL 限制为受信任的客户端。由于未使用秘密,因此除了使用已注册的重定向 URL 之外,无法验证客户端的身份。这就是为什么您需要使用 OAuth 2.0 服务预先注册您的重定向 URL。
对于基于浏览器的应用程序,由于网站中的Attack面和移动部件数量增加,因此始终存在跨站点脚本 (XSS) Attack等风险。此外,浏览器目前没有可用于存储访问令牌或刷新令牌等内容的安全存储机制。因此,与其他平台相比,浏览器在 OAuth 部署中始终被认为具有更高的风险,并且授权服务器通常会针对令牌生命周期制定特殊策略以减轻该风险。
从历史上看,在隐式流程中,从来没有任何机制可以将刷新令牌返回给 JavaScript 应用程序。这在当时是有道理的,因为众所周知,隐式流的安全性较低,并且如果没有客户端密钥,刷新令牌可以无限期地用于获取新的访问令牌,因此这比泄漏的风险更大访问令牌。也几乎不需要刷新令牌,因为 JavaScript 应用程序只会在用户积极使用浏览器时运行,因此它们可以在需要时重定向到授权服务器以获取新的访问令牌。
随着过去几年为 JavaScript 应用程序采用 PKCE 的发展,现在也有可能向 JavaScript 应用程序发布刷新令牌。这最终成为授权服务器关于是否颁发刷新令牌的政策决定,具体取决于授权服务器愿意承受的风险级别。
此外,浏览器 API 的添加意味着ServiceWorkers
现在基于浏览器的应用程序有可能在用户未主动使用浏览器时运行代码,例如响应后台同步事件。这意味着现在在浏览器应用程序中有更多可能使用刷新令牌。
如果授权服务器希望允许 JavaScript 应用程序使用刷新令牌,那么它们还必须遵循“ OAuth 2.0 安全最佳当前实践”和“基于浏览器的应用程序的 OAuth 2.0 ”中概述的最佳实践,这是 OAuth 最近采用的两个文档工作小组。具体来说,刷新令牌必须仅对一次使用有效,并且授权服务器必须在每次发布新的访问令牌以响应刷新令牌授予时发布一个新的刷新令牌。这为授权服务器提供了一种检测刷新令牌是否已被攻Attack复制和使用的方法,因为在应用程序的正常运行中,刷新令牌只会被使用一次。
刷新令牌还必须具有设置的最长生命周期,或者如果在一段时间内未使用则过期。这又是另一种帮助减轻刷新令牌被盗风险的方法。
基于浏览器的应用程序需要在授权流程中临时存储一些信息,然后永久存储生成的访问令牌和刷新令牌。这在浏览器环境中提出了一些挑战,因为目前浏览器中没有通用的安全存储机制。
通常,浏览器的LocalStorage
API 是存储此数据的最佳位置,因为它提供了最简单的 API 来存储和检索数据,并且与您在浏览器中获得的一样安全。缺点是页面上的任何脚本,即使来自不同域(例如您的分析或广告网络),也将能够访问LocalStorage
您的应用程序。这意味着您存储的任何内容都LocalStorage
可能对您页面上的第三方脚本可见。
由于第三方脚本存在数据泄露的风险,因此为您的应用配置良好的内容安全策略非常重要,这样您就可以更加确信任意脚本无法在应用程序中运行。OWASP 提供了一份关于配置内容安全策略的好文档,网址为https://owasp.org/www-project-cheat-sheets/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
由于在纯 JavaScript 环境中执行 OAuth 流程的固有风险,以及在 JavaScript 应用程序中存储令牌的风险,还建议考虑另一种架构,其中 OAuth 流程在 JavaScript 代码之外处理动态后端组件。这是一种相对常见的架构模式,其中应用程序由动态后端(如 .NET 或 Java 应用程序)提供服务,但它使用单页应用程序框架(如 React 或 Angular)作为其 UI。如果您的应用程序属于这种架构模式,那么最好的选择是将所有 OAuth 流程移动到服务器组件,并将访问令牌和刷新令牌完全保留在浏览器之外。请注意,在这种情况下,由于您的应用程序具有动态后端,
此模式在“基于浏览器的应用程序的 OAuth 2.0 ”中有更详细的描述。