LabHub
学习 学习路径 课程

Keycloak 与企业认证

把 authorization code + PKCE 流程跑完整

在 LabHub 中继续学习

目标

在不使用 SDK 的情况下从头到尾执行 authorization code + PKCE 流程,并通过失败案例确认 PKCE 和 state 分别防止什么攻击。

为什么重要

使用 OIDC 库时,这套流程只是两次函数调用。因此,人们往往在不了解具体过程的情况下使用它,出现问题时也不知道该查看哪里。在本实验中手动执行各个阶段后,会明确三点。第一,授权码会暴露在浏览器 URL 中,但为什么仅凭授权码毫无用处。第二,不知道 code_verifier 时为什么交换会失败——你将在第 6 步亲自使其失败。第三,没有 state 时,攻击者为什么能让受害者登录到攻击者自己的账号。完成最后的刷新轮换后,还能理解实际工作中推荐的组合(短期访问令牌 + 刷新令牌轮换)为什么无需黑名单也能产生失效效果。

Keycloak 启动需要 40~90 秒,因此完成前面的实验后,它通常已经准备就绪。

步骤

  1. 使用 /root/pk/pkce.py 生成 code_verifier(43~128 个字符,使用 URL 安全字符)和 code_challenge,分别以单行保存到 /root/pk/verifier.txt/root/pk/challenge.txt
  2. /root/pk/authz_url.txt 中写入一行授权请求 URL。目标是预先导入的 realm labhub 中的 public client web-app,redirect URI 为 http://127.0.0.1:8161/callback。必须包含 response_type=codeclient_idredirect_uriscope=openidstatecode_challengecode_challenge_method=S256
  3. 保持 cookie jar,GET 该 URL,并将登录表单的 action URL 保存到 /root/pk/action.txt。它必须以 http 开头。
  4. POST 用户 dev1 / 密码 Dev1!pass,从重定向响应的 Location 中提取授权码,保存到 /root/pk/code.txt。长度必须至少为 20 个字符。
  5. 向令牌端点发送 grant_type=authorization_code、授权码、redirect_uriclient_idcode_verifier 以获取令牌。/root/pk/tokens.json 中必须包含 access_tokenrefresh_token
  6. 获取新的授权码,尝试使用错误的 code_verifier 进行交换。/root/pk/pkce_fail.json 中的 error 必须是 invalid_grant
  7. /root/pk/state_check.py 接收两个参数(发送的 state、收到的 state),相同则以退出码 0 结束,不同则以 1 结束。在 /root/pk/state.out 中将匹配与不匹配两种结果写为 match=ok mismatch=rejected
  8. 使用刷新令牌进行刷新,生成 /root/pk/refresh.json。在 /root/pk/rotation.txt 中写入 rotated=<true|false>。判断依据是新刷新令牌是否与旧令牌不同。

参考

创建 code_verifier 和 challenge

使用 /root/pk/pkce.py 生成 code_verifier(43~128 个字符,使用 URL 安全字符)和 code_challenge,分别以单行保存到 /root/pk/verifier.txt/root/pk/challenge.txt

规范规定了长度限制和字符集。不能把哈希结果直接进行普通 Base64;必须使用 URL 安全编码,并去掉 padding。

组装授权请求 URL

/root/pk/authz_url.txt 中写入一行授权请求 URL。目标是预先导入的 realm labhub 中的 public client web-app,redirect URI 为 http://127.0.0.1:8161/callback。必须包含 response_type=codeclient_idredirect_uriscope=openidstatecode_challengecode_challenge_method=S256

有六个必填参数。缺少任意一个,认证服务器都会返回错误。

提取登录表单的 action

保持 cookie jar,GET 该 URL,并将登录表单的 action URL 保存到 /root/pk/action.txt。它必须以 http 开头。

GET 授权端点会返回 HTML。必须保持 cookie,下一步才能继续。

POST 凭据以获得授权码

POST 用户 dev1 / 密码 Dev1!pass,从重定向响应的 Location 中提取授权码,保存到 /root/pk/code.txt。长度必须至少为 20 个字符。

请从 HTML 中确认表单字段名称。成功后,重定向响应的位置 header 中会包含授权码。

将授权码交换为令牌

向令牌端点发送 grant_type=authorization_code、授权码、redirect_uriclient_idcode_verifier 以获取令牌。/root/pk/tokens.json 中必须包含 access_tokenrefresh_token

同时发送授权码和原始 verifier。redirect URI 也必须与首次请求时完全相同。

使用错误 verifier 确认失败

获取新的授权码,尝试使用错误的 code_verifier 进行交换。/root/pk/pkce_fail.json 中的 error 必须是 invalid_grant

这就是 PKCE 确实生效的证据。请记录错误码。

实现 state 不匹配验证

/root/pk/state_check.py 接收两个参数(发送的 state、收到的 state),相同则以退出码 0 结束,不同则以 1 结束。在 /root/pk/state.out 中将匹配与不匹配两种结果写为 match=ok mismatch=rejected

比较授权请求中发送的值与回调返回的值。不同就必须拒绝。

确认刷新与轮换

使用刷新令牌进行刷新,生成 /root/pk/refresh.json。在 /root/pk/rotation.txt 中写入 rotated=<true|false>。判断依据是新刷新令牌是否与旧令牌不同。

刷新后会返回新的刷新令牌。它与旧令牌相同还是不同,决定是否发生轮换。