Token 与撤销

流程跑完之后,对接方需要知道的 Signet token 事项:生命周期、刷新、撤销、实时验证。

On-Behalf-Of(OBO)

启用后,机密客户端 API A 可以将 Signet 签发、aud 指向 A 的用户 access token,交换成只能访问 B 的 token。
以表单 POST /oauth/token,提供 grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer、requested_token_use=on_behalf_of、assertion、单个 resource 和 scope。
A 使用 Basic 或表单凭证认证,不能混用;重复/未知参数和 extra_claims 均被拒绝。

管理员设置 OBO_ENABLED=true 与 OBO_POLICIES_FILE,明确指定 A 的 audience 所有权和 scope 映射,并将 B 加入 A 的允许资源。
用户须先通过现有授权流程,同意 F 访问 A,以及 A 访问 B。缺少同意返回 invalid_grant。

输出保留用户 sub,设置 client_id=A、act.sub=client:<A ID>、aud=B。
有效期取 OBO_TOKEN_EXPIRATION(默认及上限 5m)、client profile 与来源到期时间的最早者;不发 refresh/ID token,不允许再次 OBO。
所有 grant 的 act、may_act 均为服务端保留字段。不包含 Agent OBO 或 Entra 集成。

在线验证实时检查来源、两笔同意、用户、clients 和策略。需要即时撤销时,须结合本地 JWT 验证与不缓存的 introspection,并配置足够限流额度。
启用 ownership 检查时,B 可能只取得 active,仍须自行检查 audience 和 claims。离线验证只会在 token 到期后失效。
关闭 OBO 会拒绝交换及委派 token 在线验证;降版前须撤销剩余委派记录并等待最长 token 有效期。

使用场景、策略配置、两次同意流程与完整交换示例:OBO 集成指南。

Token 生命周期

流程成功后您会拿到其中一种或多种:

Token 格式 生命周期(依客户端 profile 而定) 用途
Access token JWT short 15m · standard 10h · long 24h(近似值) Authorization: Bearer 调 API
Refresh token JWT(请当不透明处理) short 1d · standard 7d · long 30d(近似值) 到 /oauth/token 换新 access token
ID token JWT 与 access token 相同 客户端身份信息 — 见 OIDC

Refresh token 内部是 JWT,但您应该 把它当成不透明 — 在客户端不要去解析其 claim,收获为零还会耦合到内部实现。

实际数值取决于管理员为此客户端选择的 token profile。请一律相信 token response 的 expires_in,永远不要写死。

Audience Binding (aud claim)

当流程带有 resource=<URL> 参数(RFC 8707),签发的 access token 的 aud 会绑定到该 resource。不带 resource 时 aud 会回退到部署层级的 JWT_AUDIENCE。Refresh token 一律用静态 JWT_AUDIENCE,不会带每次请求的 resource。Resource server 应检查 aud 等于自己的标识符,同时要求 type=access — 见 JWT 验证 §Audience Binding。

每客户端白名单(默认全部拒绝):客户端只能把 aud 绑定到管理员已加进该客户端 allowed-resources 白名单 的 resource 值。若白名单为空,则您传的 任何 resource= 都会被 invalid_target 拒绝 — 不传 resource(改用 JWT_AUDIENCE 回退)仍然可行。请管理员把您客户端需要的每个 resource 标识符加进白名单。见 错误处理 §Resource Indicator 错误。

刷新 token

在 refresh token 本身过期之前,任何时刻都可以:

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"
# 机密客户端:改用 -u "$CLIENT_ID:$CLIENT_SECRET",body 不要带 client_id

响应 与初次 token 交换相同格式。

刷新时缩小 resource(RFC 8707 §2.2):可选择带 resource=... 来签发 aud 为原授权 子集 的新 access token。请求未在原授权的 resource(扩张)会回 400 invalid_target — refresh token 不会被消耗。省略 resource 则拿到绑定完整授权集的 token。

何时刷新:提前刷,例如过期前 30–60 秒,不要等到收到 401 才做。这样可以避免请求失败的中途错误与重试的噪音。

若您的部署启用 rotation 模式(下节),还必须 将同一 session 的并发刷新串行化 — 两个标签页同时刷新会直接毁掉 session。

轮转模式:重用检测的陷阱

