Skip to content

feat: 多用户登录 —— 账号存 DB + session cookie + per-user 隔离#129

Merged
mirror29 merged 19 commits into
mainfrom
feat/multi-user-login
Jul 2, 2026
Merged

feat: 多用户登录 —— 账号存 DB + session cookie + per-user 隔离#129
mirror29 merged 19 commits into
mainfrom
feat/multi-user-login

Conversation

@mirror29

@mirror29 mirror29 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

背景

线上 dashboard.inalpha.dev 已对公网可达,但仍是单用户 dev 模式:BFF 用固定 sub=console:dev 自签 token,任何访客打开即等于作者本人(能看模拟盘/策略/会话)。本 PR 加一层简单登录关上入口,并把用户身份正规化。账号存数据库,暂不做注册(初始用户用 CLI 种入)。

改动(5 commit,逻辑拆分)

  1. dbusers 表迁移(subject=JWT sub / email / argon2 password_hash / roles)
  2. paperPOST /auth/login(argon2 校验,放线程池不阻塞 live runner,抗时序枚举,失败统一 401)+ create_user.py CLI + 端到端测试
  3. orchestration — 修复工具身份转发的遗留缺陷:resolveRequestToken 让 agent 的写操作(start_strategy / execute_plan / 下单)落到登录用户账户,而非此前恒落的 console:dev
  4. dashboard — httpOnly session cookie(jose,7d)+ 登录页 + 中间件登录闸门 + copilotkit/会话/占卜/因子审批的 subject 统一改从 session 派生(读路径 per-user 隔离)
  5. infraAUTH_ENABLED 开关:本地默认 false(行为与加登录前完全一致),线上 true 强制登录

隔离链路(既有,零改动复用)

paper 已按 JWT sub 派生 account_id 隔离数据;登录只是把 subject 来源从固定 console:dev 换成登录用户,下游隔离自动生效。作者用 --subject console:dev 种账号即可继承现有模拟盘/会话历史。

验证

  • 本地全流程 curl 通过:未登录 / → 307 跳登录 / 错密码 401 / 对密码 200 + Set-Cookie / 带 cookie 访问 200 / 登出清 cookie / 无 cookie 打受保护 API 401
  • paper 4 个测试通过;orchestration + dashboard typecheck 通过;ruff / mypy 通过;check-consistency.sh 通过

上线顺序(避免把自己锁在门外)

① 部署使 migrate 跑 0024 建表 → ② create_user.py(作者 --subject console:dev)→ ③ 置 AUTH_ENABLED=true 重启 dashboard

🤖 Generated with Claude Code

mirror29 and others added 5 commits July 2, 2026 10:02
0024_users.py:subject(=JWT sub,PK) / email / argon2 password_hash / roles。supersede 0001 里 users 交给 Next.js 管的旧决定,身份改为后端自管;email/username 大小写不敏感唯一。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
api/auth.py:POST /auth/login(无鉴权)查 users 表 argon2 verify,校验放 anyio 线程池不阻塞 live runner,抗时序枚举,失败统一 401。scripts/create_user.py:argon2 upsert 建/改密 CLI(无注册 UI)。加 argon2-cffi 依赖;test_auth_login.py 覆盖对/错/未知/大小写。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
auth.ts 加 resolveRequestToken:显式 authToken → 中间件注入的已认证 sub(RequestContext[AUTH_SUB_KEY]) → service subject 兜底。9 个 tool 的 getClient 从只读恒空的 ctx.authToken 改走它,ToolRequestContext 加 get 通道。修此前 agent 发起的 start_strategy/execute_plan/下单 恒落 console:dev 的隔离缺陷。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
session.ts httpOnly session cookie(jose,7d);backend.ts 每请求按登录用户 sub 铸后端 token(Map 缓存),未启用登录回落 CONSOLE_SUBJECT。加 /api/auth/{login,logout,session} + /login 页(独立于 locale 外壳)+ 侧栏登出。proxy.ts 中间件:AUTH_ENABLED 时无 session 页面跳登录、/api 返 401。copilotkit resourceId / mastra 会话 / divination / factor 审批的 subject 统一改从 session 派生,实现读路径 per-user 隔离。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.env* 三处加 AUTH_ENABLED(dev 默认 false=行为不变;prod true);docker-compose 给 dashboard 注入 AUTH_ENABLED(默认 true)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying inalpha-web with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3d3cb68
Status: ✅  Deploy successful!
Preview URL: https://21b71dd1.inalpha-web.pages.dev
Branch Preview URL: https://feat-multi-user-login.inalpha-web.pages.dev

