Files
Genarrative/docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

400 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OIDC JWT Claims 设计
日期:`2026-04-21`
## 1. 文档目的
这份文档用于完成 `M2` 中的两条任务:
1. `设计 JWT claims`
2. `确认 iss/sub/sid/provider/roles 字段`
目标是把当前 Node 后端只包含 `sub + ver` 的轻量 JWT升级为一份既兼容 Axum Bearer 鉴权、又可用于 `SpacetimeDB` 身份透传的 OIDC 风格 claims 设计,并固定:
1. 必须出现的标准字段与扩展字段
2. 哪些字段属于 access token哪些不应该塞进 JWT
3. Axum、`platform-auth``module-auth``SpacetimeDB` 各自如何使用这些 claims
4. 与当前 Node token 口径的映射方式
## 2. 当前基线
当前 Node token 实现位于:
1. `server-node/src/auth/token.ts`
2. `server-node/src/middleware/auth.ts`
3. `server-node/src/types/express.d.ts`
当前 Node access token 口径:
1. Header
- `alg = HS256`
- `typ = JWT`
2. 标准字段:
- `sub = userId`
- `iss = config.jwtIssuer`
- `iat`
- `exp`
3. 自定义字段:
- `ver = tokenVersion`
当前主要问题:
1. claims 信息过薄,不足以直接支撑 `SpacetimeDB` 侧的身份上下文判断
2. `sid/provider/roles` 等字段尚未冻结,后续 `platform-auth``module-auth` 实现没有稳定目标
3. 当前 Express `request.auth` 里也只有 `userId``tokenVersion`
## 3. 设计目标
新的 access token 必须同时满足:
1. Axum 可直接做 Bearer 校验与最小授权判断
2. `SpacetimeDB` 可识别发行者与稳定主体身份
3. 后续 `module-auth` 可基于 `sid/provider/roles` 做会话与能力判定
4. 前端无需解析大量敏感信息
5. claims 尺寸保持克制,不把会话明细或风控状态塞进 JWT
## 4. token 分层
当前阶段固定只设计两类 token
### 4.1 Access Token
用途:
1. `Authorization: Bearer <token>`
2. Axum 路由鉴权
3. `SpacetimeDB` 身份透传
特点:
1. 短期有效
2. 带完整最小 claims
3. 可在服务间透传
### 4.2 Refresh Session Cookie
用途:
1. 浏览器长期登录续期
2. 轮换 access token
特点:
1. 不走 JWT
2. 对应 `refresh_session`
3. 不通过 `SpacetimeDB` 透传
结论:
1. 只有 access token 需要本设计文档中的 claims
2. refresh cookie 不追加 JWT claims 设计复杂度
## 5. Claims 总体设计
当前阶段固定采用:
### 5.1 标准字段
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `iss` | 是 | 发行者,固定为 Axum 鉴权发行者。 |
| `sub` | 是 | 稳定用户 ID对应 `user_account.user_id`。 |
| `aud` | 否 | 当前阶段可暂不强制;若启用,固定为 `genarrative-api`。 |
| `iat` | 是 | 签发时间。 |
| `exp` | 是 | 过期时间。 |
| `nbf` | 否 | 当前阶段不强制。 |
| `jti` | 否 | 当前阶段不强制;若后续需要细粒度吊销,再单独扩展。 |
### 5.2 扩展字段
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `sid` | 是 | 会话 ID对应 `refresh_session.session_id` 或当前 access token 所属会话。 |
| `provider` | 是 | 登录来源,固定为 `password``phone``wechat`。 |
| `roles` | 是 | 角色列表;当前默认至少包含 `user`。 |
| `ver` | 是 | 用户 token 版本,对应 `user_account.token_version`。 |
| `phone_verified` | 是 | 是否已完成手机号验证。 |
| `binding_status` | 是 | 账号绑定状态,固定为 `active``pending_bind_phone`。 |
| `display_name` | 否 | 当前展示名快照,用于少量上游日志/观测;不是授权依据。 |
## 6. 关键字段定义
### 6.1 `iss`
固定要求:
1. 必填
2. 必须可稳定配置
3. 必须作为 Axum 与 `SpacetimeDB` 共同信任的发行者标识
当前阶段建议:
1. 本地开发默认:`https://auth.genarrative.local`
2. 测试/生产环境由 `GENARRATIVE_JWT_ISSUER` 或等价配置显式提供
说明:
1. 不继续沿用 Node 当前的 `genarrative-server-node` 这种非 URL 风格字符串。
2. 既然目标是 OIDC 风格 claims`iss` 应该升级为稳定 issuer 标识。
### 6.2 `sub`
固定要求:
1. 必填
2. 值为稳定用户 ID
3. 不能使用手机号、微信 openid 或用户名作为 `sub`
来源:
1. `user_account.user_id`
### 6.3 `sid`
固定要求:
1. 必填
2. 表示当前 access token 所属的会话 ID
3. 用于会话吊销、全端登出和会话列表关联
来源:
1. `refresh_session.session_id`
说明:
1. 即便当前 access token 是由 refresh cookie 刷新得到,`sid` 仍固定指向同一会话 ledger。
2. `sid` 是会话真相的索引,不是一次 access token 的唯一 ID。
### 6.4 `provider`
固定枚举:
1. `password`
2. `phone`
3. `wechat`
来源:
1. 优先来源于当前会话完成登录时的主 provider
2.`auth_identity.provider``user_account.login_provider` 保持兼容
### 6.5 `roles`
固定要求:
1. 必填
2. 类型为字符串数组
3. 当前阶段至少包含 `user`
当前阶段角色基线:
1. `user`
说明:
1. 当前阶段不预支 `admin/moderator/devops` 等角色体系
2. 但字段必须现在就冻结,避免后续 breaking change
### 6.6 `ver`
固定要求:
1. 必填
2. 表示当前用户 token 版本
3. 用于全局登录失效控制
来源:
1. `user_account.token_version`
兼容说明:
1. 继续兼容当前 Node `ver` 设计
2. Axum 校验时必须比对数据库中的最新 `token_version`
### 6.7 `phone_verified`
固定要求:
1. 必填
2. `true` 表示账号已具备已验证手机号
3. `false` 表示例如微信待绑手机号场景
来源:
1. `user_account.phone_verified_at != null`
### 6.8 `binding_status`
固定枚举:
1. `active`
2. `pending_bind_phone`
来源:
1. `user_account.account_status` 的鉴权视图
说明:
1. 它不是完整账号状态枚举,只是鉴权流程需要的最小绑定状态快照
2. 禁止把大量内部状态枚举直接透传成 JWT claim
### 6.9 `display_name`
固定规则:
1. 可选
2. 仅作为展示快照
3. 不能用于授权或数据归属判断
说明:
1. 即使写入,也必须把它视为弱一致字段
2. 后续若变更显示名,不要求立即使所有 JWT 失效
## 7. 不进入 JWT 的字段
以下内容当前阶段禁止进入 access token
1. 原始手机号
2. 手机号脱敏值
3. 微信 openid / unionid
4. refresh token hash
5. 风控状态、captcha 状态、封禁剩余时间
6. 完整用户资料对象
7. 审计日志、设备列表、IP、UA
原因:
1. 这些字段要么敏感,要么高频变动,要么不适合做 claims
2. 统一由数据库真相或接口读取承担
## 8. 推荐 payload 形态
```json
{
"iss": "https://auth.genarrative.local",
"sub": "usr_123",
"sid": "sess_456",
"provider": "wechat",
"roles": ["user"],
"ver": 3,
"phone_verified": false,
"binding_status": "pending_bind_phone",
"display_name": "微信旅人",
"iat": 1713657600,
"exp": 1713664800
}
```
说明:
1. 示例只表达字段形态,不锁死具体编码库细节
2. `iat/exp` 由签发库按标准时间戳表达
## 9. Axum / platform-auth / module-auth / SpacetimeDB 的使用边界
### 9.1 `platform-auth`
负责:
1. 签发 access token
2. 校验 `iss/exp/sub/sid/provider/roles/ver`
3. 解析 claims 为统一 Rust 结构
### 9.2 `module-auth`
负责:
1. 提供 claims 所依赖的用户、会话、绑定状态真相
2. 定义 `provider``binding_status``token_version` 的领域语义
### 9.3 `api-server`
负责:
1. 从 Bearer token 中提取 claims
2. 做路由级鉴权与用户状态校验
3. 把最小身份上下文透传给 `SpacetimeDB client`
### 9.4 `SpacetimeDB`
负责:
1. 基于受信任 issuer 识别主体身份
2. 在 reducer / view 上下文中读取稳定主体身份
当前阶段约束:
1. `SpacetimeDB` 身份判定以 `iss + sub` 为核心
2. `sid/provider/roles` 主要服务于应用层与后续模块授权,不要求第一版在 reducer 中过度使用
## 10. 与当前 Node token 的映射关系
| 当前 Node | 新 claims | 迁移规则 |
| --- | --- | --- |
| `sub = userId` | `sub` | 原样保留,但语义冻结为稳定用户 ID。 |
| `iss = jwtIssuer` | `iss` | 继续保留,但升级为 OIDC 风格 issuer 标识。 |
| `ver = tokenVersion` | `ver` | 原样保留。 |
| 无 | `sid` | 新增,绑定会话主键。 |
| 无 | `provider` | 新增,绑定本次登录 provider。 |
| 无 | `roles` | 新增,当前至少固定为 `["user"]`。 |
| 无 | `phone_verified` | 新增,表达手机号验证状态。 |
| 无 | `binding_status` | 新增,表达待绑手机/已激活状态。 |
| 无 | `display_name` | 可选新增。 |
## 11. Express / Axum 请求上下文映射
当前 Node `Express.Request.auth` 只有:
1. `userId`
2. `tokenVersion`
Rust 侧建议升级为统一 claims 结构,例如:
1. `user_id`
2. `session_id`
3. `provider`
4. `roles`
5. `token_version`
6. `phone_verified`
7. `binding_status`
说明:
1. `api-server` 不再只传 `userId`
2. 但 handler 仍应优先依赖最小必要字段,避免所有路由都耦合完整 claims
## 12. 不允许的设计漂移
后续实现时禁止出现以下情况:
1. 继续只保留 `sub + ver`,却声称已完成 `SpacetimeDB` 身份透传设计
2. 把手机号、openid、unionid 等敏感信息直接塞进 JWT
3.`sid` 设计成一次 access token 的随机 ID而不是会话 ID
4.`roles` 省略,等未来再补,导致后续 claims 结构 breaking change
5.`SpacetimeDB` 场景里信任客户端传入的 user id而不是受信任 JWT 的 `sub`
## 13. 本任务完成定义
当以下条件满足时JWT claims 设计任务视为完成:
1. `iss/sub/sid/provider/roles` 已明确冻结
2. access token 与 refresh session 的职责边界已切开
3. Axum、`platform-auth``module-auth``SpacetimeDB` 的使用边界已明确
4. 后续可以直接按这份文档实现签发、校验与身份透传
## 14. 依据文件
1. `server-node/src/auth/token.ts`
2. `server-node/src/middleware/auth.ts`
3. `server-node/src/auth/refreshSessionCookie.ts`
4. `server-node/src/config.ts`
5. `server-node/src/types/express.d.ts`
6. `packages/shared/src/contracts/auth.ts`
7. `docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md`
8. `docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md`
9. `docs/technical/SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md`