某些 Signet 部署会启用 rotation 模式(ENABLE_TOKEN_ROTATION=true)。在这个模式:

  • 每次刷新会签发 新的 refresh token 并 作废 旧的。
  • 若旧 refresh token 被再次使用(两个标签页抢刷、网络抖动后重试、token 被偷去用),Signet 会检测到重用,然后 把整个 token family 撤销。
  • 后续请求会回 {"error": "invalid_grant"}。

对对接方的实际意义:

  • 串行化每个用户 / session 的刷新(mutex、single-flight)。两个标签页同时刷新,两边都拿着同一份旧 refresh token,一个会赢,另一个会用 刚被作废 的旧 token 触发重用检测,整个 session 就死了。
  • 立刻持久化新 refresh token。存储更新前,不要先用旧的再发一轮请求。
  • 刷新时收到 invalid_grant 是终态 — 请显示登录页面,不要重试。

从 token response 本身无法判断 rotation 是否开启。若您的对接必须同时支持两种模式,一律把返回的 refresh_token 存下来(即使看起来一样 — rotation 模式下它会不同)。

登出 — /oauth/revoke(RFC 7009)

登出时撤销 refresh token(access token 可选),让被偷走的也变成哑弹:

curl -X POST https://your-signet/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=REFRESH_TOKEN" \
  -d "token_type_hint=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID"
# 机密客户端:带 client_secret 或使用 HTTP Basic
参数 必填 值
token 是 要撤销的 token
token_type_hint 否 access_token 或 refresh_token
client_id 是 机密客户端还需要 client_secret

依 RFC 7009,不论 token 原本存不存在,端点一律回 200 OK。不要依赖响应判断状态 — 直接当作 token 已经消失。

撤销一张 refresh token 也会(在 rotation 模式下)作废整个 token family。撤销 access token 不会 顺便作废对应的 refresh token — 要么两者都撤,要么登出时撤 refresh token,短效的 access token 自然过期即可。

本端点同时也能撤销个人 API 密钥。 传 token=sgk_...,不需要客户端凭证 — 持有密钥本身即是授权。适合让 script 在任务结束时自行拆掉它的短命密钥。见 API 密钥。

Caller-Supplied Extra Claims

/oauth/token 接受一个可选的 extra_claims form 参数 — 一个 JSON 对象,内含要嵌进签发 token 的额外 claim。它在全部四种 grant(authorization_code、device_code、client_credentials、refresh_token)上都有效。

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode 'extra_claims={"tenant":"acme","region":"eu"}'

规则与守护:

  • 值必须是一个 JSON 对象。JSON 畸形、超过大小限制、或使用保留键都会回 400 invalid_request。
  • 默认大小限制(运维方可各自调整或关闭):≤ 4096 字节原始大小、≤ 16 个键、每个值 ≤ 512 字节。
  • 保留键会被拒绝:标准的 RFC/OIDC/Signet 管理的 claim(iss、sub、aud、exp、iat、jti、type、scope、client_id、user_id、nonce、at_hash 等)不能被覆盖。
  • 整个功能可由运维方关闭(EXTRA_CLAIMS_ENABLED=false),此时任何非空的 extra_claims 都会被拒绝。
  • 也会嵌入 refresh token。 对于会签发 refresh token 的 grant(authorization_code、device_code,以及 rotation 模式的 refresh_token 兑换),同样的 claim 也会并入 refresh token JWT。您应把 refresh token 当作不透明,但它其实可以解码 — 请把这些 claim 纳入数据外泄评估,别把您不希望存在于一个数天有效 token 里的东西放进 extra_claims。
  • 无状态 — 不持久化。 claim 不会随授权一起存储,因此您必须 在每次刷新时重新提供 extra_claims,才能让它们出现在新的 token 里。

信任模型:这些 claim 是 调用方自行声称的,不是 Signet 背书的。Resource server 必须把它们当成不受信任的输入 — 绝不要把某个 extra_claims 值当成 Signet 已为其担保那样,用来做授权决策。

实时验证

resource server 的本地 JWT 验证见 JWT 验证。那条路速度快、可水平扩展,但 无法 察觉被撤销 / 停用的 token — 一张被撤销的 JWT 在密码学上仍然有效,直到 exp。

需要实时察觉撤销时,用以下端点之一:

