Files
home-automation/docs/auth.md
T

9.2 KiB
Raw Permalink Blame History

鉴权说明

本文档说明当前已落地的鉴权基座(基础 session 鉴权 + M4 登录加固)。

当前 auth 模型

  • 认证方式:username/password(可选启用 TOTP 二次验证)
  • 会话方式:server-side session
  • 客户端凭据:session cookie

持久化

所有 auth 相关数据存放在单一 App DB(APP_DATABASE_URL,默认 sqlite:///./data/app.db)中:

  • auth_users:用户表(含 TOTP 字段 totp_secret / totp_enabled
  • auth_sessionssession token 哈希与过期时间
  • auth_login_throttle:登录失败退避状态(按 IP / username 双键)
  • auth_recovery_codeTOTP 恢复码哈希(一次性)
  • app_configruntime 配置持久化

首次启动与 bootstrap

如果 auth DB 中还没有任何用户,应用启动时会使用:

  • AUTH_BOOTSTRAP_USERNAME
  • AUTH_BOOTSTRAP_PASSWORD

创建首个 admin 用户。当前默认 bootstrap 值为:

  • username: admin
  • password: admin

首次登录后,系统会强制要求修改密码。

基础安全设计

当前这版已经落实的安全点:

  • 密码不明文存储,使用 Argon2 哈希
  • session cookie 为 HttpOnly
  • cookie 使用 SameSite=Lax
  • Secure cookie 在非 development 环境默认开启
  • 写请求(POST/PUT/PATCH/DELETE)需携带 X-CSRF-Token headerSameSite=Lax + 自定义 header 纵深防御,无需 per-session token 值比对)
  • session token 为随机生成,服务端只持久化 token hash
  • session 有过期时间(默认 12 小时)与显式失效机制

M4 登录加固

M4 在基础鉴权之上叠加了三层防御:防爆破/指数退避、CLI 逃生通道、可选 TOTP 二次验证。

1. 防爆破 / 指数退避

机制

登录失败按指数增长延迟,目标是拖垮暴力枚举,同时不因此永久锁定账号。

退避是延迟(429 + Retry-After),不是永久封号——单 admin 场景下永久锁会被攻击者反向用来故意打锁,所以退避只增加等待时间,CLI 是最终逃生口。

双键计算

每次登录请求同时按 client IPusername 各记一套失败计数,本次需等待时间 = 两者退避的较大值

  • 按 IP:堵单点暴力(攻击 IP 越敲越慢,合法用户换 IP 不受影响)
  • 按 username:全局兜底(即便攻击者分布式换 IP 也有一层保护)

指数公式

N_FREE  = 3       — 前 3 次失败不延迟(免费次数)
BASE    = 1  秒
CAP     = 900 秒(15 分钟)

wait = min(CAP, BASE × 2^(failures - N_FREE))   failures > N_FREE 时生效)

延迟示意:

累计失败次数 等待时间
13 0 秒(免费)
4 2 秒
5 4 秒
6 8 秒
7 16 秒
13+ 900 秒(封顶)

成功登录后,该 IP 和 username 的退避状态立即清零

登录端点行为(POST /api/auth/login

  1. 先查退避(在窗口内直接 429不验密码,节省 Argon2 计算并防止枚举)
  2. 验密码失败 → 记一次失败(IP + username 各记),返回 401
  3. 密码正确但 TOTP 启用且缺少 totp_code → 返回 401 {totp_required: true}不记失败(这是正常两步流程的第一步,不是攻击信号)
  4. 密码正确但 TOTP 验证失败 → 记一次失败,返回 401
  5. 全部通过 → 清零退避状态,发 session cookie

配置项

配置项 类型 默认 说明
AUTH_LOGIN_THROTTLE_ENABLED boolCONFIG_FIELDS true 全局开关,关闭后整个退避机制 no-op
AUTH_TRUST_FORWARDED_FOR bool.env 部署级) false true 时取 X-Forwarded-For 最左作为 client IP反代后必须显式开启,否则反代 IP 全视为同一 IP,按 IP 退避失效

反代部署注意AUTH_TRUST_FORWARDED_FOR 默认 false(直接用 socket IP)。部署在 nginx 等反代后面时,应设 AUTH_TRUST_FORWARDED_FOR=true,并确保反代正确设置 X-Forwarded-For


2. CLI 逃生通道

设计原则

CLI 通过 直连本地 DBget_session_local())工作,无需 HTTP 服务器运行、无需任何已存凭据(密码、恢复码均不需要)。拿到服务器 CLI 权限本身就意味着对系统有完全控制,因此这是可接受且必须存在的最终逃生口。

CLI 只动 auth 相关行(auth_users 的密码/TOTP 字段、auth_login_throttleauth_recovery_code),绝不触碰用户数据表(locationpoo_recordspublic_ip_state 等)。

