切换深色模式
第 4 章:ID Token 验签通过,为什么还不能拿去调用 API?
本章主线:用一个常见 token 误用问题串起 OAuth、OIDC、SAML、SCIM 和持续访问事件,建立“谁给谁签发什么”的协议视角。
开场:小安的第一次协议评审
小安接好 OIDC 登录后,发现 ID Token 也是 JWT、也有签名、也包含用户 ID,于是准备把它放进 Authorization 请求头调用订单 API。
评审问题:这个 token 是谁签的、签给谁、证明什么、允许做什么?如果只会回答“它验签成功”,就还没有抓住联邦协议的核心。
先给结论
ID Token 面向 Client,表达一次认证结果;API 应接受为自身资源签发、具有正确 audience 和授权语义的 Access Token。格式相似不代表用途相同。
把问题变成可验证样例
假设 Web Client 的 client_id 是 web-report-client,订单 API 的资源标识是 https://api.example.com/orders。登录后得到的 ID Token payload 如下:
json
{
"iss": "https://id.example.com",
"sub": "user-123",
"aud": "web-report-client",
"exp": 1787500800,
"nonce": "n-7f9a"
}订单 API 需要检查的 Access Token 则应类似:
json
{
"iss": "https://id.example.com",
"sub": "user-123",
"aud": "https://api.example.com/orders",
"scope": "orders:read",
"exp": 1787497500
}即使第一个 token 验签通过,订单 API 也必须因 audience 不匹配而拒绝。测试断言应体现安全语义,而不只检查 HTTP 状态:
text
given: signed ID Token with aud=web-report-client
when: call GET /orders
then: HTTP 401
and: reason_code=TOKEN_AUDIENCE_MISMATCH
and: no order query reaches the database这就是本章后面所有协议分析的固定方法:列出参与者,写清 token 的发行者、接收者、用途与生命周期,再用负向测试证明边界。
学习目标
能完整讲解 Code + PKCE;严格区分 ID/Access/Refresh Token;处理 issuer/JWKS/轮换;设计机器与 OBO 调用;说明 SAML/SCIM/Shared Signals 的边界;建立协议负向测试矩阵。
4.1 先分清授权与认证
OAuth 解决客户端如何获得受限 API 访问;OIDC 在 OAuth 之上加入用户认证与身份声明。Access Token 给资源服务器,ID Token 给客户端确认登录,二者不可互换。
角色:Resource Owner、Client、Authorization Server、Resource Server。一个产品可承担多个角色,但安全边界仍要分别验证。
4.1.1 参与者逐项检查
| 问题 | 示例答案 |
|---|---|
| 谁在操作? | 用户 sub=user-123 |
| 哪个软件请求? | Client web-report-client |
| 谁签发? | AS https://id.example.com |
| 给谁使用? | Orders API audience |
| 允许什么? | orders:read scope + 业务策略 |
| 多久有效? | access token 5 分钟 |
| 如何撤销? | 短期 + 会话事件/版本 |
当 Client 同时也是后端 API 时,也不能省掉 audience 和角色判断。组件部署在一起不代表协议语义可以混用。
4.2 Authorization Code + PKCE
图 4-1:4.2 Authorization Code + PKCE · 可编辑 Mermaid 源码
- state 绑定请求与回调,主要防 CSRF/响应混淆。
- nonce 将 ID Token 与认证请求绑定,防重放。
- PKCE 让窃取 code 的攻击者因无 verifier 不能兑换。
- redirect URI 必须精确匹配;避免通配和开放重定向。
4.2.1 参数生命周期
state:Client 创建并保存到短期浏览器事务;回调比较后立即作废。nonce:放入认证请求,AS 写入 ID Token,Client 验证并作废。code_verifier:高熵随机;只留在 Client;code_challenge=S256(verifier)。code:一次性、短期、绑定 client、redirect URI 与 PKCE challenge。
不要把 state 直接设为回跳 URL。若需恢复业务页面,存服务端事务 ID,回调后从可信状态读取并限制站内路径。
4.2.2 登录 CSRF 与账号混淆
攻击者可能让受害者把攻击者账号的授权响应绑定到受害者浏览器,之后受害者上传的数据进入攻击者账号。state/nonce、正确会话绑定和 issuer 校验共同降低这类风险。
4.3 Token 设计
Access Token 应最小 scope、明确 resource/audience、短有效期。Refresh Token 属高价值凭证,采用旋转与重用检测;公共客户端不能靠内置 client secret 证明身份。
Bearer token 谁拿到谁能用。高风险 API 可使用 mTLS 或 DPoP sender-constrained token。资源服务器必须独立验证受众和权限,不能因为 token“来自公司 IdP”就接受。
4.3.1 Access 与 Refresh Token 存放
- 服务端 Web/BFF:token 留在服务端会话存储,浏览器只持受保护 session Cookie。
- 原生应用:使用系统安全存储;Authorization Code + PKCE;避免自定义 URL scheme 被抢占。
- SPA:尽量缩短 access token 生命周期,避免持久化到 localStorage;结合 BFF 可减少浏览器 token 暴露。
XSS 可调用页面已有权限,即使读不到 HttpOnly Cookie。存储选择只是降低某类泄漏,不替代 XSS 防护和最小权限。
4.3.2 Refresh rotation 状态
每次刷新签发新 token 并作废旧 token。若旧 token 再出现,视为家族重用:撤销整个 token family,要求重新认证并记录风险事件。并发正常刷新需用短 overlap 或原子状态避免误判。
4.4 Client Credentials 与机器访问
适用于客户端代表自己而非用户。给每个工作负载独立 client identity;避免共享 secret;优先私钥认证、工作负载联邦或短期凭证。审计中不能把 client credential 行为归因于某个员工。
若服务代表用户调用下游,需要明确 on-behalf-of/token exchange 模型,不能把收到的 token 任意向下传。
4.4.1 两条调用链必须分开
text
后台对账任务:service identity → service permission
用户发起导出:user delegation + calling service → scoped downstream token前者审计不能伪造 user;后者既保留用户又保留调用服务。下游最终权限通常是用户权限、服务权限和任务范围的交集。
4.5 OIDC 关键组件
- Discovery:从 issuer 获取端点和能力;固定并验证 issuer。
- JWKS:获取验证公钥;安全缓存并处理轮换。
- ID Token:验证签名、issuer、audience、时间、nonce,必要时
azp。 - UserInfo:用 access token 获取 claims;不能覆盖已验证安全属性而无规则。
- Logout/Session:不同实现差异大,需设计本地会话与 IdP 会话关系。
4.5.1 多租户 issuer 处理
不要根据未验证 token 中的 issuer 任意访问网络。先由请求上下文/租户配置选择允许 issuer,再从受控 metadata/JWKS 获取密钥。缓存需遵守 TTL,并对未知 kid 做受限刷新,防止攻击者造成 JWKS 请求风暴。
claims 映射要版本化:外部 groups、roles、邮箱不能未经治理直接变成高权限。对可变邮箱使用稳定 sub + issuer 作为联邦关联键。
4.6 SAML 2.0
企业存量常见。IdP 产生签名 Assertion,SP 消费。重点验证 issuer、audience、recipient、destination、时间、InResponseTo、签名覆盖的具体节点。
XML Signature Wrapping 是典型风险:程序验证了一个节点签名,却消费另一个同名节点。使用成熟库、固定安全解析配置、禁用外部实体、避免自己操作 XML 签名。
4.6.1 SAML 运维要点
- 元数据签名与证书轮换需要 overlap;
- SP-initiated 与 IdP-initiated 登录风险和会话绑定不同;
- Assertion 短期、
InResponseTo一次性、clock skew 有界; - 加密 Assertion 不替代签名和条件验证;
- 禁用 XML 外部实体,限制文档大小和解析深度。
4.7 SCIM 生命周期
SCIM 用标准资源模型和 REST API 供应 User/Group。核心工程问题不是 CRUD,而是:
- externalId 与系统内 ID 的稳定映射;
- PUT/PATCH 语义、幂等与版本冲突;
- 分页、过滤、限速、重试;
- 删除还是禁用;
- 组嵌套、超大组和批量变更;
- 上下游状态对账与人工修复。
4.7.1 SCIM 映射表示例
text
source.employee_id → User.externalId(稳定关联)
source.work_email → User.userName(按目标约束)
source.display_name → User.displayName
source.active → User.active
source.department → extension.enterprise.department目标系统返回的 id 要持久保存,不能每次靠邮箱搜索。属性所有权要明确:若下游允许本地修改 displayName,下一次同步是覆盖、保留还是冲突工单?
4.7.2 乱序示例
事件 A 把部门改为财务,事件 B 稍后禁用员工;网络导致 B 先到。若 A 后到又把完整对象 PUT 成 active,离职账号可能被复活。使用源事件版本、状态转换规则和 PATCH 最小变化,离职/禁用具有更高优先级。
图 4-2:4.7.2 乱序示例 · 可编辑 Mermaid 源码
4.8 Shared Signals、CAEP 与 RISC
传统 token 在签发时做一次判断,之后风险可能变化。Shared Signals 提供安全事件流;CAEP 传播会话和访问状态变化;RISC 传播账号风险事件。
接收方仍需验证事件发行者、受众、事件 ID、时间和重放,并定义动作映射。事件“设备不合规”可能触发限权或重新认证,而不是永远封号。
4.8.1 事件处理状态
接收器为每个 stream 保存游标/事件 ID,幂等处理并记录动作结果。未知事件类型不能导致整个 stream 中断;高风险事件失败进入死信并告警。还要定义乱序:会话撤销后到达旧的低风险更新不能恢复会话。
4.9 FAPI 与高价值 API
FAPI 强化授权请求、响应和 token 绑定,适合金融/高价值数据。学习重点是威胁模型与安全 profile,而非机械增加参数。生产采用具体版本时必须查 Final 规范和认证测试。
4.10 协议选型
| 场景 | 首选 | 说明 |
|---|---|---|
| 新 Web/移动登录 | OIDC + Code + PKCE | 现代生态 |
| API 授权 | OAuth + 精确资源/scope | 与登录分离 |
| 企业旧 SaaS SSO | SAML 或 OIDC | 看应用能力 |
| 用户/组供应 | SCIM + 对账 | 不替代登录 |
| 服务到服务 | 工作负载身份/OAuth client | 禁止共享密钥 |
| 风险状态传播 | SSF/CAEP/RISC | 连续评估 |
4.10.1 选型评审问题
- 是登录、API 授权、供应还是风险事件?不要用一个协议包办。
- Client 是服务端、浏览器、原生应用还是机器?能否安全持有凭证?
- 资源价值、撤销时效和跨组织互操作要求?
- 对方支持的规范版本、profile、认证测试和轮换能力?
- 迁移期间旧协议如何隔离,何时退役?
4.11 本章实验与验收
实验:搭建 Client、AS、API;抓取完整 Code + PKCE 流程;逐一制造 state、nonce、audience、redirect URI、JWKS 轮换错误;实现一个幂等 SCIM disable。
验收:解释 ID Token 为何不能调用 API;公共客户端为何没有可靠 secret;SCIM 成功响应为何仍需对账;token passthrough 为何造成混淆代理。
工程复盘:把 ID Token 放进 API 请求后发生了什么
小安看到 ID Token 与 Access Token 都是 JWT,于是复用同一段验签代码。功能联调成功,却掩盖了 token substitution:订单 API 接受了本应只交给前端客户端、受众为 Client 的认证结果。
团队没有靠“记住不要这样做”收尾,而是建立协议契约测试:Client 只消费 ID Token;Resource Server 只接受面向自身 audience、具备所需 scope 的 Access Token;授权码必须绑定 PKCE;浏览器事务必须校验 state 和 nonce。每个错误 token 都成为固定回归样本。
随后他们把同样的思路用到 SCIM:发送 disable 不等于离职完成,必须记录源对象版本、目标系统 ID、请求幂等键、响应版本、重试状态和最终对账结果。协议字段不再是考试名词,而是一组可以观察、重放和证明的系统契约。
小安最后交付的不是“OIDC 已接通”,而是一份正常路径、拒绝矩阵、密钥轮换、撤销传播和乱序补偿都能运行的测试报告。
面试与设计评审
- 30 秒:OAuth 与 OIDC 的区别?OAuth 是委托授权框架,OIDC 在其上定义认证结果和身份声明。
- 2 分钟:完整讲 Code + PKCE,必须说明 state、nonce、PKCE 各自解决的不同问题。
- 生产级:补 redirect 精确匹配、issuer/audience、JWKS 轮换、refresh 重用、sender constraint、日志与错误监控。
本章小结
协议学习不能靠记参数,要持续问发行者、接收者、用途、生命周期与失败处理。SCIM 管生命周期而非登录,Shared Signals 补充签发后的状态变化,它们共同组成身份闭环。