docs: document auth design and user isolation (#2913)

* 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>
This commit is contained in:
greatmengqi 2026-05-12 23:07:11 +08:00 committed by GitHub
parent 84f88b6610
commit f734e14d8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 547 additions and 145 deletions

View File

@ -62,7 +62,7 @@ async def _ensure_admin_user(app: FastAPI) -> None:
Subsequent boots (admin already exists):
- Runs the one-time "no-auth → with-auth" orphan thread migration for
existing LangGraph thread metadata that has no owner_id.
existing LangGraph thread metadata that has no user_id.
No SQL persistence migration is needed: the four user_id columns
(threads_meta, runs, run_events, feedback) only come into existence
@ -177,7 +177,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
async with langgraph_runtime(app):
logger.info("LangGraph runtime initialised")
# Ensure admin user exists (auto-create on first boot)
# Check admin bootstrap state and migrate orphan threads after admin exists.
# Must run AFTER langgraph_runtime so app.state.store is available for thread migration
await _ensure_admin_user(app)

View File

@ -28,7 +28,7 @@ class User(BaseModel):
oauth_id: str | None = Field(None, description="User ID from OAuth provider")
# Auth lifecycle
needs_setup: bool = Field(default=False, description="True for auto-created admin until setup completes")
needs_setup: bool = Field(default=False, description="True when a reset account must complete setup")
token_version: int = Field(default=0, description="Incremented on password change to invalidate old JWTs")

View File

@ -305,7 +305,7 @@ async def login_local(
async def register(request: Request, response: Response, body: RegisterRequest):
"""Register a new user account (always 'user' role).
Admin is auto-created on first boot. This endpoint creates regular users.
The first admin is created explicitly through /initialize. This endpoint creates regular users.
Auto-login by setting the session cookie.
"""
try:

View File

@ -535,14 +535,28 @@ All APIs return errors in a consistent format:
## Authentication
Currently, DeerFlow does not implement authentication. All APIs are accessible without credentials.
DeerFlow enforces authentication for all non-public HTTP routes. Public routes are limited to health/docs metadata and these public auth endpoints:
Note: This is about DeerFlow API authentication. MCP outbound connections can still use OAuth for configured HTTP/SSE MCP servers.
- `POST /api/v1/auth/initialize` creates the first admin account when no admin exists.
- `POST /api/v1/auth/login/local` logs in with email/password and sets an HttpOnly `access_token` cookie.
- `POST /api/v1/auth/register` creates a regular `user` account and sets the session cookie.
- `POST /api/v1/auth/logout` clears the session cookie.
- `GET /api/v1/auth/setup-status` reports whether the first admin still needs to be created.
For production deployments, it is recommended to:
1. Use Nginx for basic auth or OAuth integration
2. Deploy behind a VPN or private network
3. Implement custom authentication middleware
The authenticated auth endpoints are:
- `GET /api/v1/auth/me` returns the current user.
- `POST /api/v1/auth/change-password` changes password, optionally changes email during setup, increments `token_version`, and reissues the cookie.
Protected state-changing requests also require the CSRF double-submit token: send the `csrf_token` cookie value as the `X-CSRF-Token` header. Login/register/initialize/logout are bootstrap auth endpoints: they are exempt from the double-submit token but still reject hostile browser `Origin` headers.
User isolation is enforced from the authenticated user context:
- Thread metadata is scoped by `threads_meta.user_id`; search/read/write/delete APIs only expose the current user's threads.
- Thread files live under `{base_dir}/users/{user_id}/threads/{thread_id}/user-data/` and are exposed inside the sandbox as `/mnt/user-data/`.
- Memory and custom agents are stored under `{base_dir}/users/{user_id}/...`.
Note: MCP outbound connections can still use OAuth for configured HTTP/SSE MCP servers; that is separate from DeerFlow API authentication.
---

331
backend/docs/AUTH_DESIGN.md Normal file
View File

@ -0,0 +1,331 @@
# 用户认证与隔离设计
本文档描述 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 下不是全局精确限速。
## 核心模型
```mermaid
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 hashOAuth 用户可为空 |
| `system_role` | `admin``user` |
| `needs_setup` | reset 后要求用户完成邮箱 / 密码设置 |
| `token_version` | 改密码或 reset 时递增,用于废弃旧 JWT |
### 运行时身份
认证成功后,`AuthMiddleware` 把用户同时写入:
- `request.state.user`
- `request.state.auth`
- `deerflow.runtime.user_context``ContextVar`
`ContextVar` 是这里的核心边界。上层 Gateway 负责写入身份,下层 persistence / file path 只读取结构化的当前用户,不反向依赖 `app.gateway.auth` 具体类型。
可以把 repository 调用的用户参数理解成一个三态 ADT
```scala
enum UserScope:
case AutoFromContext
case Explicit(userId: String)
case BypassForMigration
```
对应 Python 实现是 `AUTO | str | None`
- `AUTO`:从 `ContextVar` 解析当前用户;没有上下文则抛错。
- `str`:显式指定用户,主要用于测试或管理脚本。
- `None`:跳过用户过滤,只允许迁移脚本或 admin CLI 使用。
## 登录与初始化流程
### 首次初始化
首次启动时,如果没有 admin服务不会自动创建账号只记录日志提示访问 `/setup`
流程:
1. 用户访问 `/setup`
2. 前端调用 `GET /api/v1/auth/setup-status`
3. 如果返回 `{"needs_setup": true}`,前端展示创建 admin 表单。
4. 表单提交 `POST /api/v1/auth/initialize`
5. 服务端确认当前没有 admin创建 `system_role="admin"``needs_setup=false` 的用户。
6. 服务端设置 `access_token` HttpOnly cookie用户进入 workspace。
`/api/v1/auth/initialize` 只在没有 admin 时可用。并发初始化由数据库唯一约束兜底,失败方返回 409。
### 普通登录
`POST /api/v1/auth/login/local` 使用 `OAuth2PasswordRequestForm`
- `username` 是邮箱。
- `password` 是密码。
- 成功后签发 JWT放入 `access_token` HttpOnly 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_token` cookie。
- 前端 state-changing 请求发送同值 `X-CSRF-Token` header。
- 服务端用 `secrets.compare_digest` 比较 cookie/header。
需要 CSRF 的方法:
- `POST`
- `PUT`
- `DELETE`
- `PATCH`
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避免泄露资源存在性。
### 文件系统
当前线程文件布局:
```text
{base_dir}/users/{user_id}/threads/{thread_id}/user-data/
├── workspace/
├── uploads/
└── outputs/
```
agent 在 sandbox 内看到统一虚拟路径:
```text
/mnt/user-data/workspace
/mnt/user-data/uploads
/mnt/user-data/outputs
```
`ThreadDataMiddleware` 使用 `get_effective_user_id()` 解析当前用户并生成线程路径。没有认证上下文时会落到 `default` 用户桶,主要用于内部调用、嵌入式 client 或无 HTTP 的本地执行路径。
### Memory
默认 memory 存储:
```text
{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 写入:
```text
{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。
当前策略:
1. 首次启动如果没有 admin只提示访问 `/setup`,不迁移。
2. 操作者创建 admin。
3. 后续启动时,`_ensure_admin_user()` 找到 admin并把 LangGraph store 中缺少 `metadata.user_id` 的 thread 迁移到 admin。
文件系统旧布局迁移由脚本处理:
```bash
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读完应删除 |

View File

@ -24,11 +24,11 @@ All other test plan sections were executed against either:
| Case | Title | What it covers | Why not run |
|---|---|---|---|
| TC-DOCKER-01 | `users.db` volume persistence | Verify the `DEER_FLOW_HOME` bind mount survives container restart | needs `docker compose up` |
| TC-DOCKER-01 | `deerflow.db` volume persistence | Verify the `DEER_FLOW_HOME` bind mount survives container restart | needs `docker compose up` |
| TC-DOCKER-02 | Session persistence across container restart | `AUTH_JWT_SECRET` env var keeps cookies valid after `docker compose down && up` | needs `docker compose down/up` |
| TC-DOCKER-03 | Per-worker rate limiter divergence | Confirms in-process `_login_attempts` dict doesn't share state across `gunicorn` workers (4 by default in the compose file); known limitation, documented | needs multi-worker container |
| TC-DOCKER-04 | IM channels skip AuthMiddleware | Verify Feishu/Slack/Telegram dispatchers run in-container against `http://langgraph:2024` without going through nginx | needs `docker logs` |
| TC-DOCKER-05 | Admin credentials surfacing | **Updated post-simplify** — was "log scrape", now "0600 credential file in `DEER_FLOW_HOME`". The file-based behavior is already validated by TC-1.1 + TC-UPG-13 on sg_dev (non-Docker), so the only Docker-specific gap is verifying the volume mount carries the file out to the host | needs container + host volume |
| TC-DOCKER-04 | IM channels use internal Gateway auth | Verify Feishu/Slack/Telegram dispatchers attach the process-local internal auth header plus CSRF cookie/header when calling Gateway-compatible LangGraph APIs | needs `docker logs` |
| TC-DOCKER-05 | Reset credentials surfacing | `reset_admin` writes a 0600 credential file in `DEER_FLOW_HOME` instead of logging plaintext. The file-based behavior is validated by non-Docker reset tests, so the only Docker-specific gap is verifying the volume mount carries the file out to the host | needs container + host volume |
| TC-DOCKER-06 | Gateway-mode Docker deploy | `./scripts/deploy.sh --gateway` produces a 3-container topology (no `langgraph` container); same auth flow as standard mode | needs `docker compose --profile gateway` |
## Coverage already provided by non-Docker tests
@ -41,8 +41,8 @@ the test cases that ran on sg_dev or local:
| TC-DOCKER-01 (volume persistence) | TC-REENT-01 on sg_dev (admin row survives gateway restart) — same SQLite file, just no container layer between |
| TC-DOCKER-02 (session persistence) | TC-API-02/03/06 (cookie roundtrip), plus TC-REENT-04 (multi-cookie) — JWT verification is process-state-free, container restart is equivalent to `pkill uvicorn && uv run uvicorn` |
| TC-DOCKER-03 (per-worker rate limit) | TC-GW-04 + TC-REENT-09 (single-worker rate limit + 5min expiry). The cross-worker divergence is an architectural property of the in-memory dict; no auth code path differs |
| TC-DOCKER-04 (IM channels skip auth) | Code-level only: `app/channels/manager.py` uses `langgraph_sdk` directly with no cookie handling. The langgraph_auth handler is bypassed by going through SDK, not HTTP |
| TC-DOCKER-05 (credential surfacing) | TC-1.1 on sg_dev (file at `~/deer-flow/backend/.deer-flow/admin_initial_credentials.txt`, mode 0600, password 22 chars) — the only Docker-unique step is whether the bind mount projects this path onto the host, which is a `docker compose` config check, not a runtime behavior change |
| TC-DOCKER-04 (IM channels use internal auth) | Code-level: `app/channels/manager.py` creates the `langgraph_sdk` client with `create_internal_auth_headers()` plus CSRF cookie/header, so channel workers do not rely on browser cookies |
| TC-DOCKER-05 (credential surfacing) | `reset_admin` writes `.deer-flow/admin_initial_credentials.txt` with mode 0600 and logs only the path — the only Docker-unique step is whether the bind mount projects this path onto the host, which is a `docker compose` config check, not a runtime behavior change |
| TC-DOCKER-06 (gateway-mode container) | Section 七 7.2 covered by TC-GW-01..05 + Section 二 (gateway-mode auth flow on sg_dev) — same Gateway code, container is just a packaging change |
## Reproduction steps when Docker becomes available
@ -72,6 +72,6 @@ Then run TC-DOCKER-01..06 from the test plan as written.
about *container packaging* details (bind mounts, multi-worker, log
collection), not about whether the auth code paths work.
- **TC-DOCKER-05 was updated in place** in `AUTH_TEST_PLAN.md` to reflect
the post-simplify reality (credentials file → 0600 file, no log leak).
the current reset flow (`reset_admin` → 0600 credentials file, no log leak).
The old "grep 'Password:' in docker logs" expectation would have failed
silently and given a false sense of coverage.

View File

@ -19,7 +19,7 @@
```bash
# 清除已有数据
rm -f backend/.deer-flow/users.db
rm -f backend/.deer-flow/data/deerflow.db
# 选择模式启动
make dev # 标准模式
@ -28,10 +28,11 @@ make dev-pro # Gateway 模式
```
**验证点:**
- [ ] 控制台输出 admin 邮箱和随机密码
- [ ] 密码格式为 `secrets.token_urlsafe(16)` 的 22 字符字符串
- [ ] 邮箱为 `admin@deerflow.dev`
- [ ] 提示 `Change it after login: Settings -> Account`
- [ ] 控制台不输出 admin 邮箱或明文密码
- [ ] 控制台提示 `First boot detected — no admin account exists.`
- [ ] 控制台提示访问 `/setup` 完成 admin 创建
- [ ] `GET /api/v1/auth/setup-status` 返回 `{"needs_setup": true}`
- [ ] 前端访问 `/login` 会跳转 `/setup`
### 1.2 非首次启动
@ -42,7 +43,8 @@ make dev
**验证点:**
- [ ] 控制台不输出密码
- [ ] 如果 admin 仍 `needs_setup=True`,控制台有 warning 提示
- [ ] `GET /api/v1/auth/setup-status` 返回 `{"needs_setup": false}`
- [ ] 已登录用户如果 `needs_setup=True`,访问 workspace 会被引导到 `/setup` 完成改邮箱 / 改密码流程
### 1.3 环境变量配置
@ -76,19 +78,22 @@ make dev
curl -s $BASE/api/v1/auth/setup-status | jq .
```
**预期:** 返回 `{"needs_setup": false}`admin 在启动时已自动创建,`count_users() > 0`)。仅在启动完成前的极短窗口内可能返回 `true`
**预期:**
- 干净数据库且尚未初始化 admin返回 `{"needs_setup": true}`
- 已存在 admin返回 `{"needs_setup": false}`
#### TC-API-02: Admin 首次登录
#### TC-API-02: 首次初始化 Admin
```bash
curl -s -X POST $BASE/api/v1/auth/login/local \
-d "username=admin@deerflow.dev&password=<控制台密码>" \
curl -s -X POST $BASE/api/v1/auth/initialize \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"AdminPass1!"}' \
-c cookies.txt | jq .
```
**预期:**
- 状态码 200
- Body: `{"expires_in": 604800, "needs_setup": true}`
- 状态码 201
- Body: `{"id": "...", "email": "admin@example.com", "system_role": "admin", "needs_setup": false}`
- `cookies.txt` 包含 `access_token`HttpOnly`csrf_token`(非 HttpOnly
#### TC-API-03: 获取当前用户
@ -97,9 +102,9 @@ curl -s -X POST $BASE/api/v1/auth/login/local \
curl -s $BASE/api/v1/auth/me -b cookies.txt | jq .
```
**预期:** `{"id": "...", "email": "admin@deerflow.dev", "system_role": "admin", "needs_setup": true}`
**预期:** `{"id": "...", "email": "admin@example.com", "system_role": "admin", "needs_setup": false}`
#### TC-API-04: Setup 流程(改邮箱 + 改密码)
#### TC-API-04: 改密码流程
```bash
CSRF=$(grep csrf_token cookies.txt | awk '{print $NF}')
@ -107,13 +112,36 @@ curl -s -X POST $BASE/api/v1/auth/change-password \
-b cookies.txt \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d '{"current_password":"<控制台密码>","new_password":"NewPass123!","new_email":"admin@example.com"}' | jq .
-d '{"current_password":"AdminPass1!","new_password":"NewPass123!"}' | jq .
```
**预期:**
- 状态码 200
- `{"message": "Password changed successfully"}`
- 再调 `/auth/me` 邮箱变为 `admin@example.com``needs_setup` 变为 `false`
- 再调 `/auth/me` 仍为 `admin@example.com``needs_setup` 仍为 `false`
#### TC-API-04a: reset_admin 后的 Setup 流程(改邮箱 + 改密码)
```bash
cd backend
python -m app.gateway.auth.reset_admin --email admin@example.com
# 从 .deer-flow/admin_initial_credentials.txt 读取 reset 后密码
curl -s -X POST $BASE/api/v1/auth/login/local \
-d "username=admin@example.com&password=<凭据文件密码>" \
-c cookies.txt | jq .
CSRF=$(grep csrf_token cookies.txt | awk '{print $NF}')
curl -s -X POST $BASE/api/v1/auth/change-password \
-b cookies.txt \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d '{"current_password":"<凭据文件密码>","new_password":"AdminPass2!","new_email":"admin2@example.com"}' | jq .
```
**预期:**
- 登录返回 `{"expires_in": 604800, "needs_setup": true}`
- `change-password``/auth/me` 邮箱变为 `admin2@example.com``needs_setup` 变为 `false`
#### TC-API-05: 普通用户注册
@ -493,7 +521,7 @@ curl -s -X POST $BASE/api/v1/auth/register \
```bash
# 检查数据库
sqlite3 backend/.deer-flow/users.db "SELECT email, password_hash FROM users LIMIT 3;"
sqlite3 backend/.deer-flow/data/deerflow.db "SELECT email, password_hash FROM users LIMIT 3;"
```
**预期:** `password_hash``$2b$` 开头bcrypt 格式)
@ -506,24 +534,25 @@ sqlite3 backend/.deer-flow/users.db "SELECT email, password_hash FROM users LIMI
### 4.1 首次登录流程
#### TC-UI-01: 访问首页跳转登录
#### TC-UI-01: 无 admin 时访问 workspace 跳转 setup
1. 打开 `http://localhost:2026/workspace`
2. **预期:** 自动跳转到 `/login`
2. **预期:** 自动跳转到 `/setup`
#### TC-UI-02: Login 页面
#### TC-UI-02: Setup 页面创建 admin
1. 输入 admin 邮箱和控制台密码
2. 点击 Login
3. **预期:** 跳转到 `/setup`(因为 `needs_setup=true`
#### TC-UI-03: Setup 页面
1. 输入新邮箱、控制台密码current、新密码、确认密码
2. 点击 Complete Setup
1. 输入 admin 邮箱、密码、确认密码
2. 点击 Create Admin Account
3. **预期:** 跳转到 `/workspace`
4. 刷新页面不跳回 `/setup`
#### TC-UI-03: 已初始化后 Login 页面
1. 退出登录后访问 `/login`
2. 输入 admin 邮箱和密码
3. 点击 Login
4. **预期:** 跳转到 `/workspace`
#### TC-UI-04: Setup 密码不匹配
1. 新密码和确认密码不一致
@ -602,7 +631,7 @@ sqlite3 backend/.deer-flow/users.db "SELECT email, password_hash FROM users LIMI
#### TC-UI-15: reset_admin 后重新登录
1. 执行 `cd backend && python -m app.gateway.auth.reset_admin`
2. 使用新密码登录
2. `.deer-flow/admin_initial_credentials.txt` 读取新密码并登录
3. **预期:** 跳转到 `/setup` 页面(`needs_setup` 被重置为 true
4. 旧 session 已失效
@ -645,18 +674,28 @@ make install
make dev
```
#### TC-UPG-01: 首次启动创建 admin
#### TC-UPG-01: 首次启动等待 admin 初始化
**预期:**
- [ ] 控制台输出 admin 邮箱(`admin@deerflow.dev`)和随机密码
- [ ] 控制台不输出 admin 邮箱或随机密码
- [ ] 访问 `/setup` 可创建第一个 admin
- [ ] 无报错,正常启动
#### TC-UPG-02: 旧 Thread 迁移到 admin
```bash
# 创建第一个 admin
curl -s -X POST http://localhost:2026/api/v1/auth/initialize \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"AdminPass1!"}' \
-c cookies.txt
# 重启一次:启动迁移只在已有 admin 的启动路径执行
make stop && make dev
# 登录 admin
curl -s -X POST http://localhost:2026/api/v1/auth/login/local \
-d "username=admin@deerflow.dev&password=<控制台密码>" \
-d "username=admin@example.com&password=AdminPass1!" \
-c cookies.txt
# 查看 thread 列表
@ -670,8 +709,8 @@ curl -s -X POST http://localhost:2026/api/threads/search \
**预期:**
- [ ] 返回的 thread 数量 ≥ 旧版创建的数量
- [ ] 控制台日志有 `Migrated N orphaned thread(s) to admin`
- [ ] 每个 thread 的 `metadata.owner_id` 都已被设为 admin 的 ID
- [ ] 控制台日志有 `Migrated N orphan LangGraph thread(s) to admin`
- [ ] 旧 thread 只对 admin 可见
#### TC-UPG-03: 旧 Thread 内容完整
@ -683,7 +722,7 @@ curl -s http://localhost:2026/api/threads/<old-thread-id> \
**预期:**
- [ ] `metadata.title` 保留原值(如 `old-thread-1`
- [ ] `metadata.owner_id` 已填充
- [ ] 响应不回显服务端保留的 `user_id` / `owner_id`
#### TC-UPG-04: 新用户看不到旧 Thread
@ -706,18 +745,19 @@ curl -s -X POST http://localhost:2026/api/threads/search \
### 5.3 数据库 Schema 兼容
#### TC-UPG-05: 无 users.db 时自动创建
#### TC-UPG-05: 无 deerflow.db 时创建 schema 但不创建默认用户
```bash
ls -la backend/.deer-flow/users.db
ls -la backend/.deer-flow/data/deerflow.db
sqlite3 backend/.deer-flow/data/deerflow.db "SELECT COUNT(*) FROM users;"
```
**预期:** 文件存在,`sqlite3` 可查到 `users` 表含 `needs_setup``token_version`
**预期:** 文件存在,`sqlite3` 可查到 `users` 表含 `needs_setup``token_version`;未调用 `/initialize` 前用户数为 0
#### TC-UPG-06: users.db WAL 模式
#### TC-UPG-06: deerflow.db WAL 模式
```bash
sqlite3 backend/.deer-flow/users.db "PRAGMA journal_mode;"
sqlite3 backend/.deer-flow/data/deerflow.db "PRAGMA journal_mode;"
```
**预期:** 返回 `wal`
@ -768,9 +808,9 @@ make dev
```
**预期:**
- [ ] 服务正常启动(忽略 `users.db`,无 auth 相关代码不报错)
- [ ] 服务正常启动(忽略 `deerflow.db`,无 auth 相关代码不报错)
- [ ] 旧对话数据仍然可访问
- [ ] `users.db` 文件残留但不影响运行
- [ ] `deerflow.db` 文件残留但不影响运行
#### TC-UPG-12: 再次升级到 auth 分支
@ -781,51 +821,47 @@ make dev
```
**预期:**
- [ ] 识别已有 `users.db`,不重新创建 admin
- [ ] 旧的 admin 账号仍可登录(如果回退期间未删 `users.db`
- [ ] 识别已有 `deerflow.db`,不重新创建 admin
- [ ] 旧的 admin 账号仍可登录(如果回退期间未删 `deerflow.db`
### 5.7 休眠 Admin初始密码未使用/未更改)
### 5.7 Admin 初始化与 reset_admin
> 首次启动生成 admin + 随机密码,但运维未登录、未改密码。
> 密码只在首次启动的控制台闪过一次,后续启动不再显示。
> 首次启动不生成默认 admin也不在日志输出密码。忘记密码时走 `reset_admin`,新密码写入 0600 凭据文件。
#### TC-UPG-13: 重启后自动重置密码并打印
#### TC-UPG-13: 未初始化 admin 时重启不创建默认账号
```bash
# 首次启动,记录密码
rm -f backend/.deer-flow/users.db
rm -f backend/.deer-flow/data/deerflow.db
make dev
# 控制台输出密码 P0不登录
make stop
# 隔了几天,再次启动
make dev
# 控制台输出新密码 P1
curl -s $BASE/api/v1/auth/setup-status | jq .
```
**预期:**
- [ ] 控制台输出 `Admin account setup incomplete — password reset`
- [ ] 输出新密码 P1P0 已失效)
- [ ] 用 P1 可以登录P0 不可以
- [ ] 登录后 `needs_setup=true`,跳转 `/setup`
- [ ] `token_version` 递增(旧 session 如有也失效)
- [ ] 控制台不输出密码
- [ ] `setup-status` 仍为 `{"needs_setup": true}`
- [ ] 访问 `/setup` 仍可创建第一个 admin
#### TC-UPG-14: 密码丢失 — 无需 CLI重启即可
#### TC-UPG-14: 密码丢失 — reset_admin 写入凭据文件
```bash
# 忘记了控制台密码 → 直接重启服务
make stop && make dev
# 控制台自动输出新密码
python -m app.gateway.auth.reset_admin --email admin@example.com
ls -la backend/.deer-flow/admin_initial_credentials.txt
cat backend/.deer-flow/admin_initial_credentials.txt
```
**预期:**
- [ ] 无需 `reset_admin`,重启服务即可拿到新密码
- [ ] `reset_admin` CLI 仍然可用作手动备选方案
- [ ] 命令行只输出凭据文件路径,不输出明文密码
- [ ] 凭据文件权限为 `0600`
- [ ] 凭据文件包含 email + password 行
- [ ] 该用户下次登录返回 `needs_setup=true`
#### TC-UPG-15: 休眠 admin 期间普通用户注册
#### TC-UPG-15: 未初始化 admin 期间普通用户注册策略边界
```bash
# admin 存在但从未登录,普通用户先注册
# admin 尚不存在,普通用户尝试注册
curl -s -X POST $BASE/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"earlybird@example.com","password":"EarlyPass1!"}' \
@ -833,11 +869,11 @@ curl -s -X POST $BASE/api/v1/auth/register \
```
**预期:**
- [ ] 注册成功201角色为 `user`
- [ ] 无法提权为 admin
- [ ] 普通用户的数据与 admin 隔离
- [ ] 当前代码允许注册普通用户并自动登录201角色为 `user`
- [ ] `setup-status` 仍为 `{"needs_setup": true}`,因为 admin 仍不存在
- [ ] 这是一个产品策略边界:若要求“必须先有 admin”需要在 `/register` 增加 admin-exists gate
#### TC-UPG-16: 休眠 admin 不影响后续操作
#### TC-UPG-16: 普通用户数据与后续 admin 隔离
```bash
# 普通用户正常创建 thread、发消息
@ -849,14 +885,13 @@ curl -s -X POST $BASE/api/threads \
-d '{"metadata":{}}' | jq .thread_id
```
**预期:** 正常创建,不受休眠 admin 影响
**预期:** 普通用户正常创建 thread后续 admin 创建后,搜索不到该普通用户 thread
#### TC-UPG-17: 休眠 admin 最终完成 Setup
#### TC-UPG-17: reset_admin 后完成 Setup
```bash
# 运维终于登录
curl -s -X POST $BASE/api/v1/auth/login/local \
-d "username=admin@deerflow.dev&password=<P0或P1>" \
-d "username=admin@example.com&password=<凭据文件密码>" \
-c admin.txt | jq .needs_setup
# 预期: true
@ -866,7 +901,7 @@ curl -s -X POST $BASE/api/v1/auth/change-password \
-b admin.txt \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d '{"current_password":"<密码>","new_password":"AdminFinal1!","new_email":"admin@real.com"}' \
-d '{"current_password":"<凭据文件密码>","new_password":"AdminFinal1!","new_email":"admin@real.com"}' \
-c admin.txt
# 验证
@ -876,7 +911,7 @@ curl -s $BASE/api/v1/auth/me -b admin.txt | jq '{email, needs_setup}'
**预期:**
- [ ] `email` 变为 `admin@real.com`
- [ ] `needs_setup` 变为 `false`
- [ ] 后续重启控制台不再有 warning
- [ ] 后续登录使用新密码
#### TC-UPG-18: 长期未用后 JWT 密钥轮换
@ -890,8 +925,8 @@ make stop && make dev
**预期:**
- [ ] 服务正常启动
- [ ] 密码仍可登录(密码存在 DB与 JWT 密钥无关)
- [ ] 旧的 JWT token 失效(密钥变了签名不匹配)— 但因为从未登录过也没有旧 token
- [ ] 账号密码仍可登录(密码存在 DB与 JWT 密钥无关)
- [ ] 旧的 JWT token 失效(密钥变了签名不匹配)
---
@ -910,7 +945,7 @@ for i in 1 2 3; do
done
# 检查 admin 数量
sqlite3 backend/.deer-flow/users.db \
sqlite3 backend/.deer-flow/data/deerflow.db \
"SELECT COUNT(*) FROM users WHERE system_role='admin';"
```
@ -1055,7 +1090,7 @@ curl -s -X POST $BASE/api/v1/auth/register \
wait
# 检查用户数
sqlite3 backend/.deer-flow/users.db \
sqlite3 backend/.deer-flow/data/deerflow.db \
"SELECT COUNT(*) FROM users WHERE email='race@example.com';"
```
@ -1165,13 +1200,16 @@ curl -s -w "%{http_code}" -X DELETE "$BASE/api/threads/$TID" \
```bash
cd backend
python -m app.gateway.auth.reset_admin
# 记录密码 P1
cp .deer-flow/admin_initial_credentials.txt /tmp/deerflow-reset-p1.txt
P1=$(awk -F': ' '/^password:/ {print $2}' /tmp/deerflow-reset-p1.txt)
python -m app.gateway.auth.reset_admin
# 记录密码 P2
cp .deer-flow/admin_initial_credentials.txt /tmp/deerflow-reset-p2.txt
P2=$(awk -F': ' '/^password:/ {print $2}' /tmp/deerflow-reset-p2.txt)
```
**预期:**
- [ ] `.deer-flow/admin_initial_credentials.txt` 每次都会被重写,文件权限为 `0600`
- [ ] P1 ≠ P2每次生成新随机密码
- [ ] P1 不可用,只有 P2 有效
- [ ] `token_version` 递增了 2
@ -1324,7 +1362,8 @@ done
```bash
GW=http://localhost:8001
for path in /health /api/v1/auth/setup-status /api/v1/auth/login/local /api/v1/auth/register; do
for path in /health /api/v1/auth/setup-status /api/v1/auth/login/local \
/api/v1/auth/register /api/v1/auth/initialize /api/v1/auth/logout; do
echo "$path: $(curl -s -w '%{http_code}' -o /dev/null $GW$path)"
done
# 预期: 200 或 405/422方法不对但不是 401
@ -1399,9 +1438,9 @@ done
>
> 前置条件:
> - `.env` 中设置 `AUTH_JWT_SECRET`(否则每次容器重启 session 全部失效)
> - `DEER_FLOW_HOME` 挂载到宿主机目录(持久化 `users.db`
> - `DEER_FLOW_HOME` 挂载到宿主机目录(持久化 `deerflow.db`
#### TC-DOCKER-01: users.db 通过 volume 持久化
#### TC-DOCKER-01: deerflow.db 通过 volume 持久化
```bash
# 启动容器
@ -1416,13 +1455,13 @@ curl -s -X POST $BASE/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"docker-test@example.com","password":"DockerTest1!"}' -w "\nHTTP %{http_code}"
# 检查宿主机上的 users.db
ls -la ${DEER_FLOW_HOME:-backend/.deer-flow}/users.db
sqlite3 ${DEER_FLOW_HOME:-backend/.deer-flow}/users.db \
# 检查宿主机上的 deerflow.db
ls -la ${DEER_FLOW_HOME:-backend/.deer-flow}/data/deerflow.db
sqlite3 ${DEER_FLOW_HOME:-backend/.deer-flow}/data/deerflow.db \
"SELECT email FROM users WHERE email='docker-test@example.com';"
```
**预期:** users.db 在宿主机 `DEER_FLOW_HOME` 目录中,查询可见刚注册的用户。
**预期:** deerflow.db 在宿主机 `DEER_FLOW_HOME` 目录中,查询可见刚注册的用户。
#### TC-DOCKER-02: 重启容器后 session 保持
@ -1466,22 +1505,24 @@ done
**已知限制:** In-process rate limiter 不跨 worker 共享。生产环境如需精确限速,需要 Redis 等外部存储。
#### TC-DOCKER-04: IM 渠道不经过 auth
#### TC-DOCKER-04: IM 渠道使用内部认证
```bash
# IM 渠道Feishu/Slack/Telegram在 gateway 容器内部通过 LangGraph SDK 通信
# 不走 nginx不经过 AuthMiddleware
# IM 渠道Feishu/Slack/Telegram在 gateway 容器内部通过 LangGraph SDK 调 Gateway
# 请求携带 process-local internal auth header并带匹配的 CSRF cookie/header
# 验证方式:检查 gateway 日志中 channel manager 的请求不包含 auth 错误
docker logs deer-flow-gateway 2>&1 | grep -E "ChannelManager|channel" | head -10
```
**预期:** 无 auth 相关错误。渠道通过 `langgraph-sdk` 直连 LangGraph Server`http://langgraph:2024`),不走 auth 层
**预期:** 无 auth 相关错误。渠道不依赖浏览器 cookie服务端通过内部认证头把请求归入 `default` 用户桶
#### TC-DOCKER-05: admin 密码写入 0600 凭证文件(不再走日志)
#### TC-DOCKER-05: reset_admin 密码写入 0600 凭证文件(不再走日志)
```bash
# 凭证文件写在挂载到宿主机的 DEER_FLOW_HOME 下
# 首次启动不会自动生成 admin 密码。先重置已有 admin凭据文件写在挂载到宿主机的 DEER_FLOW_HOME 下。
docker exec deer-flow-gateway python -m app.gateway.auth.reset_admin --email docker-test@example.com
ls -la ${DEER_FLOW_HOME:-backend/.deer-flow}/admin_initial_credentials.txt
# 预期文件权限: -rw------- (0600)
@ -1512,14 +1553,15 @@ sleep 15
docker ps --filter name=deer-flow-langgraph --format '{{.Names}}' | wc -l
# 预期: 0
# auth 流程正常
# auth 流程正常:未登录受保护接口返回 401
curl -s -w "%{http_code}" -o /dev/null $BASE/api/models
# 预期: 401
curl -s -X POST $BASE/api/v1/auth/login/local \
-d "username=admin@deerflow.dev&password=<日志密码>" \
curl -s -X POST $BASE/api/v1/auth/initialize \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"AdminPass1!"}' \
-c cookies.txt -w "\nHTTP %{http_code}"
# 预期: 200
# 预期: 201
```
### 7.4 补充边界用例
@ -1587,13 +1629,15 @@ curl -s -D - -X POST $BASE/api/v1/auth/login/local \
#### TC-EDGE-05: HTTP 无 max_age / HTTPS 有 max_age
```bash
GW=http://localhost:8001
# HTTP
curl -s -D - -X POST $BASE/api/v1/auth/login/local \
curl -s -D - -X POST $GW/api/v1/auth/login/local \
-d "username=admin@example.com&password=正确密码" 2>/dev/null \
| grep "access_token=" | grep -oi "max-age=[0-9]*" || echo "NO max-age (HTTP session cookie)"
# HTTPS
curl -s -D - -X POST $BASE/api/v1/auth/login/local \
# HTTPS:直连 Gateway 才能用 X-Forwarded-Proto 模拟 HTTPSnginx 会覆盖该 header
curl -s -D - -X POST $GW/api/v1/auth/login/local \
-H "X-Forwarded-Proto: https" \
-d "username=admin@example.com&password=正确密码" 2>/dev/null \
| grep "access_token=" | grep -oi "max-age=[0-9]*"
@ -1712,10 +1756,10 @@ curl -s -X POST $BASE/api/threads \
-b cookies.txt \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: $CSRF" \
-d '{"metadata":{"owner_id":"victim-user-id"}}' | jq .metadata.owner_id
-d '{"metadata":{"owner_id":"victim-user-id","user_id":"victim-user-id"}}' | jq .metadata
```
**预期:** 返回的 `metadata.owner_id` 应为当前登录用户的 ID不是请求中注入的 `victim-user-id`。服务端应覆盖客户端提供的 `user_id`
**预期:** 返回的 `metadata` 不包含 `owner_id``user_id`。真实所有权写入 `threads_meta.user_id`,不从客户端 metadata 接收,也不通过 metadata 回显
#### 7.5.6 HTTP Method 探测
@ -1796,6 +1840,6 @@ cd backend && PYTHONPATH=. uv run pytest \
# 核心接口冒烟
curl -s $BASE/health # 200
curl -s $BASE/api/models # 401 (无 cookie)
curl -s -X POST $BASE/api/v1/auth/setup-status # 200
curl -s $BASE/api/v1/auth/setup-status # 200
curl -s $BASE/api/v1/auth/me -b cookies.txt # 200 (有 cookie)
```

View File

@ -2,13 +2,16 @@
DeerFlow 内置了认证模块。本文档面向从无认证版本升级的用户。
完整设计见 [AUTH_DESIGN.md](AUTH_DESIGN.md)。
## 核心概念
认证模块采用**始终强制**策略:
- 首次启动时自动创建 admin 账号,随机密码打印到控制台日志
- 首次启动时不会自动创建账号;首次访问 `/setup` 时由操作者创建第一个 admin 账号
- 认证从一开始就是强制的,无竞争窗口
- 历史对话(升级前创建的 thread自动迁移到 admin 名下
- 已有 admin 后,服务启动时会把历史对话(升级前创建且缺少 `user_id` 的 thread迁移到 admin 名下
- 新数据按用户隔离thread、workspace/uploads/outputs、memory、自定义 agent 都归属当前用户
## 升级步骤
@ -25,39 +28,41 @@ cd backend && make install
make dev
```
控制台会输出
如果没有 admin 账号,控制台只会提示
```
============================================================
Admin account created on first boot
Email: admin@deerflow.dev
Password: aB3xK9mN_pQ7rT2w
Change it after login: Settings → Account
First boot detected — no admin account exists.
Visit /setup to complete admin account creation.
============================================================
```
如果未登录就重启了服务,不用担心——只要 setup 未完成,每次启动都会重置密码并重新打印到控制台
首次启动不会在日志里打印随机密码,也不会写入默认 admin。这样避免启动日志泄露凭据也避免在操作者创建账号前出现可被猜测的默认身份
### 3. 登录
### 3. 创建 admin
访问 `http://localhost:2026/login`,使用控制台输出的邮箱和密码登录
访问 `http://localhost:2026/setup`,填写邮箱和密码创建第一个 admin 账号。创建成功后会自动登录并进入 workspace
### 4. 修改密码
如果这是从无认证版本升级,创建 admin 后重启一次服务,让启动迁移把缺少 `user_id` 的历史 thread 归属到 admin。
登录后进入 Settings → Account → Change Password。
### 4. 登录
后续访问 `http://localhost:2026/login`,使用已创建的邮箱和密码登录。
### 5. 添加用户(可选)
其他用户通过 `/login` 页面注册,自动获得 **user** 角色。每个用户只能看到自己的对话。
其他用户通过 `/login` 页面注册,自动获得 **user** 角色。每个用户只能看到自己的对话、上传文件、输出文件、memory 和自定义 agent
## 安全机制
| 机制 | 说明 |
|------|------|
| JWT HttpOnly Cookie | Token 不暴露给 JavaScript防止 XSS 窃取 |
| CSRF Double Submit Cookie | 所有 POST/PUT/DELETE 请求需携带 `X-CSRF-Token` |
| CSRF Double Submit Cookie | 受保护的 POST/PUT/PATCH/DELETE 请求需携带 `X-CSRF-Token`;登录/注册/初始化/登出走 auth 端点 Origin 校验 |
| bcrypt 密码哈希 | 密码不以明文存储 |
| 多租户隔离 | 用户只能访问自己的 thread |
| Thread owner filter | `threads_meta.user_id` 由服务端认证上下文写入,搜索、读取、更新、删除默认按当前用户过滤 |
| 文件系统隔离 | 线程数据写入 `{base_dir}/users/{user_id}/threads/{thread_id}/user-data/`sandbox 内统一映射为 `/mnt/user-data/` |
| Memory / agent 隔离 | 用户 memory 和自定义 agent 写入 `{base_dir}/users/{user_id}/...`;旧共享 agent 只作为只读兼容回退 |
| HTTPS 自适应 | 检测 `x-forwarded-proto`,自动设置 `Secure` cookie 标志 |
## 常见操作
@ -74,22 +79,26 @@ python -m app.gateway.auth.reset_admin
python -m app.gateway.auth.reset_admin --email user@example.com
```
输出新的随机密码。
把新的随机密码写入 `.deer-flow/admin_initial_credentials.txt`,文件权限为 `0600`。命令行只输出文件路径,不输出明文密码。
### 完全重置
删除用户数据库,重启后自动创建新 admin
删除统一 SQLite 数据库,重启后重新访问 `/setup` 创建新 admin
```bash
rm -f backend/.deer-flow/users.db
# 重启服务,控制台输出新密码
rm -f backend/.deer-flow/data/deerflow.db
# 重启服务后访问 http://localhost:2026/setup
```
## 数据存储
| 文件 | 内容 |
|------|------|
| `.deer-flow/users.db` | SQLite 用户数据库(密码哈希、角色) |
| `.deer-flow/data/deerflow.db` | 统一 SQLite 数据库users、threads_meta、runs、feedback 等应用数据) |
| `.deer-flow/users/{user_id}/threads/{thread_id}/user-data/` | 用户线程的 workspace、uploads、outputs |
| `.deer-flow/users/{user_id}/memory.json` | 用户级 memory |
| `.deer-flow/users/{user_id}/agents/{agent_name}/` | 用户自定义 agent 配置、SOUL 和 agent memory |
| `.deer-flow/admin_initial_credentials.txt` | `reset_admin` 生成的新凭据文件0600读完应删除 |
| `.env` 中的 `AUTH_JWT_SECRET` | JWT 签名密钥(未设置时自动生成临时密钥,重启后 session 失效) |
### 生产环境建议
@ -111,19 +120,21 @@ python -c "import secrets; print(secrets.token_urlsafe(32))"
| `/api/v1/auth/me` | GET | 获取当前用户信息 |
| `/api/v1/auth/change-password` | POST | 修改密码 |
| `/api/v1/auth/setup-status` | GET | 检查 admin 是否存在 |
| `/api/v1/auth/initialize` | POST | 首次初始化第一个 admin仅无 admin 时可调用) |
## 兼容性
- **标准模式**`make dev`):完全兼容admin 自动创建
- **标准模式**`make dev`):完全兼容;无 admin 时访问 `/setup` 初始化
- **Gateway 模式**`make dev-pro`):完全兼容
- **Docker 部署**:完全兼容,`.deer-flow/users.db` 需持久化卷挂载
- **IM 渠道**Feishu/Slack/Telegram通过 LangGraph SDK 通信,不经过认证层
- **Docker 部署**:完全兼容,`.deer-flow/data/deerflow.db` 需持久化卷挂载
- **IM 渠道**Feishu/Slack/Telegram通过 Gateway 内部认证通信,使用 `default` 用户桶
- **DeerFlowClient**(嵌入式):不经过 HTTP不受认证影响
## 故障排查
| 症状 | 原因 | 解决 |
|------|------|------|
| 启动后没看到密码 | admin 已存在(非首次启动) | 用 `reset_admin` 重置,或删 `users.db` |
| 启动后没看到密码 | 当前实现不在启动日志输出密码 | 首次安装访问 `/setup`;忘记密码用 `reset_admin` |
| `/login` 自动跳到 `/setup` | 系统还没有 admin | 在 `/setup` 创建第一个 admin |
| 登录后 POST 返回 403 | CSRF token 缺失 | 确认前端已更新 |
| 重启后需要重新登录 | `AUTH_JWT_SECRET` 未持久化 | 在 `.env` 中设置固定密钥 |

View File

@ -8,6 +8,7 @@ This directory contains detailed documentation for the DeerFlow backend.
|----------|-------------|
| [ARCHITECTURE.md](ARCHITECTURE.md) | System architecture overview |
| [API.md](API.md) | Complete API reference |
| [AUTH_DESIGN.md](AUTH_DESIGN.md) | User authentication, CSRF, and per-user isolation design |
| [CONFIGURATION.md](CONFIGURATION.md) | Configuration options |
| [SETUP.md](SETUP.md) | Quick setup guide |
@ -42,6 +43,7 @@ docs/
├── README.md # This file
├── ARCHITECTURE.md # System architecture
├── API.md # API reference
├── AUTH_DESIGN.md # User authentication and isolation design
├── CONFIGURATION.md # Configuration guide
├── SETUP.md # Setup instructions
├── FILE_UPLOAD.md # File upload feature