第三方 OAuth 接入参考
令牌
访问令牌、刷新令牌与 id_token 的结构、claim 含义与校验步骤。
MuoCeon 签发三种令牌:访问令牌(access token)、刷新令牌(refresh token)、身份令牌(id_token)。访问令牌与 id_token 是签名 JWT,你可以本地验签;刷新令牌对客户端是不透明的,不要解析。
访问令牌
- 算法:
RS256,header 里typ为at+jwt。 - 验签公钥:
{issuer}/jwks,按 header 的kid匹配。 - 寿命:机密客户端 1 小时,公开客户端 30 分钟。
{
"iss": "https://id.example.com/v1/oauth",
"aud": ["auth", "https://id.example.com/v1/auth"],
"client_id": "app_9f3c",
"cid": "app_9f3c",
"principal": "user",
"sub": "0192f3a4-...",
"scope": "openid profile email",
"at_version": 1,
"acr": "urn:muoceon:acr:aal2",
"auth_time": 1760000000,
"iat": 1760000000,
"nbf": 1759999995,
"exp": 1760003600,
"jti": "..."
}| claim | 含义 |
|---|---|
iss | 授权服务器标识,必须与元数据里的 issuer 逐字一致 |
aud | 受众,同时含代号(auth)与资源标识符(https://…/v1/auth) |
client_id / cid | 签发对象;两者取值相同,前者是标准字段 |
principal | user 表示代表用户,client 表示代表应用(client_credentials) |
sub | 用户稳定标识,仅用户令牌存在 |
did | 设备标识,仅第一方客户端的令牌存在 |
scope | 本次实际生效的 scope(已按客户端能力裁剪) |
at_version | 会话版本号,会话被撤销/封禁后递增,旧令牌即失效 |
acr / auth_time | 签发时刻的认证强度与认证时间,之后不再变化 |
authorization_details | 细粒度授权,仅当授权请求申请过才出现 |
jti / iat / nbf / exp | 标准令牌标识与时间窗 |
acr 与 auth_time 是快照
令牌一旦签发就不再变化:用户之后提权不会让旧令牌变强,刷新也不会让 auth_time 变新。
需要「刚刚认证过」的判断,必须基于新签发的令牌。
刷新令牌
- 形态:签名 JWT,但对客户端是不透明的——内部结构与
jti都不应被依赖。 - 寿命:机密客户端 30 天,公开客户端 14 天,不因刷新而延长到超过上限。
- 轮换:每次刷新都换新的,旧值立即失效;同一会话的旧访问令牌也会失效。
不要解析、不要拼接、不要假设长度。你只需要把它存好,刷新时原样提交。
id_token
仅在授权码换令牌、且 scope 含 openid 时签发。
{
"iss": "https://id.example.com/v1/oauth",
"sub": "0192f3a4-...",
"aud": "app_9f3c",
"exp": 1760003600,
"iat": 1760000000,
"at_hash": "...",
"auth_time": 1760000000
}aud是你的 client_id,不是资源受众——这与访问令牌刻意不同。- 只携带协议 claim;昵称、邮箱等资料不在 id_token 里,走 UserInfo。
at_hash可用于把 id_token 与本次拿到的访问令牌绑定。
校验步骤
按顺序做,任何一步失败即拒绝:
- 从
jwks_uri取公钥,按kid验签(RS256)。 - 校验
iss与可信 issuer 一致。 - 校验
aud命中预期:访问令牌看资源标识符,id_token 看自己的client_id。 - 校验
exp未过、nbf未到(留少量时钟偏移,例如 60 秒)。 - 需要身份资料时调 UserInfo,不要试图从访问令牌里读昵称或邮箱。
UserInfo 的受众是 auth
https://id.example.com/v1/auth/userinfo 要求令牌受众为 auth,且 scope 里至少有
openid / profile / email 之一。拿 oauth 受众的令牌调它一定失败。
撤销与内省
| 操作 | 端点 | 效果 |
|---|---|---|
| 撤销 | POST {issuer}/revoke | 撤销整个会话:该用户 + 该客户端(第一方还含该设备)的访问令牌与刷新令牌一并失效 |
| 内省 | POST {issuer}/introspect | 返回令牌是否有效及元信息;仅机密客户端可调用 |
撤销端点对不存在的令牌同样返回 200,不要用它判断令牌是否存在。撤销成功后你手上的访问令牌会立刻失效——包括你正准备用来调接口的那一枚。
令牌存哪里
| 令牌 | 机密客户端 | 公开客户端 |
|---|---|---|
| 访问令牌 | 服务端内存/会话 | 内存变量(不要写 localStorage) |
| 刷新令牌 | 服务端加密存储 | 系统钥匙串 / 安全存储 |
| id_token | 仅用于读取身份,可短期缓存 | 同左 |
任何令牌都不要进日志、URL、Referer 可泄漏的位置。