/oauth/introspect(RFC 7662)— 首选

需要客户端认证(调用端本身必须是已注册的 Signet 客户端):

curl -X POST https://your-signet/oauth/introspect \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=TOKEN_TO_CHECK" \
  -d "token_type_hint=access_token"

响应:

{
  "active": true,
  "scope": "openid profile email",
  "client_id": "client-uuid",
  "username": "alice",
  "token_type": "Bearer",
  "exp": 1700000000,
  "iat": 1699996400,
  "sub": "user-uuid",
  "iss": "https://your-signet",
  "jti": "unique-token-id"
}

若 token 无效、过期、被撤销或停用,响应是:

{ "active": false }

归属闸门:默认情况下(INTROSPECTION_REQUIRE_OWNERSHIP=true)上面的完整元数据 只 会对您自己客户端签发的 token 返回。若您对属于 其他 客户端的 有效 token 做 introspect,响应会被精简成 { "active": true } — 没有 sub、scope、username、client_id、aud、exp 等。除非运维方已把该 flag 设为 false,否则不要围绕跨客户端 introspection 来构建 resource server。

策略强制 要有实时性时用这个 — 管理仪表板、高价值操作,任何无法容忍「陈旧有效」窗口长达整个 access token 寿命(standard profile 约 10 小时、long 24 小时)的场景。

/oauth/tokeninfo — 轻量替代

以 Bearer header 带 token,返回较少的字段。不用客户端凭证(token 本身即认证):

curl -H "Authorization: Bearer TOKEN_TO_CHECK" https://your-signet/oauth/tokeninfo
{
  "active": true,
  "user_id": "user-uuid",
  "client_id": "client-uuid",
  "scope": "openid profile email",
  "exp": 1700000000,
  "iss": "https://your-signet",
  "subject_type": "user"
}

Client Credentials 发出的 token,subject_type 会是 "client"。无效 token 回 401 并带 OAuth invalid_token 错误。

个人 API 密钥:本端点与 /oauth/introspect 也接受不透明的 sgk_ 密钥,响应会多一个 token_type: "personal_api_key",方便您套用不同策略。密钥 不是 JWT,所以这两个端点是验证它的 唯一 途径 — 见 API 密钥。

已登录用户可从 开发 → Token Info(/account/token-info)粘贴任一凭证,查看经过验证的项目、过期时间、scopes 与主体信息。表单使用 POST,不会把凭证放进网址或结果页,并且查询会写入用户的审计日志。无效、过期、吊销与未知凭证有意共用相同结果。

该选哪个?

需求 方式
resource server 大量验证,可容忍短暂陈旧 本地 JWKS 验证(不调 Signet)
需要实时撤销状态,调用端能做客户端认证 /oauth/introspect
用户 session 内的交互式检查 开发 → Token Info(/account/token-info)
调用端本身就是此 token 的持有者 /oauth/tokeninfo
验证不透明的 sgk_ 个人 API 密钥 /oauth/tokeninfo,除非您就是以密钥所绑定的那个应用认证 — 本地验证不可能(API 密钥)

速率限制

Signet 对 token 路径端点做每 IP 速率限制 — 唯一例外是 /oauth/introspect:每个已认证的客户端应用各有自己的额度(同一个 egress IP 后面的整个 fleet 不会挤成一个桶),后面再加一道每 IP 上限。默认值(运维者可调整):

端点 默认限制
POST /oauth/token 每 IP 20 req/min
POST /oauth/device/code 每 IP 10 req/min
POST /device/verify 每 IP 10 req/min
POST /oauth/introspect 每客户端应用 600 req/min,每 IP 1200
GET /oauth/tokeninfo 每 IP 600 req/min
POST /account/token-info 每 IP 600 req/min(独立浏览器配额)
GET/POST /oauth/userinfo 每 IP 600 req/min
POST /login 每 IP 5 req/min

超过限制回 429 Too Many Requests。若有 Retry-After header 请遵守;没有的话指数退避。能批量就批量 — 可以本地 JWKS 验证时,绝对不要每个请求都调 /oauth/tokeninfo 或 /oauth/introspect 来验 JWT;唯一真的需要往返的凭证是 sgk_ 个人 API 密钥,而且您应该短暂缓存其验证结果(API 密钥)。

相关文档