View logs

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

3d3cb68 轮 review

一句话:多用户登录闭环(DB 账号 + argon2 + session cookie + per-user 隔离),前几轮 CR 已把节流并发竞态、LRU 绕过、IDOR(chat thread/pending-plan 越权)、429 透传、生产 fail-safe、密码留痕等问题逐个堵上,基础扎实。本轮补一个前几轮 CR 遗留的口子。

必修(major)

  • packages/orchestration/src/auth.ts verifyToken/resolveRequestToken + mastra/index.ts identityMiddleware 未校验 token_use,可把泄露的 session cookie 在 mastra 层"洗"成合法 service token —— services/_shared/src/inalpha_shared/auth.py:get_current_user 这轮已加了 token_use==="session" 拒收(commit b794f2),理由是"session cookie 与 service token 同 secret、同形状,泄露可当长效后端凭据重放"。但这个校验只加在直接打 Python 的路径packages/orchestration/src/mastra/index.ts:96identityMiddleware 只用 verifyToken()(auth.ts:85,纯验签,不看 claim)解出 sub 塞进 AUTH_SUB_KEY;随后 resolveRequestToken(auth.ts:70)拿这个 submintServiceToken({ sub }) 现铸一个全新、不带 token_use 的 service token 转发给 paper/data 等 Python 服务。
    • 失败场景:攻击者以任意方式拿到一份 inalpha_session cookie(XSS / 日志泄漏 / 中间代理),只要能连到 mastra:4111(内网互通;infra/docker-compose.prod.yml 注释里也写了"CF 控制台配 ingress 把 api.域名 → http://mastra:4111"这个备选拓扑,不是不可能被启用),带 Authorization: Bearer <session-cookie> 直接打 /api/agents/.../stream 或 scheduler/permissions API,identityMiddleware 会照单全收、按该用户 sub 打通所有 agent 工具(读全部会话历史、start_strategy/execute_plan/下单……),完全绕开这轮专门为"session cookie 当后端凭据重放"加的防线——Python 侧的 token_use 拒收对这条路径不起作用,因为 mastra 转发前已经重新铸造了一个"干净"的 token。
    • 依据:CLAUDE.md 通用原则·安全(§Step3.8 越权/不可信输入进危险路径)+ 本 PR 自己在 services/_shared/src/inalpha_shared/auth.py 建立的威胁模型(session token 不应被当 service token 用)——packages/orchestration 是同一威胁模型下遗漏的一层。
    • 建议:verifyToken/identityMiddleware 里同样拒收 payload.token_use === "session"(视为未认证,走现有"沿用 fallback"路径,不阻断请求,只是不注入 AUTH_SUB_KEY),使 session cookie 在 mastra 这一跳也失效。

可选优化(medium)

  • apps/dashboard/src/proxy.tshasValidSession() 只验签名、不检查 token_use(与 session.ts:readSession() 的校验口径不一致)。实际不构成越权(下游 readSession() 仍会拒收非 session token 触发 401),但用一个不带 token_use 的 service token 当 cookie 能让 middleware "误判已登录"放行到页面 / API,再在更深层失败,排障体验比直接被 middleware 拦在登录页更差。建议 middleware 里也顺带判一下 token_use === "session",两处校验口径统一。

其余(节流原子性、LRU 淘汰、IDOR owns-check、生产 fail-safe、密码不留痕、429 透传)已在前几轮 CR 逐条修复,复核无遗留问题。

