* docs: document auth design and user isolation * docs: align auth docs with current storage and reset behavior --------- Co-authored-by: greatmengqi <chenmengqi.0376@bytedance.com>
13 KiB
用户认证与隔离设计
本文档描述 DeerFlow 当前内置认证模块的设计,而不是历史 RFC。它覆盖浏览器登录、API 认证、CSRF、用户隔离、首次初始化、密码重置、内部调用和升级迁移。
设计目标
认证模块的核心目标是把 DeerFlow 从“本地单用户工具”提升为“可多用户部署的 agent runtime”,并让用户身份贯穿 HTTP API、LangGraph-compatible runtime、文件系统、memory、自定义 agent 和反馈数据。
设计约束:
- 默认强制认证:除健康检查、文档和 auth bootstrap 端点外,HTTP 路由都必须有有效 session。
- 服务端持有所有权:客户端 metadata 不能声明
user_id或owner_id。 - 隔离默认开启:repository(仓储)、文件路径、memory、agent 配置默认按当前用户解析。
- 旧数据可升级:无认证版本留下的 thread 可以在 admin 存在后迁移到 admin。
- 密码不进日志:首次初始化由操作者设置密码;
reset_admin只写 0600 凭据文件。
非目标:
- 当前 OAuth 端点只是占位,尚未实现第三方登录。
- 当前用户角色只有
admin和user,尚未实现细粒度 RBAC。 - 当前登录限速是进程内字典,多 worker 下不是全局精确限速。
核心模型
graph TB
classDef actor fill:#D8CFC4,stroke:#6E6259,color:#2F2A26;
classDef api fill:#C9D7D2,stroke:#5D706A,color:#21302C;
classDef state fill:#D7D3E8,stroke:#6B6680,color:#29263A;
classDef data fill:#E5D2C4,stroke:#806A5B,color:#30251E;
Browser["Browser — access_token cookie and csrf_token cookie"]:::actor
AuthMiddleware["AuthMiddleware — strict session gate"]:::api
CSRFMiddleware["CSRFMiddleware — double-submit token and Origin check"]:::api
AuthRoutes["Auth routes — initialize login register logout me change-password"]:::api
UserContext["Current user ContextVar — request-scoped identity"]:::state
Repositories["Repositories — AUTO resolves user_id from context"]:::state
Files["Filesystem — users/{user_id}/threads/{thread_id}/user-data"]:::data
Memory["Memory and agents — users/{user_id}/memory.json and agents"]:::data
Browser --> AuthMiddleware
Browser --> CSRFMiddleware
AuthMiddleware --> AuthRoutes
AuthMiddleware --> UserContext
UserContext --> Repositories
UserContext --> Files
UserContext --> Memory
用户表
用户记录定义在 app.gateway.auth.models.User,持久化到 users 表。关键字段:
| 字段 | 语义 |
|---|---|
id |
用户主键,JWT sub 使用该值 |
email |
唯一登录名 |
password_hash |
bcrypt hash,OAuth 用户可为空 |
system_role |
admin 或 user |
needs_setup |
reset 后要求用户完成邮箱 / 密码设置 |
token_version |
改密码或 reset 时递增,用于废弃旧 JWT |
运行时身份
认证成功后,AuthMiddleware 把用户同时写入:
request.state.userrequest.state.authdeerflow.runtime.user_context的ContextVar
ContextVar 是这里的核心边界。上层 Gateway 负责写入身份,下层 persistence / file path 只读取结构化的当前用户,不反向依赖 app.gateway.auth 具体类型。
可以把 repository 调用的用户参数理解成一个三态 ADT:
enum UserScope:
case AutoFromContext
case Explicit(userId: String)
case BypassForMigration
对应 Python 实现是 AUTO | str | None:
AUTO:从ContextVar解析当前用户;没有上下文则抛错。str:显式指定用户,主要用于测试或管理脚本。None:跳过用户过滤,只允许迁移脚本或 admin CLI 使用。
登录与初始化流程
首次初始化
首次启动时,如果没有 admin,服务不会自动创建账号,只记录日志提示访问 /setup。
流程:
- 用户访问
/setup。 - 前端调用
GET /api/v1/auth/setup-status。 - 如果返回
{"needs_setup": true},前端展示创建 admin 表单。 - 表单提交
POST /api/v1/auth/initialize。 - 服务端确认当前没有 admin,创建
system_role="admin"、needs_setup=false的用户。 - 服务端设置
access_tokenHttpOnly cookie,用户进入 workspace。
/api/v1/auth/initialize 只在没有 admin 时可用。并发初始化由数据库唯一约束兜底,失败方返回 409。
普通登录
POST /api/v1/auth/login/local 使用 OAuth2PasswordRequestForm:
username是邮箱。password是密码。- 成功后签发 JWT,放入
access_tokenHttpOnly cookie。 - 响应体只返回
expires_in和needs_setup,不返回 token。
登录失败会按客户端 IP 计数。IP 解析只在 TCP peer 属于 AUTH_TRUSTED_PROXIES 时信任 X-Real-IP,不使用 X-Forwarded-For。
注册
POST /api/v1/auth/register 创建普通 user,并自动登录。
当前实现允许在没有 admin 时注册普通用户,但 setup-status 仍会返回 needs_setup=true,因为 admin 仍不存在。这是当前产品策略边界:如果后续要求“必须先初始化 admin 才能注册普通用户”,需要在 /register 增加 admin-exists gate。
改密码与 reset setup
POST /api/v1/auth/change-password 需要当前密码和新密码:
- 校验当前密码。
- 更新 bcrypt hash。
token_version += 1,使旧 JWT 立即失效。- 重新签发 cookie。
- 如果
needs_setup=true且传了new_email,则更新邮箱并清除needs_setup。
python -m app.gateway.auth.reset_admin 会:
- 找到 admin 或指定邮箱用户。
- 生成随机密码。
- 更新密码 hash。
token_version += 1。- 设置
needs_setup=true。 - 写入
.deer-flow/admin_initial_credentials.txt,权限0600。
命令行只输出凭据文件路径,不输出明文密码。
HTTP 认证边界
AuthMiddleware 是 fail-closed(默认拒绝)的全局认证门。
公开路径:
/health/docs/redoc/openapi.json/api/v1/auth/login/local/api/v1/auth/register/api/v1/auth/logout/api/v1/auth/setup-status/api/v1/auth/initialize
其余路径都要求有效 access_token cookie。存在 cookie 但 JWT 无效、过期、用户不存在或 token_version 不匹配时,直接返回 401,而不是让请求穿透到业务路由。
路由级别的 owner check 由 require_permission(..., owner_check=True) 完成:
- 读类请求允许旧的未追踪 legacy thread 兼容读取。
- 写 / 删除类请求使用
require_existing=True,要求 thread row 存在且属于当前用户,避免删除后缺 row 导致其他用户误通过。
CSRF 设计
DeerFlow 使用 Double Submit Cookie:
- 服务端设置
csrf_tokencookie。 - 前端 state-changing 请求发送同值
X-CSRF-Tokenheader。 - 服务端用
secrets.compare_digest比较 cookie/header。
需要 CSRF 的方法:
POSTPUTDELETEPATCH
auth bootstrap 端点(login/register/initialize/logout)不要求 double-submit token,因为首次调用时浏览器还没有 token;但这些端点会校验 browser Origin,拒绝 hostile Origin,避免 login CSRF / session fixation。
用户隔离
Thread metadata
Thread metadata 存在 threads_meta,关键隔离字段是 user_id。
创建 thread 时:
- 客户端传入的
metadata.user_id和metadata.owner_id会被剥离。 ThreadMetaRepository.create(..., user_id=AUTO)从ContextVar解析真实用户。/api/threads/search默认只返回当前用户的 thread。
读取 / 修改 / 删除时:
get()默认按当前用户过滤。check_access()用于路由 owner check。- 对其他用户的 thread 返回 404,避免泄露资源存在性。
文件系统
当前线程文件布局:
{base_dir}/users/{user_id}/threads/{thread_id}/user-data/
├── workspace/
├── uploads/
└── outputs/
agent 在 sandbox 内看到统一虚拟路径:
/mnt/user-data/workspace
/mnt/user-data/uploads
/mnt/user-data/outputs
ThreadDataMiddleware 使用 get_effective_user_id() 解析当前用户并生成线程路径。没有认证上下文时会落到 default 用户桶,主要用于内部调用、嵌入式 client 或无 HTTP 的本地执行路径。
Memory
默认 memory 存储:
{base_dir}/users/{user_id}/memory.json
{base_dir}/users/{user_id}/agents/{agent_name}/memory.json
有用户上下文时,空或相对 memory.storage_path 都使用上述 per-user 默认路径;只有绝对 memory.storage_path 会视为显式 opt-out(退出) per-user isolation,所有用户共享该路径。无用户上下文的 legacy 路径仍会把相对 storage_path 解析到 Paths.base_dir 下。
自定义 agent
用户自定义 agent 写入:
{base_dir}/users/{user_id}/agents/{agent_name}/
├── config.yaml
├── SOUL.md
└── memory.json
旧布局 {base_dir}/agents/{agent_name}/ 只作为只读兼容回退。更新或删除旧共享 agent 会要求先运行迁移脚本。
内部调用与 IM 渠道
IM channel worker 不是浏览器用户,不持有浏览器 cookie。它们通过 Gateway 内部认证:
- 请求带
X-DeerFlow-Internal-Token。 - 同时带匹配的 CSRF cookie/header。
- 服务端识别为内部用户,
id="default"、system_role="internal"。
这意味着 channel 产生的数据默认进入 default 用户桶。这个选择适合“平台级 bot 身份”,但不是“每个 IM 用户单独隔离”。如果后续要做到外部 IM 用户隔离,需要把外部 platform user 映射到 DeerFlow user,并让 channel manager 设置对应的 scoped identity。
LangGraph-compatible 认证
Gateway 内嵌 runtime 路径由 AuthMiddleware 和 CSRFMiddleware 保护。
仓库仍保留 app.gateway.langgraph_auth,用于 LangGraph Server 直连模式:
@auth.authenticate校验 JWT cookie、CSRF、用户存在性和token_version。@auth.on在写入 metadata 时注入user_id,并在读路径返回{"user_id": current_user}过滤条件。
这保证 Gateway 路由和 LangGraph-compatible 直连模式使用同一 JWT 语义。
升级与迁移
从无认证版本升级时,可能存在没有 user_id 的历史 thread。
当前策略:
- 首次启动如果没有 admin,只提示访问
/setup,不迁移。 - 操作者创建 admin。
- 后续启动时,
_ensure_admin_user()找到 admin,并把 LangGraph store 中缺少metadata.user_id的 thread 迁移到 admin。
文件系统旧布局迁移由脚本处理:
cd backend
PYTHONPATH=. python scripts/migrate_user_isolation.py --dry-run
PYTHONPATH=. python scripts/migrate_user_isolation.py --user-id <target-user-id>
迁移脚本覆盖 legacy memory.json、threads/ 和 agents/ 到 per-user layout。
安全不变量
必须长期保持的不变量:
- JWT 只在 HttpOnly cookie 中传输,不出现在响应 JSON。
- 任何非 public HTTP 路由都不能只靠“cookie 存在”放行,必须严格验证 JWT。
token_version不匹配必须拒绝,保证改密码 / reset 后旧 session 失效。- 客户端 metadata 中的
user_id/owner_id必须剥离。 - repository 默认
AUTO必须从当前用户上下文解析,不能静默退化成全局查询。 - 只有迁移脚本和 admin CLI 可以显式传
user_id=None绕过隔离。 - 本地文件路径必须通过
Paths和 sandbox path validation 解析,不能拼接未校验的用户输入。 - 捕获认证、迁移、后台任务异常必须记录日志;不能空 catch。
已知边界
| 边界 | 当前行为 | 后续方向 |
|---|---|---|
| 无 admin 时注册普通用户 | 允许注册普通 user |
如产品要求先初始化 admin,给 /register 加 gate |
| 登录限速 | 进程内 dict,单 worker 精确,多 worker 近似 | Redis / DB-backed rate limiter |
| OAuth | 端点占位,未实现 | 接入 provider 并统一 token_version / role 语义 |
| IM 用户隔离 | channel 使用 default 内部用户 |
建立外部用户到 DeerFlow user 的映射 |
| 绝对 memory path | 显式共享 memory | UI / docs 明确提示 opt-out 风险 |
相关文件
| 文件 | 职责 |
|---|---|
app/gateway/auth_middleware.py |
全局认证门、JWT 严格验证、写入 user context |
app/gateway/csrf_middleware.py |
CSRF double-submit 和 auth Origin 校验 |
app/gateway/routers/auth.py |
initialize/login/register/logout/me/change-password |
app/gateway/auth/jwt.py |
JWT 创建与解析 |
app/gateway/auth/reset_admin.py |
密码 reset CLI |
app/gateway/auth/credential_file.py |
0600 凭据文件写入 |
app/gateway/authz.py |
路由权限与 owner check |
deerflow/runtime/user_context.py |
当前用户 ContextVar 与 AUTO sentinel |
deerflow/persistence/thread_meta/ |
thread metadata owner filter |
deerflow/config/paths.py |
per-user filesystem layout |
deerflow/agents/middlewares/thread_data_middleware.py |
run 时解析用户线程目录 |
deerflow/agents/memory/storage.py |
per-user memory storage |
deerflow/config/agents_config.py |
per-user custom agents |
app/channels/manager.py |
IM channel 内部认证调用 |
scripts/migrate_user_isolation.py |
legacy 数据迁移到 per-user layout |
.deer-flow/data/deerflow.db |
统一 SQLite 数据库,包含 users / threads_meta / runs / feedback 等表 |
.deer-flow/users/{user_id}/agents/{agent_name}/ |
用户自定义 agent 配置、SOUL 和 agent memory |
.deer-flow/admin_initial_credentials.txt |
reset_admin 生成的新凭据文件(0600,读完应删除) |