入口:python -m scripts.admin_cli <command>

子命令

命令 用途 逃生场景
reset-password <username> [--password <pwd>] 重置密码(不带 --password 则交互式输入,不回显) 忘记密码
unlock [--all | --ip <ip> | --username <u>] auth_login_throttle 被退避锁住(429)时解锁
disable-totp <username> 关停 TOTP(清 secret + 删全部恢复码),零凭据可执行 恢复码全丢也能进
reissue-totp <username> 生成新 TOTP secret 并打印 otpauth:// URI 设备丢失、需重新注册 Authenticator
list-admin 列出所有用户与状态列 排障用

使用示例

# 重置密码(不带 --password 时交互式 prompt,不回显)
python -m scripts.admin_cli reset-password admin

# 重置密码(非交互,脚本里用)
python -m scripts.admin_cli reset-password admin --password "newpassword"

# 解锁所有退避
python -m scripts.admin_cli unlock --all

# 解锁特定 IP
python -m scripts.admin_cli unlock --ip 1.2.3.4

# 解锁特定 username
python -m scripts.admin_cli unlock --username admin

# 关停 TOTP(零凭据,最终逃生口)
python -m scripts.admin_cli disable-totp admin

# 重新发放 TOTP secret(打印新 otpauth:// URI,扫码重新注册)
python -m scripts.admin_cli reissue-totp admin

# 查看用户列表
python -m scripts.admin_cli list-admin

在 Docker 容器内执行:

docker compose exec app python -m scripts.admin_cli <command>

reissue-totp 语义说明

  • 已启用 TOTP 的用户:新 secret 立即在登录时生效,旧 Authenticator 生成的码立即失效;无需再走 web enable 步骤。
  • 现有恢复码不被删除——恢复码是独立的随机哈希值,与 TOTP secret 无密码学绑定,reissue 后恢复码依然有效(仍可用于登录)。
  • 如果需要完整清理(secret + 所有恢复码),使用 disable-totp

3. 可选 TOTP 二次验证

设计

  • TOTP 遵循 RFC 6238,使用 pyotp 库。
  • 二维码在前端qrcode.react 渲染(后端只返回 otpauth:// URI,不引图像依赖)。
  • totp_secret 明文存库(与项目其他 secret 处理一致,靠数据库文件权限保护)。
  • 恢复码以 Argon2 哈希存库,一次性(使用后标记 used_at)。

启用流程

  1. setupPOST /api/auth/totp/setup):生成 pending secret,返回 secret、otpauth:// URI、10 个明文恢复码(仅此一次);此时 totp_enabled 仍为 false
  2. 扫码:用 Authenticator App 扫前端渲染的二维码(或手动输入 secret)。
  3. enablePOST /api/auth/totp/enable):输入当前 6 位码确认 → totp_enabled 变为 true
  4. 妥善保存恢复码(不会再次展示)。

停用流程

web 停用POST /api/auth/totp/disable):需提供当前密码或当前 6 位 TOTP 码。成功后清 secret、删全部恢复码,恢复纯密码登录。

CLI 停用(逃生口,恢复码全丢时):python -m scripts.admin_cli disable-totp admin,零凭据,立即生效。

登录二步流程

  1. 提交 POST /api/auth/login {username, password} → 密码正确但 TOTP 已启用 → 401 {totp_required: true}(不发 session
  2. 前端切到第二屏,提交 POST /api/auth/login {username, password, totp_code} → 通过 → 发 session cookie

totp_code 可以是 6 位 TOTP 动态码,也可以是 xxxx-xxxx 格式恢复码(命中即消费,不可复用)。

API 端点

端点 用途
POST /api/auth/totp/setup 生成 pending secret + URI + 恢复码(一次性明文返回)
POST /api/auth/totp/enable 带当前 6 位码确认启用
POST /api/auth/totp/disable 带密码或当前码停用
GET /api/auth/totp 返回当前 TOTP 状态({enabled: bool}),不返回 secret/恢复码

全部端点需要 session cookieGET /api/auth/totp 不需 CSRF;其余写端点需 X-CSRF-Token)。

配置项

配置项 类型 默认 说明
AUTH_TOTP_ISSUER str.env 部署级) 空(回退 app_name 显示在 Authenticator App 里的 issuer 标签

受保护范围

当前 JSON API 端点(/api/*)需要 session cookie;写端点需额外携带 X-CSRF-Token header。

裸 ingestion 端点(/location/record/poo/record 等设备调用端点)暂未收口到 session 保护(M3 计划引入 token 鉴权)。

下一步(不在当前范围)

  • M3:token 鉴权(供脚本 / 设备 / 移动端调用 API),ingestion 端点收口。