mirror29 and others added 8 commits July 2, 2026 10:54
CR 必修:chat 路径 pending-plan-notice processor 恒用 console:dev 查 /plans —— 对真实登录用户既漏查其 plan、又把 console 账户 plan_id 泄进回复(跨租户)。改为从 processOutputResult 的 requestContext 取 AUTH_SUB_KEY,经 sessionId 传给 fetcher 按该用户 sub 铸 token。补 resolveRequestToken 3+1 case 单测防同类静默回落回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR 建议:公网登录入口原无限流。加进程内(paper 单进程)滑动窗口失败计数,同邮箱 5 分钟内失败 ≥5 次返 429;成功清零;tracked 邮箱超上界整体清空防膨胀。按邮箱而非 IP(paper 只见 dashboard 同源 IP,per-IP 应在边缘做)。补 429 测试。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:route.ts 只特判 401,paper 的 429(失败节流)落到 else 被吞成 502;LoginForm 也只认 401。现 route 透传 429、LoginForm 加 rateLimited 中英文案,让节流原因真正到达用户。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:_record_failure 超上界时整体 clear 会被'刷 1w+ 不同邮箱各失败一次'清空、绕过对目标邮箱的节流。改 OrderedDict LRU:超上界淘汰最久未活动 key,目标每次失败 move_to_end 保活。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR 必修:listChatMessages/setChatThreadTitle 按 threadId 直取/直改,mastra Memory 层不按 resourceId 把关 → 任一登录用户凭 threadId 就能读他人对话历史、改他人标题。加 ownsThread(取回 thread 校验 resourceId===登录用户 sub);不属于/取不到一律当不存在(读返空、写跳过,不再 create 兜底避免'过继')。顺带把 copilotkit/divination 注释里过时的 'CONSOLE_SUBJECT' 更新为 getSessionSubject 派生。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:check(_recent_failures)与 record(_record_failure)原分处 verify 前后,中间隔着 await → 并发同邮箱请求在写回前都读到未超限、各免费试一把密码(check-then-act 竞态)。改为在同一同步块(无 await)内先检查再乐观预记,verify 成功再撤销;asyncio 单线程下该块对并发原子。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dashboard 原无测试基建,上一轮 chat 越权修复(ownsThread)只能人工核查。加最小 vitest(node env,显式 import 不开 globals 免动 tsconfig),导出 ownsThread 并覆盖 4 case:本人/他人/无 resourceId/get 抛错。typecheck + next build 均不受测试文件影响。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dashboard job 原只 typecheck+build,新加的 vitest 不进 CI 就没有回归保护。在 Type check 后加 Unit tests 步(不改 job name,避免动 required check 匹配)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mirror29

mirror29 commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

第 5 轮 auto-review 指出聊天发送路径还有一个潜在 IDOR(mastra 侧未校验 threadId 归属)。评估为潜在、当前不可利用(threadId 是不可枚举 UUID、无分享/导出功能、不进 URL),已按讨论决定:本 PR 先合(已修读/改标题两处同根 IDOR + 一堆隔离加固),发送路径这条开 follow-up 单独跟踪 → #130(分享/导出会话功能前必修)。

mirror29 and others added 6 commits July 2, 2026 14:02
CR 硬化:session cookie 与 service token 同用 JWT_SECRET 签、claim 形状一致,泄露的 session cookie 本可当长效后端凭据重放。session token 现带 token_use=session,_shared get_current_user 一律拒(INVALID_TOKEN_USE);service token 不带此 claim 不受影响(dashboard/mastra 铸的均无)。readSession 也只认 token_use=session。顺带 login cookie maxAge 复用 SESSION_TTL_SEC 常量去重。补 _shared 两个测试(拒 session / 收无标记 service)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:登录查询原用未 strip 的 body.email,节流 key 却是 strip+lower——带首尾空格的邮箱查不到却照样计入节流。改查询用 email_key(strip+lower),create_user 也存 strip 后的邮箱。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
第 6 轮 CR 担心聊天发送路径 IDOR(B 拿 A 的 threadId 读/写 A 会话)。深挖 mastra 内核确认:agent.stream 恒用可信 resourceId(getRemoteAgents 注入=登录用户 sub),LibSQLStore.getThreadById 传 resourceId 时按其过滤。本测试直证 memory.getThreadById 对他人 resourceId 返 null、本人正常——即框架层已拦住越权。作为回归守卫:若升级 mastra 悄悄去掉该过滤,CI 变红。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:AUTH_ENABLED 靠 compose pin true,换编排/env 拼错会静默退回 false=任何访客即作者。改为 NODE_ENV=production 时恒 true(session.ts + proxy.ts 同判断),配置缺失也不静默放行;仅非生产靠 AUTH_ENABLED=true opt-in。不影响 next build(不跑 middleware/请求)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:失败节流的进程内 dict 与 in-process live runner 都假设单进程(compose pin WORKERS=1),但无代码层守护。启动期若 WORKERS>1 记 error 日志(不 crash),暴露'节流按进程各自计数=爆破窗口×N'的静默失效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR:--password 走命令行明文会进 shell history + ps aux。默认交互式 getpass 输入(不回显);--password-stdin 供容器/自动化;--password 仍留但告警。更新 docstring 示例。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mirror29 mirror29 merged commit 90ec96b into main Jul 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant