feat: add refresh token rotation flow

This commit is contained in:
2026-04-21 15:27:04 +08:00
parent 70dbefda2b
commit 584a77e572
16 changed files with 1048 additions and 85 deletions

View 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. 文档、任务清单与测试已同步更新。