docs: design oidc jwt claims

This commit is contained in:
2026-04-21 12:21:13 +08:00
parent 854a7a2568
commit e37163d4d3
7 changed files with 417 additions and 2 deletions

View File

@@ -191,8 +191,10 @@
### OIDC 与 SpacetimeDB 身份透传
- [ ] 设计 JWT claims
- [ ] 确认 `iss/sub/sid/provider/roles` 字段
- [x] 设计 JWT claims
交付物:[../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
- [x] 确认 `iss/sub/sid/provider/roles` 字段
交付物:[../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
- [ ] 让 Axum 自身可校验 JWT
- [ ] 让 SpacetimeDB 可识别 Axum 签发的身份令牌
- [ ] 验证 reducer / view 可读取用户身份上下文

View File

@@ -0,0 +1,399 @@
# 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`

View File

@@ -4,6 +4,7 @@
## 文档列表
- [OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](./OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md):面向 Axum、`platform-auth``SpacetimeDB` 身份透传的 OIDC 风格 JWT claims 设计,冻结 `iss/sub/sid/provider/roles` 等关键字段。
- [RUST_SHARED_LOGGING_CRATE_DESIGN_2026-04-21.md](./RUST_SHARED_LOGGING_CRATE_DESIGN_2026-04-21.md)Rust 工作区统一日志模块 `shared-logging` 的职责边界、API、输出风格与 `api-server` 迁移规则。
- [SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md)`M2` 第七张微信 OAuth 状态表 `wechat_auth_state` 的字段、过期/消费语义、`wechat/start``wechat/callback` 的单次消费规则,以及多实例下的清理策略。
- [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md)`M2` 第六张短信鉴权统计表 `sms_auth_event` 的事件范围、统计口径、索引与和风控/审计表的协作边界。

View File

@@ -486,6 +486,10 @@ server-rs/
- `phone_verified`
- `display_name`
`iss/sub/sid/provider/roles/ver/phone_verified/binding_status` 的字段定义、哪些字段禁止进入 JWT、以及 Axum 与 `SpacetimeDB` 的使用边界,见:
- [OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](./OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
## 9.3 Refresh Session
建议保留当前模式:

View File

@@ -31,6 +31,7 @@
5. [../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md)
6. [../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md)
7. [../../../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_WECHAT_AUTH_STATE_TABLE_DESIGN_2026-04-21.md)
8. [../../../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../../../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
## 3. 边界约束

View File

@@ -23,6 +23,10 @@
3. 落地短信发送、校验与风控适配
4. 落地微信 OAuth start / callback 适配
当前优先冻结依据:
1. [../../../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../../../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
## 3. 边界约束
1. `platform-auth` 只承接平台适配,不承接 `module-auth` 的业务规则和状态真相。

View File

@@ -23,6 +23,10 @@
3. 接入身份 claims 透传
4. 在实体 module scaffold 落地后接入 publish / dev 循环
当前身份透传设计依据:
1. [../../../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md](../../../docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md)
当前本地开发脚本约定:
1. `../../scripts/spacetime-dev.ps1``../../scripts/spacetime-dev.sh` 当前固定执行 `spacetime start` 的 standalone 模式。