TT Lab
开始
学习 学习路径 课程

会话与令牌 — 从浏览器到服务网格

client credentials 与令牌交换(亲手搭建 STS)

在 TT Lab 中继续学习

目标

在 Keycloak 中用 client credentials 获取并确认服务令牌,用本地 STS 亲手搭建 Keycloak 26.0.7 所没有的标准令牌交换(RFC 8693),把用户令牌换成下游专用的委托令牌,然后用真实请求证明:下游会检查 aud 和 act,伪造的令牌会被拒绝。

为什么重要

服务代表用户去调用其他服务时,如果把收到的用户令牌原样转发,下游要接受它就得关掉 aud 检查,也不知道是谁代为调用的,权限也没有缩小。一处泄露的令牌就成了整条链的钥匙。如果只使用服务自己名义的令牌(client credentials),这次又会丢失“这是为谁发的请求”。令牌交换保留用户身份,同时把 audience 收窄到下游,并在 act 中留下行为者,从而同时解决这两个问题。这时如果 STS 不验证 subject_token,就会变成一台用自己的签名把伪造令牌洗白的机器;如果下游不看 aud 和 act,交换就没有意义。Keycloak 从 26.2 起正式支持标准交换,所以版本不足时需要自己动手做什么,也会在本实验中看到。

步骤

  1. 如果 Keycloak 是关着的,就用 lab-start-keycloak 启动,并用 /root/st/exchange/wait.sh 最多等 240 秒,直到 http://127.0.0.1:8080/realms/labhub 返回 200,然后在 /root/st/exchange/ready.txt 中写入 ready_seconds=<정수>(占位符为整数)。
  2. 用机密客户端 api-svc 获取 client credentials 令牌,保存到 /root/st/exchange/svc_token.txt,验证签名后,在 /root/st/exchange/svc_claims.txt 中写入 azp=、aud=、username=、roles=、refresh_token=。
  3. 向 Keycloak 发出 token-exchange grant 请求,把结果以 kc_exchange_error=、token_exchange_advertised= 写入 /root/st/exchange/kc_exchange.txt,并在 127.0.0.1:8308 上启动本地 STS /root/st/exchange/sts.py(/health、/jwks、签名密钥 /root/st/exchange/sts_key.pem)。
  4. 以 dev1 令牌为 subject_token、api-svc 令牌为 actor_token,换取 audience=orders-api 的交换令牌,放到 /root/st/exchange/exchanged_token.txt,并把验证过的值写入 /root/st/exchange/exchanged.txt。辅助脚本做成 /root/st/exchange/tokens.sh。
  5. 在 127.0.0.1:8309 上启动下游服务 /root/st/exchange/downstream.py,用四种令牌调用 GET /orders,把状态码写入 /root/st/exchange/downstream.txt。
  6. 把伪造、篡改、无签名的 subject_token、缺少 actor_token、未知的 audience,以及正常的对照组发给 STS,写入 /root/st/exchange/reject.txt。
  7. 用 /root/st/exchange/e2e.sh 测试整个流程,在 /root/st/exchange/e2e.out 中留下七行。

参考

等待 Keycloak 就绪

如果 Keycloak 是关着的,就用 lab-start-keycloak 启动,并用 /root/st/exchange/wait.sh 最多等 240 秒,直到 http://127.0.0.1:8080/realms/labhub 返回 200,然后把耗时按 ready_seconds=<정수>(占位符为整数)写入 /root/st/exchange/ready.txt。

Keycloak 是 JVM,需要 40–90 秒。不要用固定的 sleep,而要用轮询状态码的循环。用 lab-status 可以看到什么在运行。

用 client credentials 获取服务令牌

用机密客户端 api-svc 获取 grant_type=client_credentials 令牌,保存到 /root/st/exchange/svc_token.txt 的第一行,用 Realm 的 JWKS 验证签名后,把 azp=、aud=(用逗号连接)、username=、roles=、refresh_token=present|absent 写入 /root/st/exchange/svc_claims.txt。客户端密钥从 /opt/fixtures/kc/realm-info.env 的 KC_CONFIDENTIAL_SECRET 中读取,不要抄写到文件里。

client credentials 是不依赖用户、以服务自己名义获取的令牌(RFC 6749 第 4.4 节)。令牌端点是 realm-info.env 中的 KC_TOKEN_URL。这是谁的令牌,由 azp 和 preferred_username 说明。也请确认响应 JSON 里有没有 refresh_token。

确认 Keycloak 的局限并启动本地 STS

向 Keycloak 以 grant_type=urn:ietf:params:oauth:grant-type:token-exchange 发出请求,把返回的 error 值以 kc_exchange_error= 写入 /root/st/exchange/kc_exchange.txt,并把 discovery 文档的 grant_types_supported 中是否有该 grant,以 token_exchange_advertised=yes|no 一并写入。然后在 127.0.0.1:8308 上启动本地 STS /root/st/exchange/sts.py。GET /health 返回 {"ok":true},GET /jwks 返回带 kid 的 RSA 公钥,签名私钥放在 /root/st/exchange/sts_key.pem。POST /token 负责处理 RFC 8693 交换(在第 4、6 步评分)。

这个 Pod 的 Keycloak 是 26.0.7,标准令牌交换(V2)从 26.2 开始提供。discovery 地址是在签发方后面加 /.well-known/openid-configuration。STS 必须用 Keycloak 的 JWKS 验证 subject_token(PyJWT 的 PyJWKClient),form 正文用 parse_qs 读取。

把用户令牌交换成 orders-api 专用令牌

把 dev1 的用户令牌(web-app,password grant)作为 subject_token,把 api-svc 的 client credentials 令牌作为 actor_token,向 STS 发送 POST /token(audience=orders-api),得到交换令牌。新令牌要沿用原用户的 sub,在 act 中放入行为者的 sub 和 client_id,roles 只放原用户角色中该下游所需要的,寿命必须比原令牌(900 秒)短。把令牌放到 /root/st/exchange/exchanged_token.txt,用 STS 公钥验证,并把 issued_token_type=、iss=、aud=、sub_matches=yes|no、act_client_id=、roles=、ttl= 写入 /root/st/exchange/exchanged.txt。签发和交换的辅助脚本做成 /root/st/exchange/tokens.sh,在之后的步骤中使用。

在 RFC 8693 中,subject_token 和 subject_token_type 是必填的,响应中必须有 issued_token_type。令牌类型标识符是 urn:ietf:params:oauth:token-type:access_token。curl 请用 --data-urlencode 传值。

让下游检查 aud 和 act

在 127.0.0.1:8309 上启动下游服务 /root/st/exchange/downstream.py。GET /orders 只对由 STS 签名、aud=orders-api 且 act.client_id 为 api-svc 的令牌返回 200 和 {"user":..., "actor":...},其余一律返回 401。把四个请求的状态码按 exchanged=(正常交换令牌)、keycloak_direct=(Keycloak 的原始用户令牌)、other_aud=(billing-api 用的交换令牌)、no_act=(用 STS 密钥签名但没有 act 的令牌)写入 /root/st/exchange/downstream.txt。

给 PyJWT 的 jwt.decode 传入 audience 和 issuer,就会一并检查 aud 和 iss。act 是 PyJWT 不认识的声明,必须自己检查。RFC 8693 要求访问控制只使用最外层的 act。

拒绝伪造的 subject_token 和未知的 audience

测试 STS 是否会拒绝验证失败的请求,并写入 /root/st/exchange/reject.txt。tampered=(保留签名只改了 sub 的令牌)、forged_key=(用另一把 RSA 密钥签名、只是模仿 Keycloak 的 kid 的令牌)、alg_none=(无签名令牌)、no_actor=(没有 actor_token)写状态码,bad_audience= 写 audience=admin-api 时的 error 值,control= 写正常请求的状态码。

RFC 8693 规定,subject_token 无效时用 invalid_request,无法为所请求的 audience 签发时用 invalid_target。不做签名验证、只做解码的 STS,以及没有固定 algorithms 的 STS,会在这里暴露。评分器还会发送用 Realm 密钥签名的过期令牌和 aud 不同的令牌。

一次性证明完整流程

用 /root/st/exchange/e2e.sh 一次性测试整个流程,在 /root/st/exchange/e2e.out 中留下 keycloak_exchange=(Keycloak 拒绝交换的 error)、exchange=、act=、downstream=、direct_keycloak=、forged_subject=(签名被破坏的 subject_token 的状态码)、bad_audience= 七行。

用 tokens.sh 辅助脚本把前面几步串起来即可。所有值都不要手写进文件,而要通过请求结果得到。评分器除了看文件,还会亲自再确认一次交换→下游的流程。