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 密钥)。