JWT 的解析与签名、声明校验
目标
手动拆解 JWT、验证签名并检查 claim,从而理解库替你完成的工作究竟是什么。
为什么重要
JWT 验证通常只需一行库调用,因此很容易在不了解内部原理的情况下略过。但如果那一行的选项配置错误,就会悄无声息地产生漏洞。典型例子是没有启用 aud 检查时,同一认证服务器为其他 API 签发的令牌也能通过本 API。若不验证 alg,现实中还曾发生过利用 none 算法使无签名令牌通过的事故。本实验最后测量的数字也很重要:introspection 每次请求都需要一次网络往返,而本地验证只是 CPU 运算。如果十个微服务分别询问,认证服务器就会成为瓶颈。因此实际工作中通常将访问令牌寿命设为 5~15 分钟,并使用本地验证。这是一种把撤销生效延迟限制为令牌寿命的权衡。
步骤
- 使用
/root/kc/wait.sh等待http://127.0.0.1:8080/realms/master返回 200,最长等待 180 秒。在/root/kc/ready.txt中写入ready_seconds=<정수>。 - 使用
/opt/fixtures/kc/realm-info.env中的值签发令牌,只把访问令牌以单行保存到/root/kc/token.txt。它必须是包含 2 个点的字符串。 - 使用
/root/kc/decode.py将 header 保存到/root/kc/header.json,payload 保存到/root/kc/claims.json。两者都必须是有效 JSON。 - 在
/root/kc/claimcheck.txt中写入iss=<값> aud=<값> sub=<값> exp_in=<남은초>。exp_in必须大于 0。 - 从
http://127.0.0.1:8080/realms/labhub/protocol/openid-connect/certs获取 JWKS,将与 header 中kid匹配的 key 保存到/root/kc/jwk.json。kty必须是RSA。 - 使用
/root/kc/verify.py验证签名,并在/root/kc/verify.out中写入signature=valid alg=<값>。 - 使用 payload 被篡改的令牌进行相同验证,并在
/root/kc/tamper.out中写入signature=invalid。
参考
- Base64URL 解码时补齐 padding:
s + '=' * (-len(s) % 4) - 签名对象就是
<header_b64>.<payload_b64>这一原始字符串。 - ID token 与 access token 不同。调用 API 时使用 access token。
- 常见错误 1:不检查
aud,导致为其他 API 签发的令牌也能通过。 - 常见错误 2:不检查 header 中的
alg,为none算法绕过留下通道。
等待 Keycloak 准备就绪
使用 /root/kc/wait.sh 等待 http://127.0.0.1:8080/realms/master 返回 200,最长等待 180 秒。在 /root/kc/ready.txt 中写入 ready_seconds=<정수>。
由于使用 JVM,启动较慢。请用循环轮询就绪状态,并留出充足的最长等待时间。
签发令牌
使用 /opt/fixtures/kc/realm-info.env 中的值签发令牌,只把访问令牌以单行保存到 /root/kc/token.txt。它必须是包含 2 个点的字符串。
预先导入的 realm 信息位于 /opt/fixtures/kc/realm-info.env。向令牌端点发送表单数据。
解码 header 与 payload
使用 /root/kc/decode.py 将 header 保存到 /root/kc/header.json,payload 保存到 /root/kc/claims.json。两者都必须是有效 JSON。
它们是由点分隔的三个片段中的前两个。Base64URL 可能没有 padding,因此需要补齐。
检查必要 claim
在 /root/kc/claimcheck.txt 中写入 iss=<값> aud=<값> sub=<값> exp_in=<남은초>。exp_in 必须大于 0。
分别检查 issuer、audience、expiration 和 subject。遗漏任何一个都会留下漏洞。
从 JWKS 中寻找签名 key
从 http://127.0.0.1:8080/realms/labhub/protocol/openid-connect/certs 获取 JWKS,将与 header 中 kid 匹配的 key 保存到 /root/kc/jwk.json。kty 必须是 RSA。
通过 header 中的 key identifier 从列表中选择。轮换期间可能同时存在多个 key。
验证签名
使用 /root/kc/verify.py 验证签名,并在 /root/kc/verify.out 中写入 signature=valid alg=<값>。
签名对象是用点连接 header 和 payload 得到的字符串,而不是解码后的 JSON。
确认篡改令牌会被拒绝
使用 payload 被篡改的令牌进行相同验证,并在 /root/kc/tamper.out 中写入 signature=invalid。
即使只修改 payload 中一个字符,签名也应失效。