08. 锦上添花:PKCE和授权码模式
授权码拦截攻击
在第(4)小节——客户端注册中我和你讨论了 OAuth 2.0 RFC 文档规约的两种客户端类型——机密客户端(confidential client)和公共客户端(public client),一般的 Web 应用程序被视为机密客户端,而其他无法安全地保存客户端ID和客户端密钥的客户端称为公共客户端。公共客户端很容易遭受授权码拦截攻击。一旦攻击者获得了授权码,它就可以使用授权码来获取可用于访问用户资源的“钥匙”——访问令牌。下图展示了这种攻击的过程。
如上图所示,在用户的终端设备上同时存在两个应用——合法应用和恶意应用
- 合法应用通过浏览器或者操作系统向授权服务器发起授权请求(Authorzation Request),通常在这种情况下授权请求中的回调地址(redirect_uri)使用自定义URI协议(custom URI scheme)。自定义URI协议允许移动操作系统将控制权从另一个外部应用程序(例如从系统浏览器)传递回你的应用程序。如果你在浏览器上向特定于应用程序的自定义URI协议发送一些参数,移动操作系统将跟踪这些参数并使用这些参数调用相应的本地应用程序。
- 自定义 URI 协议会存在一些问题,例如在相同的移动环境中,自定义URI可能会相互冲突。例如,两个 APP 可以注册相同的 URI 协议。如上图所示,恶意应用可能会同样将自己注册为自定义URI协议的处理程序。
- 因为 OAuth 要求必须使用 TLS 通信,因此在第(3)步中授权服务器安全地返回了授权码(Authz Code)。
- 在第(4)步中,当浏览器命中自定义URI协议时,会调用当前自定义URI协议已注册和绑定的本地应用程序。此时恶意APP将有机会拦截步骤(4)中的授权码,并且在第(5)和第(6)步获得使用该授权码交换访问令牌。
什么是PKCE
如上文所述,由于本地应用程序被视为是公共客户端,它的客户端ID是公开且容易获得的,因此上述这种拦截授权码并且使用截获的授权码交换访问令牌的风险是可能发生的。为了对抗这种攻击,OAuth 工作组制定了一项 Proof Key for Code Exchange (简称为PKCE, 读作“pixy”)的扩展技术,该技术被定义在 RFC 7636 文档(https://datatracker.ietf.org/doc/html/rfc7636)。
简而言之,PKCE 是一种确保请求授权码与使用授权码换取访问令牌的应用程序是同一应用程序的机制。PKCE 可以防止恶意进程拦截授权码并使用被拦截的授权码获取访问令牌。
值得注意的是 PKCE 最初设计是用来保护本地 APP,但在 OAuth 2 最佳安全实践的2.1.1小节里(https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-18#section-2.1.1)要求公共客户端必须使用PKCE,对于机密客户端推荐使用 PKCE 来增强授权码交换的安全性。与此同时,在OAuth 2.1草案中已经找不到隐式授权类型的描述,作为替代你可以使用授权码类型+PKCE。
授权码模式+PKCE
那么PKCE是如何保证发出授权请求的客户端和请求令牌的客户端是同一客户端呢?如下图授权码模式的时序图所示。
- 在第(2)步中客户端初始化授权请求时,先生成一个额外的参数——code_verifier 。RFC 7636 规定code_verifier 的值必须是一个高熵加密且随机的字符串,它由[A-Z] 、 [a-z] 、 [0-9] 、 "-" 、 "." 、 "_" 、 "~"等这些字符组成,并且长度最好在43到128个字符之间。客户端生成该高熵字符串后并不直接将它附加在授权请求中,而是先对 code_verifier 进行 SHA-256 哈希计算,再将哈希之后的值进行 BASE64-URL 编码,最终计算的结果称为挑战码(code_challenge)。最终在授权请求中携带该挑战码。同时客户端还要将code_verifier的值存储在会话 Cookie 或其他本地存储中,在第(5)步发送令牌请求时需要携带code_verifier。挑战码的生成伪代码如下:
code_challenge = BASE64URL-ENCODE(SHA256(code_verifier))
注意在授权请求中还多了另外一个参数——code_challenge_method。一般的,code_challenge_method 的值都是S256,表示 code_challenge 的值是对 code_verifier 进行 SHA-256 哈希计算的结果,但对于一些没有能力进行SHA256 哈希计算的客户端,PKCE 允许直接将 code_verifier 的值赋值给 code_challenge,此时code_challenge_method 的值就是 plain。完整的授权请求示例如下:
GET /oauth/authorize?
response_type=code
& client_id=[CLIENT_ID]
& state=[STATE]
& scope=[SCOPE]
& redirect_uri=[REDIRECT_URI]
& code_challenge=[CODE_CHALLENGE]
& code_challenge_method=S256 HTTP/1.1
Host: authorizationsever.com
- 在第(5)步中,授权服务器在收到用户的授权许可后向客户端颁发授权码,同时授权服务器必须将它生成的授权码与授权请求中的 code_challenge 和 code_challenge_method 进行关联和保存。我将在授权服务器实现的章节向你介绍这一细节。
- 在第(6)步中,授权服务器将使用授权码换取访问令牌。除了在第(7)小节授权请求中定义的参数之外,客户端还要将它在第(1)步中保存在本地的 code_verifier 发送到授权服务器。完整的令牌请求示例如下:
POST /token HTTP/1.1 Host: authorizationsever.com content-type: application/x-www-form-urlencoded
grant_type=authorization_code
& code=[AUTHORIZATION_CODE]
& client_id=[CLIENT_ID]
& client_secret=[CLIENT_SECRET]
& code_verifier=[CODE VERIFIER]
& redirect_uri=[REDIRECT_URI]
- 授权服务器在收到令牌请求后,除了校验 code、client_id、client_secret以及redirect_uri 等参数的合法性外,授权服务器还将校验 code_verifier 参数值。如上所述,在第(5)步中授权服务器将它生成的授权码与授权请求中的 code_challenge 和 code_challenge_method 进行关联和保存。授权服务器在收到令牌请求后:
- 校验当前授权码是否存在。如果存在,查询当前授权码关联的 code_challenge 和 code_challenge_method。
- 如果关联的 code_challenge_method 是plain,授权服务器只需要比对当前令牌请求中的 code_verifier 是否等于授权服务器端保存的 code_challenge。
- 如果 code_challenge_method 是 SHA256,授权服务只需要按照第(2)步中客户端初始化授权请求时计算挑战码的方法对 code_verifier 进行哈希计算和 BASE64-URL 编码,如果计算的结果等于授权服务器端保存的code_challenge,那么就可以说授权请求和令牌请求来自同一客户端。否则令牌请求将返回 invalid_grant 的错误。
更进一步,PKCE 技术是怎么防止授权码被拦截的
让我们回顾在本节开始的示例,如下图所示。
- 在第(1)步中合法应用生成了 code_verifier,但是它并没有直接使用 code_verifier 进行授权请求,而是对code_verifier 进行哈希计算后生成对应的 code_challenge,在授权请求中将暴露和使用 code_challenge。
- 在第(3)步中授权服务器发放了授权码,同时它会将授权请求中的 code_challenge 和code_challenge_method 与发放的授权码进行关联并保存。
- 第(4)步中恶意应用拦截了授权码,它企图使用该授权码换取访问令牌。但是它不知道合法应用保存在本地存储中的 code_verifier。
- 授权服务器收到该授权码后并不会直接发放访问令牌,它会根据该授权码绑定的 code_challenge_method 和令牌请求中传递的 code_verifier计 算出一个新的值,并校验该值是否与授权码绑定的 code_challenge 值一致,如果一致则发放访问令牌,否则认为授权请求和令牌请求的应用不是同一个应用,返回异常信息。
- 推荐 code_challenge_method 使用 SHA256 而非 plain 的原因是,在极端情况下恶意应用有可能观测到第(1)步授权请求中的 code_challenge 值,如果使用 plain,恶意应用会使用第(1)步拦截到 code_challenge(此时 code_challenge 的值就是code_verifier)和第(4)步拦截到的授权码,在第(5)和第(6)步顺利换到访问令牌。当 code_challenge_method 使用 SHA256 时,因为 SHA256 哈希计算是单向的,合法应用将 code_verifier 转换成 code_challenge,即使恶意应用在第(1)步拦截到 code_challenge 的值,它也绝无可能还原回在令牌请求中需要的 code_verifier。所以它是更安全的。
总结
- PKCE 技术最初是为公共客户端设计的。授权码拦截攻击可能出现在运行在用户浏览器的纯JavaScript应用程序或移动应用程序中,而OAuth 2.0最佳安全实践里推荐对于机密客户端也同样应该使用PKCE来防止授权码拦截攻击。我在实战篇里将会搭建支持PKCE的授权服务器和客户端角色,为你演示PKCE的工作过程。
- 我将在第(10)小节中讨论如何在公共客户端中使用授权码模式+PKCE来代替简化模式。
-
在最后我补充讨论一下编码、加密和哈希的区别,以便你能理解为什么 PKCE 推荐使用 SHA256 作为加密code_verifier 的方法。如下图所示。
- 编码它不是一种加密算法,它本质上是信息形式的转化。编码的目的不是为了加密信息,是将消息转化成统一的格式,方便在不同系统之中传输,例如常见的Base64编码。你可以很轻松地将编码后的文本内容解码得到最开始的文本而不需要任何成本。
- 加密是为了保证数据安全传输,使得其他人不能获取的具体信息内容。一般在加密的过程中你需要使用一把钥匙,在解密的时候你需要使用同一把或者不同的钥匙去解密才能得到最开始的文本内容。
- 哈希算法也称摘要算法,是指把可变长度的数据通过运算得到固定长度散列值的不可逆算法,它是单向的,只要原始数据稍微改动得到的散列值机会完全不同。例如我们上面用到的SHA256算法。即使你可以拿到挑战码的值,但是你绝无可能还原拿到最原始的code_verifier的值。这样就保证了发送授权请求的客户端和使用授权码的客户端是同一客户端。


浙公网安备 33010602011771号