解密OAuth安全机制:State、Nonce与PKCE的核心差异与应用

本文深入解析OAuth 2.0与OpenID Connect中的三个关键安全参数:State、Nonce与PKCE,阐述它们各自如何防御CSRF、令牌重放和授权码拦截攻击,并阐明为何需要在认证流程中同时使用这三者以构建分层防御体系。

在构建安全的身份验证和授权流程时,OAuth 2.0和OpenID Connect(OIDC)规范是行业标准。但作为开发者,我们很快会遇到几个令人困惑的神秘参数:state、nonce、code_challenge和code_verifier。后两个与一个名为"Proof Key for Code Exchange"(PKCE)的安全机制相关。

所有这些参数看起来都是在登录流程中传递的随机秘密字符串。这引出了一个常见且有时很危险的问题:它们可以互换吗?PKCE是否取代了对state的需求?如果我使用了state,是否还需要nonce?

简短而有力的回答是:不,它们不可互换。

可以将它们视为三位不同的安全专家,各自在认证流程的不同阶段守卫着不同的门。使用其中一个不能也做不到其他参数的工作。它们协同工作,形成分层防御,保护您的应用程序和用户免受各种攻击。

为了更好地理解它们,让我们逐一分析,看看每个参数旨在解决的具体问题。

State参数:预防OAuth CSRF

让我们从state参数开始。它的存在是为了回答您应用程序的一个简单问题:“这个传入的登录响应是我实际请求的吗?”

它的全部目的是防止一类称为跨站请求伪造的攻击。

攻击方式:跨站请求伪造 想象一个用户登录了他们的网上银行。在另一个浏览器标签页中,他们访问了一个恶意网站。该网站可能有一个隐藏的链接或表单,该链接或表单会伪造一个OAuth授权响应,并将其发送到用户网上银行的回调URL,如下所示: https://online-bank.com/callback?code=FAKE_CODE_FROM_ATTACKER 这是一个"伪造的请求"。攻击者诱使用户的浏览器向其银行应用程序发送一个并非由用户发起的请求。如果没有state参数,您应用程序的回调端点可能会收到这个伪造的code。如果攻击者足够聪明,他们可能利用这个媒介欺骗银行应用程序,将用户的会话与攻击者的账户关联起来,即"登录CSRF"攻击。

解决方案:添加State参数 state参数通过充当"凭证"来解决此问题。它是一个不透明、随机且不可猜测的字符串,由您的应用程序在认证流程开始前生成。

流程变为一个安全、可验证的循环。首先,您的客户端应用程序生成这个唯一的state值。然后保存这个值,通常保存在用户的会话或安全的短期HttpOnly cookie中。这个值现在与这个特定用户的浏览器会话相关联。

接下来,您的应用程序将用户重定向到授权服务器的登录页面,并在URL中将state值作为查询参数包含其中。授权服务器对此值一无所知;它唯一的工作是保留这个值,并在将用户重定向回您的应用程序时原样返回。

当用户返回到您的redirect_uri时,您的应用程序必须做的第一件事就是比较传入请求中的state值与存储在用户会话中的state值。

如果匹配,这是一个合法的响应。您的应用程序可以自信地说:“我认得这个。这是我刚刚为这个特定用户发出的请求的响应。“现在,您可以继续执行下一步:交换授权码。

如果不匹配,这就是红色警报。这个请求是未经请求的。它不是源自您的应用程序,或者被发送给了错误的用户。您的应用程序必须立即拒绝该请求,记录该尝试,并显示错误。

state参数是您的应用程序在HTTP重定向过程中保持上下文,并确保其处理的响应不是伪造的方式。

Nonce参数:防止ID令牌重放

接下来,我们有nonce。这个参数特定于OpenID Connect,这是构建在OAuth 2.0之上的认证层。如果您仅使用OAuth进行授权,则不会使用nonce。但如果您使用OIDC登录用户并获取ID令牌,则nonce参数至关重要。

nonce(“一次性数字”)的存在是为了回答一个不同的问题:“我刚收到的这个ID令牌真的是为我刚刚发起的登录而创建的吗?”

它的主要工作是防止ID令牌重放攻击。

攻击方式:ID令牌重放 ID令牌是一个签名的JSON Web令牌。它是一个数字身份证,证明用户的身份以及他们何时进行了认证。但是,如果攻击者以某种方式截获了之前合法登录的有效ID令牌呢?

攻击者可以尝试将此令牌"重放"给您的应用程序。他们可能试图欺骗您的应用程序启动新的登录流程,然后在适当时机,用他们窃取来的ID令牌进行替换。如果没有nonce,您的应用程序可能会接受此令牌。它会检查签名,看到它是有效的,然后将攻击者作为窃取令牌中的用户登录。

解决方案:添加Nonce参数 nonce参数通过将ID令牌加密绑定到请求它的特定客户端会话来解决此攻击。

流程与state类似,但在验证位置上有至关重要的区别。与state一样,您的客户端应用程序在重定向前生成一个唯一的、不可猜测的nonce值,并将其存储在用户会话中。它在授权请求中将此nonce值与state一起发送。

授权服务器在认证用户后,将您发送的nonce值作为声明嵌入到ID令牌的JWT有效负载中,如下例所示:

1
2
3
4
5
6
7
8
{
  "iis": "https://your-domain.auth0.com",
  "sub": "1234567890",
  "name": "John Doe",
  "nonce": "q-385k-2Pc7",
  "iat": 1516239022,
  "exp": 1516240022
}

