Skip to content

feat(sso): MV-1 MyVote SSO 通道(cos72 半)— 3 轮安全审闭环#18

Merged
jhfnetboy merged 3 commits into
masterfrom
feat/mv1-sso-endpoints
Jul 15, 2026
Merged

feat(sso): MV-1 MyVote SSO 通道(cos72 半)— 3 轮安全审闭环#18
jhfnetboy merged 3 commits into
masterfrom
feat/mv1-sso-endpoints

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

/dev/stdin

新增 aastar/src/sso 模块(MV-1 的 cos72 半,KMS signTypedData 由 E-5 另行交付):

- POST /sso/authorize(JWT 保护):为当前登录用户签发一次性 code(32B hex,
  TTL 60s,单次消费),绑定 { userId, aaAddress };redirect_uri 走
  SSO_ALLOWED_REDIRECTS 白名单(origin 解析比对 + 前缀匹配,空=拒绝一切,
  fail-closed)
- POST /sso/exchange(公开):code 换短时 SSO session token — 独立
  SSO_JWT_SECRET(缺失则启动即 fail fast)+ audience "myvote" + TTL 10min,
  payload { sub, aa };get+delete 同步连续操作,单线程 Node 下天然原子
- GET /sso/verify(公开):Bearer 校验 secret/audience/过期,返回
  { valid, aaAddress },坏 token 不抛 401(供 MyVote 后端/edge 轮询)
- exchange/verify 挂 per-IP 滑窗限流 20 req/min(仿 otp-rate-limit.guard,
  IP 键;单实例内存实现,多副本需换 Redis——code 存储同)
- CORS 不动:main.ts 已是 origin:true 宽松配置,无需并入 SSO 白名单
- 单测 21 例:单次消费/过期/白名单 fail-closed(含 lookalike origin)/
  audience/secret/过期 token/限流(IP、bucket 隔离与滑窗释放)

Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
- M1 code 绑 redirect_uri:authorize 把规范化(URL.toString)redirect_uri 存进
  SsoCodeEntry;exchange DTO 新增 redirect_uri,兑换时与存值精确匹配,不匹配 401
  且照样烧掉 code(OAuth code↔redirect 绑定)
- M2 path 前缀绕过:白名单匹配改为 URL 解析后 origin 精确相等 + pathname 边界
  匹配(等于白名单 path 或以 path+'/' 开头;白名单 path 尾斜杠规范化)——
  /sso/callback 不再放过 /sso/callback-evil
- M3 secret 隔离强制:SsoService 构造时校验 SSO_JWT_SECRET != JWT_SECRET,相等
  即 fail fast;JwtStrategy 采用 audience 排斥方案(拒绝 aud 含 "myvote" 的
  token 走 session 路径,存量无 aud 的 session token 不受影响),常量抽到
  sso.constants.ts 避免 auth→sso 重依赖链循环
- L1 算法固定:SSO 签发 algorithm HS256、verify algorithms ['HS256'];
  JwtStrategy 同样 pin HS256
- L2 错误 oracle:exchange 的不存在/已用/过期/redirect 不匹配统一为
  401 'Invalid SSO code'
- 单测 21→33:redirect 不匹配拒绝+烧码、URL 规范化等价匹配、callback-evil 拒/
  callback/sub 放/精确放/尾斜杠规范化、secret 相等 fail fast、JwtStrategy aud
  排斥×2+存量放行、HS512 拒绝、三态错误一致性

Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
Codex 终审剩余 Medium:WHATWG URL 的 pathname 不解码 %2f/%5c/%2e,于是
/sso/callback/%2f..%2fevil 能满足 startsWith(entryPath + "/") 通过白名单;
一旦 MyVote 或其前置代理解码并归一化路径,就逃逸到 /sso/evil。

- resolveRedirect 在 origin/path 边界匹配之前,先以 /%2f|%5c|%2e/i 大小写不敏感
  拒绝 path 含编码分隔符/点段的 redirect_uri(fail-closed,合法 callback 不需要)
- path 比较保持大小写敏感(URL path 本就区分大小写)+ 双方同一 canonical 化
  (去尾斜杠)后做 segment 边界匹配 —— 现有实现即如此,保持不变
- 注释补运营建议:生产 SSO_ALLOWED_REDIRECTS 应配精确 callback path,不要配裸
  origin(裸 origin 放行该 origin 任意 path)
- 单测 33→38:%2f 拒、%2F 大写拒、%5c 拒、%2e%2e 拒、正常 /sso/callback/sub 仍放行

Claude-Session: https://claude.ai/code/session_01MCxVCnKkpzi5sCGf1FZ4CH
@jhfnetboy
jhfnetboy requested a review from fanhousanbu as a code owner July 14, 2026 08:52

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — 这是我最近审到的最扎实的一段鉴权后端。45/45 测试通过(独立跑通,含 JwtStrategy 的 SSO-audience 排除)。

按攻击面逐条核对:

  • Token 域隔离(双保险) — 独立 SSO_JWT_SECRET,未设即 boot 失败,== JWT_SECRET 也 boot 失败(密码学不可互换);再加 JwtStrategyaud==myvote 主动排除(scalar + array 都覆盖),即使密钥配错也拦得住。verify 里 algorithms:["HS256"] 钉死算法 —— 不信任 token header 的 alg,堵掉 alg-confusion / none。
  • 一次性 code — 32 字节随机;get+delete 之间无 await,Node 单线程下原子,并发也不可能兑换两次;失败尝试也无条件销毁 code(过期/redirect 不匹配都烧掉),符合 OAuth 语义。code 绑定 {userId, aaAddress, redirectUri},exchange 必须出示完全一致的 redirect —— 偷到 code 也没法换个 redirect 兑现。
  • redirect 校验(resolveRedirect) — 双方都 URL 解析、绝不裸字符串前缀:origin 全等(挡掉 myvote.example.com.evil.com)、path 按段边界匹配(挡掉 /sso/callback-evil)、拒绝编码分隔符 %2f/%5c/%2e(挡掉下游解码后的路径穿越)、空白名单 fail-closed。
  • Oracle 抗性 — exchange 所有失败统一 INVALID_SSO_CODE,分不出「过期」还是「从没存在」;verify 设计成非抛出。
  • 内存 DoS 有界 — code 10k 上限 + 驱逐,rate-limit key 50k 上限 + 驱逐,sweeper 都 unref
  • 限流接入正确 — authorize=JwtAuthGuard,exchange/verify=SsoRateLimit。且诚实标注了 trust proxy 未配时所有客户端共享代理 IP(fail-closed:限太多不会限太少)。

多副本部署时那两处 in-memory store(code + 限流)要挪到 Redis —— 注释已明确写了,当前单实例正确。无阻塞。

— clestons (Opus independent review, tool-verified: 源码通读 + 45 测试跑通)

@jhfnetboy
jhfnetboy merged commit 7b261a6 into master Jul 15, 2026
14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants