KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

03 · 授权码与 PKCE — keel 龙骨

类型:衍生概念(服务于「重定向与回跳」) · 应用课对应:02 章 · 授权码流与 PKCE

类型:衍生概念(服务于「重定向与回跳」) · 应用课对应:02 章 · 授权码流与 PKCE

这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。它不讲怎么实现一个授权服务器。

一、它为什么会出现

回跳是经过浏览器走的(02 页),而浏览器经过的每一站都会留痕:地址栏历史、代理日志、服务器的访问日志、Referer 头。

两个现象:

① 如果回跳地址里直接带 access_token,它就会进浏览器历史、进每一级访问日志
② 谁从日志里捡到那段串,谁就能冒充这个人调接口

它面对的需求是:让"经过浏览器的那一段"里,不含任何拿到手就能用的凭据。

答案是把凭据拆成两半:经过浏览器的只是一张兑不出来任何东西的凭据,真正能用的东西由应用在背后换。

二、它是什么

一句话:授权码是 IdP 发出的一张"兑换凭据"——它本身调不了任何接口,只能用一次,且必须由应用在背后换成令牌。

而 PKCE 是给这张凭据配的一把锁:code_verifier 是发起登录的那一方自己造的随机串,code_challenge 是它的摘要;只有同时握着这两个里应外合的值的那个应用,才能把 code 换成令牌。

它不是下面三样东西:

三、它长什么样

一次真实登录里,这两个值由应用在发授权请求之前就生成好(环境:Keycloak 26.8.1,realm = sso-lab):

code_verifier  = SsVH2Ja25l4B-VkTksOhmYU7qrBJU1pQ-pRfywZv470
code_challenge = 8Vklj4QX3rXa9CurOob2v3rc6v8ei2wZzj6s7Uij7tY   (= S256(verifier))

回跳带回来的那个 code(经过浏览器的那一段):

code = 44f80113-b402-a38e-bbc0-56310447ebf8.VnXksyjhgJexcsLOoOE5fc8c.74717848-0bf1-4f44-81f2-e31422eb12c8

IdP 侧支持两种摘要方式,登记在发现文档里:

code_challenge_methods_supported = ['plain', 'S256']

两个值归谁管,是这一页最该记住的一张表:

值 谁生成 谁保存 什么时候拿出来
code_verifier 发起登录的应用 只有应用自己 换令牌那一刻
code_challenge 应用(对 verifier 求摘要) 随授权请求发给 IdP IdP 收到时就用它把这次登录记下来

兑换失败时 IdP 给的原话:

# 用被篡改的 verifier 去换
400 {"error":"invalid_grant","error_description":"PKCE verification failed: Code mismatch"}

# 同一个 code 再用一次(这次 verifier 是对的)
400 {"error":"invalid_grant","error_description":"Code not valid"}

第二行是一个反直觉的地方:被篡改的 verifier 失败的那一次,也已经把 code 消耗掉了。 所以"用错 verifier 失败后、再用对的 verifier 重试"是拿不到令牌的——重试必须从授权请求重新开始。

四、它怎么工作

① 生成  应用造一个 code_verifier,对它求一次摘要得到 code_challenge
② 带上  授权请求里带上 code_challenge;IdP 把它跟这次登录绑在一起记下
③ 回跳  浏览器带回 code —— 它在地址里,但单独它什么也换不出
④ 兑换  应用在背后用 code + code_verifier 换令牌;IdP 现算一次摘要,跟 ② 记下的比

PKCE 防的那一类攻击在第 ③④ 步之间:别人从日志或 Referer 里捡到了 code,但他没有 code_verifier,摘要对不上,换不出令牌。

而 code 一次性的属性,防的是另一件事:即使这张凭据泄了,它的可用窗口也只有一次。 两个性质合起来,才把"经过浏览器的东西"压到了无害。

五、它会带出哪些概念

六、回到应用课

应用课里的那句话 现在你知道它为什么成立
"PKCE 到底防住了什么" 它让 code 只对"发起这次登录的那个应用"有用:别人截到 code 也没有 code_verifier,摘要对不上
"应用不该从 code 里面读任何东西" code 不是结论,只是一张兑换凭据;结论在换回来的 ID Token 载荷里
"PKCE 失败那次也消耗了 code" 兑换无论成败都算一次消耗;所以重试必须从授权请求那一头重新走

这一页改变了你的哪个判断:看到回跳里那个 code,你不再只当它是一段"马上去兑换的字符串",而是先问**"这次兑换要用的 code_verifier 在谁手上、还在不在"**——verifier 丢了,code 再新也换不出东西。

→ 这一页的展开在应用课 02 章 · 授权码流与 PKCE。

自测题

  1. code_verifier 与 code_challenge 分别是谁生成、谁保存、什么时候拿出来?
  2. 一次兑换因为 verifier 不对而失败,为什么"再拿正确的 verifier 重试"也拿不到令牌?

进入 keel 阅读