PKCE 所挫败的攻击

2.0 授权码流中,授权服务器把一个短命的授权码交给客户端,客户端随后用它换取令牌。机密客户端用一个客户端秘密来保护这次交换。但公共客户端,单页应用与移动应用,无法保守一个秘密:任何发送到浏览器或设备上的东西都可被提取。这便留下一个漏洞。若攻击者拦截了授权码(通过一个为同一重定向注册的恶意应用、一份泄露的日志或一个精心构造的 URL),他便能自行将其兑换为令牌。

,即 Proof Key for Code Exchange(RFC 7636,读作 “pixy”),无需一个预共享的秘密便堵上了那个漏洞。它把授权码绑定到一个唯有真正的客户端知晓的一次性秘密。

验证者与挑战如何契合

该流程增加了两个值:

  • code_verifier 是客户端生成并保留的一个高熵随机字符串。RFC 7636 要求来自非保留集(字母、数字以及 - . _ ~)的 43 到 128 个字符。
  • code_challenge 由验证者派生而来。对 S256 方法,它是 BASE64URL(SHA256(ASCII(code_verifier)))

序列是:

  1. 客户端生成一个新的 code_verifier、派生出 code_challenge,并在授权请求中只发送挑战
  2. 授权服务器存储该挑战,并如常返回一个授权码。
  3. 在令牌请求时,客户端发送验证者
  4. 服务器以同样的方式对验证者做哈希,并将其与所存储的挑战核对。不匹配,则无令牌。

安全性系于一个事实:挑战是验证者的一个单向哈希。一个拦截了授权码的攻击者仍无法完成交换,因为他从未见过验证者,也无法从挑战反推出它。

为何用 S256,而非 plain

RFC 7636 定义了两种方法。plain 方法把挑战设为等于验证者,若授权请求本身被观察到,这便毫无保护。S256 方法只发送 哈希,于是验证者一直隐藏到最终那次受 TLS 保护的令牌调用。只要客户端能计算 SHA-256,便要求使用 S256,而那在一切要紧之处皆然;plain 仅为那种无法做哈希的罕见受限设备而存在。

从可选到强制

PKCE 起初是面向公共客户端的一个扩展,但指导意见已果断转向。OAuth 2.0 Security Best Current Practice(RFC 9700)现在要求所有授权码客户端使用 PKCE,机密客户端也包括在内,因为它还能防御某些代码注入攻击。若你今天正在构建或测试一个 OAuth 或 OpenID Connect 集成,请把 PKCE 当作默认,而非附加项。

PKCE 工具 生成一个合规的验证者、用 Web Crypto 派生其 S256 挑战,并将任意验证者与 RFC 的长度与字符集规则相核对,全部在本地。验证者从不离开你的浏览器。