feat: add refresh token rotation flow
This commit is contained in:
205
docs/technical/AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md
Normal file
205
docs/technical/AUTH_REFRESH_ROTATION_DESIGN_2026-04-21.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# `/api/auth/refresh` 轮换落地设计
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档用于指导 `M2` 中 `实现 refresh token 轮换` 任务的首版落地,冻结:
|
||||
|
||||
1. `POST /api/auth/refresh` 的请求与响应 contract。
|
||||
2. refresh cookie、服务端 refresh session 与 access token 三者的职责边界。
|
||||
3. “会话 ID 稳定、refresh token 可轮换”的固定语义。
|
||||
4. Rust 首版在未切入 SpacetimeDB 前的临时进程内实现方式。
|
||||
|
||||
## 2. 当前基线
|
||||
|
||||
当前 Node `/api/auth/refresh` 已具备以下稳定语义:
|
||||
|
||||
1. 从 HttpOnly cookie 中读取原始 refresh token。
|
||||
2. 服务端只按 `refresh_token_hash` 查找当前活跃会话。
|
||||
3. refresh 成功后,不新建第二条会话,而是在原会话上轮换 refresh token。
|
||||
4. 轮换时会更新 `expires_at` 与 `last_seen_at`。
|
||||
5. 成功后返回新的 access token,并写回新的 refresh cookie。
|
||||
6. 失败时会主动清空 refresh cookie,要求前端重新登录。
|
||||
|
||||
Rust 首版必须保留以上语义。
|
||||
|
||||
## 3. 当前阶段范围
|
||||
|
||||
本阶段只落以下内容:
|
||||
|
||||
1. `module-auth` 增加进程内 refresh session 真相与轮换服务。
|
||||
2. `api-server` 暴露 `POST /api/auth/refresh`。
|
||||
3. 登录成功时创建 refresh session。
|
||||
4. refresh 成功时在原 session 上轮换 refresh token。
|
||||
5. access token 的 `sid` 固定改为稳定 `session_id`,不再直接复用 refresh token。
|
||||
|
||||
本阶段不包含:
|
||||
|
||||
1. `/api/auth/logout`
|
||||
2. `/api/auth/logout-all`
|
||||
3. `/api/auth/sessions`
|
||||
4. `/api/auth/sessions/:sessionId/revoke`
|
||||
5. SpacetimeDB reducer 真正写表
|
||||
|
||||
## 4. contract
|
||||
|
||||
### 4.1 请求
|
||||
|
||||
1. 方法:`POST`
|
||||
2. 路径:`/api/auth/refresh`
|
||||
3. 请求体:空
|
||||
4. 鉴权来源:refresh cookie
|
||||
|
||||
### 4.2 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "<access-token>"
|
||||
}
|
||||
```
|
||||
|
||||
同时响应头必须写回新的 refresh cookie。
|
||||
|
||||
### 4.3 失败响应
|
||||
|
||||
当 refresh token 缺失、会话不存在、会话已过期或用户不存在时:
|
||||
|
||||
1. 返回 `401 UNAUTHORIZED`
|
||||
2. 同时清理 refresh cookie
|
||||
|
||||
## 5. 固定语义
|
||||
|
||||
### 5.1 session_id 与 refresh token 必须拆开
|
||||
|
||||
从本任务开始固定以下规则:
|
||||
|
||||
1. `session_id` 是稳定会话主键。
|
||||
2. refresh token 是可轮换的会话凭证。
|
||||
3. access token 的 `sid` 必须写入 `session_id`。
|
||||
4. refresh 轮换只更新 refresh token,不更改 `session_id`。
|
||||
|
||||
禁止继续把 refresh token 直接塞进 JWT `sid`。
|
||||
|
||||
### 5.2 refresh 是“原会话轮换”
|
||||
|
||||
refresh 成功后:
|
||||
|
||||
1. 保留原 `session_id`
|
||||
2. 生成新的原始 refresh token
|
||||
3. 用新的 `refresh_token_hash` 覆盖旧值
|
||||
4. 更新 `expires_at`
|
||||
5. 更新 `last_seen_at`
|
||||
|
||||
不允许新建第二条 session。
|
||||
|
||||
## 6. crate 边界
|
||||
|
||||
### 6.1 `module-auth`
|
||||
|
||||
负责:
|
||||
|
||||
1. 管理 refresh session 进程内真相。
|
||||
2. 提供创建 refresh session 与轮换 refresh session 的用例。
|
||||
3. 提供按 `user_id` 查询用户快照的能力,供 refresh 成功后重新签发 access token。
|
||||
|
||||
不负责:
|
||||
|
||||
1. 生成原始 refresh token。
|
||||
2. 读写 cookie。
|
||||
3. 签发 JWT。
|
||||
|
||||
### 6.2 `platform-auth`
|
||||
|
||||
负责:
|
||||
|
||||
1. 生成原始 refresh token。
|
||||
2. 对 refresh token 做哈希。
|
||||
3. 构造 refresh cookie 的 `Set-Cookie` 头。
|
||||
4. 从 cookie header 中读取 refresh token。
|
||||
|
||||
### 6.3 `api-server`
|
||||
|
||||
负责:
|
||||
|
||||
1. 从请求 cookie 中提取 refresh token。
|
||||
2. 调用 `module-auth` 执行 refresh session 轮换。
|
||||
3. 根据用户快照与稳定 `session_id` 重新签发 access token。
|
||||
4. refresh 失败时清理 cookie。
|
||||
|
||||
## 7. 进程内存储模型
|
||||
|
||||
当前阶段 `module-auth` 继续使用进程内内存仓储承接 refresh session,字段至少包括:
|
||||
|
||||
1. `session_id`
|
||||
2. `user_id`
|
||||
3. `refresh_token_hash`
|
||||
4. `issued_by_provider`
|
||||
5. `expires_at`
|
||||
6. `created_at`
|
||||
7. `updated_at`
|
||||
8. `last_seen_at`
|
||||
9. `revoked_at`
|
||||
|
||||
说明:
|
||||
|
||||
1. 这只是 SpacetimeDB 正式落地前的阶段性实现。
|
||||
2. 字段命名与语义继续对齐 [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)。
|
||||
|
||||
## 8. 流程
|
||||
|
||||
### 8.1 登录创建 session
|
||||
|
||||
密码登录成功后:
|
||||
|
||||
1. `api-server` 生成原始 refresh token。
|
||||
2. `api-server` 计算 `refresh_token_hash`。
|
||||
3. `module-auth` 创建一条新 session,并返回稳定 `session_id`。
|
||||
4. `api-server` 用该 `session_id` 写入 access token 的 `sid`。
|
||||
5. `api-server` 把原始 refresh token 写回 cookie。
|
||||
|
||||
### 8.2 refresh 轮换 session
|
||||
|
||||
当请求 `POST /api/auth/refresh` 时:
|
||||
|
||||
1. 从 cookie 中读取原始 refresh token。
|
||||
2. 计算 `refresh_token_hash`。
|
||||
3. `module-auth` 查找当前活跃 session。
|
||||
4. 校验 `expires_at > now` 且 `revoked_at == null`。
|
||||
5. 读取该 session 对应用户。
|
||||
6. 生成新的原始 refresh token。
|
||||
7. 用新 hash 更新同一条 session。
|
||||
8. 返回新的 access token 与新的 refresh cookie。
|
||||
|
||||
## 9. 错误语义
|
||||
|
||||
以下情况统一返回 `401`:
|
||||
|
||||
1. 缺少 refresh cookie
|
||||
2. refresh token 命中不到 session
|
||||
3. refresh session 已过期
|
||||
4. refresh session 已吊销
|
||||
5. session 对应用户不存在
|
||||
|
||||
错误文案统一保持中文,并沿用“当前登录态已失效,请重新登录”这类恢复导向语义。
|
||||
|
||||
## 10. 测试策略
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. 登录成功后可用 cookie 调用 `/api/auth/refresh`
|
||||
2. refresh 成功会写回新的 cookie
|
||||
3. refresh 成功返回新的 access token
|
||||
4. refresh 后旧 refresh token 立即失效
|
||||
5. 缺少 cookie 时返回 `401`
|
||||
6. 无效 refresh token 时返回 `401` 且清理 cookie
|
||||
|
||||
## 11. 完成定义
|
||||
|
||||
满足以下条件时,本任务视为完成:
|
||||
|
||||
1. Rust 侧已提供 `POST /api/auth/refresh`。
|
||||
2. access token `sid` 已改为稳定 `session_id`。
|
||||
3. refresh token 轮换成功时不创建新会话。
|
||||
4. refresh 失败时会清理 cookie。
|
||||
5. 文档、任务清单与测试已同步更新。
|
||||
Reference in New Issue
Block a user