Files
xsl 140a63effa feat(identity): refresh token rotation with server-side revocable sessions
id_session 落地:opaque refresh(token_urlsafe(48))+ sha256 哈希存储 +
一次性轮换(旧行撤销与新行同一事务)+ logout 幂等撤销 + 已撤销重放检测
告警(V1 不做家族连坐);错误码 40103/40104/40105 接入。新增 POST
/auth/refresh、POST /auth/logout;dev-token 扩展为双令牌,旧字段
access_token/token_type/expires_in/user_id 原名保留,新增
refresh_token/refresh_expires_in(smoke.sh 不受影响)。

测试:tests/unit/test_refresh_tokens.py 覆盖 token 长度/字符域/唯一性、
哈希稳定性与不可逆(7 例,不连网不连库);tests/integration/
test_auth_refresh.py 覆盖轮换后新 access 可调 /quota、旧 refresh 二次
使用 40105、logout 后失效 40105、伪造 40103、TTL=0 过期 40104(5 例)。
门禁全绿:ruff check/format、mypy 两段、lint-imports 4 契约、
单测 176 通过、集成 8 通过。

偏差说明:1) SessionIssued 较任务文档字段清单增加 user_id ——
/auth/refresh 换发 access JWT 必须知道用户,opaque refresh 本身不携带
信息,只能随签发结果带回;2) 过期分支「顺手落库 revoked=true」在 HTTP
路径会随 401 的统一回滚一起丢失(仅事务内标记,已注释说明,不影响判定
语义);3) repo 撤销用 SELECT FOR UPDATE + ORM 变更而非 UPDATE
rowcount —— 对齐 billing.repo 现行模式,且 rowcount 在 mypy 下类型
不可用。
2026-08-14 16:21:52 +08:00

37 lines
1.3 KiB
Python
Executable File

"""identity 域的请求/响应模型(T4:双令牌会话,FR-ACC-05)。
``apps/api/schemas.py`` 是本批次的共享禁改文件,refresh/logout 的契约模型
放在域内这里,由 ``apps/api/routers/auth.py`` 引用;wire 响应仍只是统一
信封 (架构文档 §9.2) 里的 ``data`` 部分。
"""
from __future__ import annotations
from pydantic import BaseModel, Field
#: 生成端 token_urlsafe(48) 恒为 64 字符;上限放宽到 128 容忍未来换编码。
_REFRESH_TOKEN_MAX_LENGTH = 128
class RefreshRequest(BaseModel):
refresh_token: str = Field(min_length=1, max_length=_REFRESH_TOKEN_MAX_LENGTH)
class LogoutRequest(BaseModel):
refresh_token: str = Field(min_length=1, max_length=_REFRESH_TOKEN_MAX_LENGTH)
class TokenPairResponse(BaseModel):
"""dev-token 与 /auth/refresh 共用的双令牌响应。
旧字段名 ``access_token/token_type/expires_in/user_id`` 保持不变
(smoke.sh 与既有测试依赖),新增 ``refresh_token/refresh_expires_in``。
"""
access_token: str
refresh_token: str
token_type: str = "Bearer"
expires_in: int = Field(description="access token 有效期(秒)")
refresh_expires_in: int = Field(description="refresh token 有效期(秒)")
user_id: str = Field(description="雪花 ID 以字符串返回,避免 JS 精度丢失")