手工复现 OIDC 授权码流程
目标
面对实验用 OIDC 提供者,亲手完成授权码流程,解码并验证 ID token 的签名,再编写验证检查清单。
为什么重要
只通过库使用过 OIDC 的人,发生故障时往往不知道该看哪里。redirect_uri 不匹配、未验证 state、缺少 nonce、重复使用过期 code——只有亲眼看过完整流程,才能形成直觉。
尤其重要的是亲手确认:ID token 采用 base64,任何人都能解码。如果修改 payload 后仍不验证签名,伪造内容也会直接通过。亲自复现后,就会真正理解为什么必须固定 alg。
步骤
- 启动实验用 IdP。
python3 /opt/lab/fixtures/auth/oidc/idp.py 9000(后台运行) 获取 discovery 文档并保存到/root/oidc/discovery.json。 (http://127.0.0.1:9000/.well-known/openid-configuration) 必须包含issuer、authorization_endpoint、token_endpoint、jwks_uri。 - 在
/root/oidc/auth-url.txt中写入一行 authorization URL。必需参数:response_type=code、client_id=labhub-web、redirect_uri=http://127.0.0.1:9100/callback、scope=openid profile email、state=<16자 이상>、nonce=<16자 이상>。 - 调用该 URL 时不要跟随重定向,将响应的
Locationheader 保存到/root/oidc/callback.txt,再从中只提取code值保存到/root/oidc/code.txt。Location 中的state必须与第 2 步发送的值一致。 - 在 token endpoint 用 code 换取 token,将完整响应 JSON 保存到
/root/oidc/token.json。同时发送client_id=labhub-web、client_secret=labhub-secret。必须包含access_token、id_token、token_type,且token_type为Bearer。 - 解码
id_token的 payload,保存到/root/oidc/claims.json。必须包含iss、aud、sub、exp、nonce;aud必须为labhub-web,nonce必须与第 2 步发送的值一致。 - 创建
/root/oidc/verify.sh。它接收两个参数(ID토큰 공개키PEM),签名有效时退出码为 0,否则为非零。公钥位于/opt/lab/fixtures/auth/oidc/idp-public.pem。 - 使用
access_token调用 userinfo endpoint,将结果保存到/root/oidc/userinfo.json。sub值必须与第 5 步claims.json中的sub相同。 - 创建
/root/oidc/checklist.csv,第一行为item,risk。서명、iss、aud、exp、nonce、alg六项都必须出现在item中;risk中要用至少 10 个字符说明不验证该项可能导致的攻击或事故。
参考
- 不跟随重定向:执行
curl -s -D - -o /dev/null "<URL>"后查看Location:行 - base64url 解码:把
-替换为+、_替换为/,补齐 padding(=)后执行base64 -d - 签名验证:签名对象是
<헤더>.<페이로드>字符串,算法为 RS256(SHA-256)openssl dgst -sha256 -verify <공개키> -signature <서명파일> <데이터파일> - 常见错误 1:使用 authorization code 两次。它是一次性的,第二次必定失败。
- 常见错误 2:直接把 base64url 交给
base64 -d,从而产生错误。 - 常见错误 3:token 交换时发送的
redirect_uri与 authorization 时不同。两者必须完全一致。
启动 IdP 并获取 discovery 文档
启动实验用 IdP。
python3 /opt/lab/fixtures/auth/oidc/idp.py 9000(后台运行)
获取 discovery 文档并保存到 /root/oidc/discovery.json。
(http://127.0.0.1:9000/.well-known/openid-configuration)
必须包含 issuer、authorization_endpoint、token_endpoint、jwks_uri。
OIDC 提供者会在标准路径提供配置文档。仅凭这一份文档即可获知全部 endpoint 地址。试着用 jq 提取所需值。
组装 authorization URL
在 /root/oidc/auth-url.txt 中写入一行 authorization URL。
必需参数:response_type=code、client_id=labhub-web、
redirect_uri=http://127.0.0.1:9100/callback、
scope=openid profile email、state=<16자 이상>、nonce=<16자 이상>。
遗漏任何必需参数都会使 IdP 返回错误。state 与 nonce 的职责不同:一个防止 CSRF,另一个防止 token replay。
接收 authorization code
调用该 URL 时不要跟随重定向,将响应的 Location header
保存到 /root/oidc/callback.txt,再从中只提取 code 值
保存到 /root/oidc/code.txt。
Location 中的 state 必须与第 2 步发送的值一致。
即使没有浏览器,也可读取重定向响应的 Location header 来查看 code。必须让 curl 不跟随重定向。
交换 token
在 token endpoint 用 code 换取 token,将完整响应 JSON
保存到 /root/oidc/token.json。
同时发送 client_id=labhub-web、client_secret=labhub-secret。
必须包含 access_token、id_token、token_type,且 token_type 为 Bearer。
token endpoint 使用 POST 和 form 格式。authorization code 只能使用一次,第二次会失败;失败后请重新执行第 2~3 步。
解码 ID token payload
解码 id_token 的 payload,保存到 /root/oidc/claims.json。
必须包含 iss、aud、sub、exp、nonce;
aud 必须为 labhub-web,nonce 必须与第 2 步发送的值一致。
JWT 由点号分隔的三部分组成,每部分都是 base64url。base64url 与标准 base64 有两个字符不同,而且可能没有 padding。
编写签名验证脚本
创建 /root/oidc/verify.sh。它接收两个参数(ID토큰 공개키PEM),
签名有效时退出码为 0,否则为非零。
公钥位于 /opt/lab/fixtures/auth/oidc/idp-public.pem。
签名对象是完整的“header.payload”字符串。可使用 openssl dgst 和公钥验证,签名值需要先进行 base64url 解码。
调用 userinfo
使用 access_token 调用 userinfo endpoint,
将结果保存到 /root/oidc/userinfo.json。
sub 值必须与第 5 步 claims.json 中的 sub 相同。
access token 通过 Authorization header 以 Bearer 方式发送。本步骤的关键是确认返回的 sub 与 ID token 中的 sub 相同。
编写 ID token 验证检查清单
创建 /root/oidc/checklist.csv,第一行为 item,risk。
서명、iss、aud、exp、nonce、alg 六项都必须出现在 item 中;
risk 中要用至少 10 个字符说明不验证该项可能导致的攻击或事故。
不要只写“为什么需要该项”,而要写“不验证会遭受什么攻击”。这样在后续 review 中更有说服力。