当您的应用程序收到ID令牌后,其验证过程现在变为双重。它当然必须验证令牌的签名。但它还必须:

  1. 解码ID令牌。
  2. 从令牌内部提取nonce声明。
  3. 将此nonce声明与存储在用户会话中的nonce值进行比较。

如果匹配,应用程序就知道此ID令牌是刚刚生成的,并且是为这个登录请求专门创建的。如果不匹配,这就是一次重放攻击。该令牌要么是旧的,要么是为不同会话准备的。您的应用程序必须拒绝它。

这确保了即使是有效签名的ID令牌也不能被重用以在新登录流程中冒充用户。

PKCE:保护授权码

最后,我们来看PKCE。这是OAuth 2.0的一个较新扩展,最初用于公共客户端,但现在被认为是所有客户端类型的强制性最佳实践。

PKCE为授权服务器回答一个问题:“正在交换此授权码以获取令牌的客户端,与最初请求授权码的客户端是同一个吗?”

它的设计目的是防止授权码拦截攻击。

攻击方式:授权码拦截 此漏洞对公共客户端最为危险,例如移动应用和基于浏览器的单页面应用。这些客户端无法安全地存储客户端密钥,因为任何捆绑在应用程序中或在浏览器中运行的代码都可能被逆向工程。

在标准的授权码流程中,授权服务器将授权码发送回客户端的redirect_uri。在移动设备上,这可能涉及自定义URL方案。同一设备上的恶意应用程序可能注册自己来处理相同的URL方案,拦截此回调,并窃取授权码。

攻击者窃取授权码后,可以将其发送到令牌端点。由于公共客户端的令牌端点不需要客户端密钥,服务器无法知道这不是合法的应用程序。它会乐意地将窃取的授权码换成有效的访问令牌,从而让攻击者完全访问用户的账户。

解决方案:使用PKCE PKCE通过要求客户端证明它是启动流程的同一客户端来防止这种攻击。这是一个加密扩展,工作原理类似于两部分秘密握手。

流程现在涉及两个新值。首先,在重定向之前,您的客户端应用程序会生成一个唯一的、随机的、高熵的字符串,称为code_verifier。这是秘密。

其次,您的客户端转换code_verifier,以创建code_challenge。这是秘密的公共部分。

您的应用程序将秘密的code_verifier保存在其本地存储中。然后,它在初始授权请求中将公共的code_challenge发送给授权服务器。授权服务器存储此code_challenge,将其与即将生成的授权码关联起来。

当用户返回时,客户端收到授权码。现在,关键步骤:当客户端前往/token端点交换授权码时,它需要包含授权码以及它保存的原始秘密code_verifier。

授权服务器收到此请求并执行检查:

  1. 从请求中获取code_verifier。
  2. 使用客户端声明的相同方法对其进行哈希处理。
  3. 将结果与之前存储的code_challenge进行比较。

如果匹配,授权服务器就知道此请求另一端的客户端正是启动流程的同一客户端。可以安全地颁发令牌。

如果不匹配,这就是冒名顶替者。攻击者只有授权码,而没有原始的code_verifier秘密。授权服务器必须拒绝该请求。

PKCE有效地为每次登录流程创建了一个动态的一次性秘密,即使对于处于不安全环境中的客户端,也能保护授权码免遭盗窃和滥用。

为何需要三者结合

现在应该很清楚,这三个参数并非冗余。它们是分层安全的完美示例,在流程的不同阶段保护不同的资产。

遵循以下步骤,让我们设想攻击者在试图入侵一个配置得当的OIDC流程时非常糟糕的一天:

  1. CSRF攻击:攻击者使用伪造的授权码制作恶意链接,并将其发送给已登录的用户。
  2. 用户的浏览器将请求发送到客户端的redirect_uri。客户端检查URL中的state参数,将其与用户会话中的state进行比较,发现缺失或不匹配,并拒绝请求。state参数保护了授权请求步骤
  3. 代码拦截攻击:攻击者在用户的移动设备上,并成功拦截了来自合法登录的真实授权码。
  4. 攻击者将窃取的授权码发送到令牌端点。期待PKCE流程的授权服务器要求提供code_verifier。攻击者没有。PKCE保护了授权码交换步骤
  5. 令牌重放攻击:攻击者以某种方式绕过了所有防护,并获取了之前会话中的有效ID令牌。他们发起新的登录,并尝试将此窃取的令牌发送给客户端。
  6. 客户端刚刚发起了新的登录,其会话中存储了一个新的nonce。它解码攻击者窃取的ID令牌,查看其内部的nonce声明,发现与会话中的nonce不匹配。nonce参数保护了令牌颁发/验证步骤

您不是在state、nonce和PKCE之间"选择”。您是在利用这三者来围绕您的认证流程构建一个堡垒。

对比总结

现在您已经清楚地了解了state、nonce和PKCE。请记住以下总结表格中的概念:

参数 针对的攻击 保护阶段 由谁验证 OAuth / OIDC
State 登录CSRF 授权请求/回调 客户端应用程序 OAuth 2.0
Nonce ID令牌重放 令牌颁发/验证 客户端应用程序 OpenID Connect
PKCE 授权码拦截 授权码交换 授权服务器 OAuth 2.0 PKCE 扩展
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计