diff --git a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md index dd24d191..42949a09 100644 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md @@ -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 可读取用户身份上下文 diff --git a/docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md b/docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md new file mode 100644 index 00000000..6a81f721 --- /dev/null +++ b/docs/technical/OIDC_JWT_CLAIMS_DESIGN_2026-04-21.md @@ -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 ` +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` diff --git a/docs/technical/README.md b/docs/technical/README.md index 71efd7f2..20557fed 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -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` 的事件范围、统计口径、索引与和风控/审计表的协作边界。 diff --git a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md index 8c6ee47e..69abac45 100644 --- a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md +++ b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md @@ -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 建议保留当前模式: diff --git a/server-rs/crates/module-auth/README.md b/server-rs/crates/module-auth/README.md index 9b40c737..68d5aaba 100644 --- a/server-rs/crates/module-auth/README.md +++ b/server-rs/crates/module-auth/README.md @@ -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. 边界约束 diff --git a/server-rs/crates/platform-auth/README.md b/server-rs/crates/platform-auth/README.md index 43f8e20a..73879025 100644 --- a/server-rs/crates/platform-auth/README.md +++ b/server-rs/crates/platform-auth/README.md @@ -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` 的业务规则和状态真相。 diff --git a/server-rs/crates/spacetime-module/README.md b/server-rs/crates/spacetime-module/README.md index b968d072..346e7ce1 100644 --- a/server-rs/crates/spacetime-module/README.md +++ b/server-rs/crates/spacetime-module/README.md @@ -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 模式。