Skip to content

第 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

图 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 乱序示例

图 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 SSOSAML 或 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 补充签发后的状态变化,它们共同组成身份闭环。