# dsh-auth-gateway

<p align="center">
  <img src="https://raw.githubusercontent.com/xbzbing/dsh-auth-gateway/main/docs/assets/architecture.png" alt="dsh-auth-gateway 架构图" width="720">
</p>

<p align="center">
<a href="https://www.npmjs.com/package/dsh-auth-gateway"><img src="https://img.shields.io/npm/v/dsh-auth-gateway.svg" alt="npm version"></a>
<a href="https://www.npmjs.com/package/dsh-auth-gateway"><img src="https://img.shields.io/npm/dt/dsh-auth-gateway.svg" alt="npm total downloads"></a>
<a href="LICENSE"><img src="https://img.shields.io/npm/l/dsh-auth-gateway.svg" alt="npm license"></a>
</p>

<p align="center"><b>Language: 简体中文 | <a href="README.en.md">English</a></b></p>

为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) Web 提供认证门禁的 Cordis 插件：**密码认证 + TOTP 双因素认证 + 多层防爆破 + 会话管理 + 登录审计**，并在网关层**真实拦截每一个请求**（HTTP 与 WebSocket），未认证流量无法触及后端。

`dsh web` 的官方认证只面向本机回环：dsh 0.1.2 起内部 webserver 启用内置浏览器认证（BrowserAuth），但其设计说明明确写道「没有登出操作，也没有针对反向代理/网关的处理」（*"There is no logout operation or reverse-proxy-specific handling"*），CLI 依旧拒绝 `--host 0.0.0.0`——**dsh 从未预想或支持远程访问，也没有为「前端再套一层网关」预留任何集成通道**。本插件以进程内网关形态补齐官方未提供的远程访问认证面：对外端口由网关独占，内部 webserver 由 bundle patch 钉在回环地址，网关是唯一入口。

本项目已支持最新的 dsh 0.1.6-alpha.2 版本。dsh 0.1.2 起内部 webserver 新增了内置浏览器认证（BrowserAuth）：网关经官方 `credentials` 服务读取 upstream 会话密钥，为回环转发自动铸造 upstream cookie，对浏览器与部署方式透明（机制详见 [docs/zh/SECURITY.md](docs/zh/SECURITY.md)）。0.1.5 与 0.1.6 系列均已验证兼容：网关所依赖的全部扩展点（`webServer.tapIndex`、`dsh.bundle` patch、`settings.section` slot、`credentials` record 与 BrowserAuth cookie 格式）在 0.1.6-alpha.2 均未变动（settings 面板新增官方 section 至 order 25，本插件 100 依然严格最大），WS 无限重连与文件上传流式转发均可正常通过网关；0.1.6 起 profile 配置 HMR 默认开启（`dsh-hmr`），部署后插件自身变更仍按既有流程生效。

## 安装和卸载

```bash
# 安装（从 npm registry）
dsh plugin --profile web add dsh-auth-gateway

# 启动（对外端口 8080，内部 webserver 自动挪到 8081）
dsh web --port 8080

# 卸载（先清凭据，再移除插件）
~/.dsh/profiles/web/node_modules/.bin/dsh-auth-gateway-uninstall
dsh plugin --profile web remove dsh-auth-gateway
```

- 支持从 GitHub / 本地目录安装，见 [docs/zh/INSTALL.md](docs/zh/INSTALL.md)；
- 忘记密码用 `dsh-auth-gateway-reset` 重置（重启后控制台打印新初始密码）；
- 部署指南：[docs/zh/DEPLOYMENT.md](docs/zh/DEPLOYMENT.md)

## 功能特性

- **密码认证**：首次部署自动生成初始密码（控制台打印，一次性），登录后引导设置个人密码（scrypt 哈希存储），之后每次访问需登录；
- **双因素认证（TOTP）**：可选启用，兼容 Google Authenticator、Authy、1Password 等主流认证器；含一次性备份代码（scrypt 哈希存储、单次使用），设备丢失时可恢复访问；**OTP 密钥以 AES-256-GCM 加密存储**（主密钥来自环境变量 `DSH_AUTH_GATEWAY_MASTER_KEY` 或自动生成的 `auth-gateway/otp-master.key`），磁盘泄露不再直接暴露第二因素根密钥；
- **真实请求拦截**：未认证 `/api/*` 返回 401、页面类路径 302 到登录页、WebSocket 升级直接拒绝；认证通过后请求透明转发（Host/Origin 规范化，兼容内部 trust fence）；
- **登录审计**：登录成功 / 失败 / 登出 / 改密与暴力破解告警（锁定/限流）均输出审计日志（`ctx.logger.info`/`warn`，含来源 IP 与失败原因，不记录任何凭据），并**持久化落盘** `$DSH_HOME/auth-gateway/log/audit.log`（JSONL，按天轮转、保留 90 天），形成完整可审计闭环；
- **多层防爆破**：密码失败按来源锁定（默认 5 次/5 分钟）+ 全局速率限制（默认 60 次/分钟）+ OTP/备份码独立限流（默认 10 次/分钟），scrypt 在 libuv 线程池异步执行，登录洪峰不阻塞事件循环；
- **会话管理**：内存 256-bit token（30 天），HttpOnly + SameSite=Strict Cookie，修改密码/禁用 OTP 吊销全部会话；
- **关于卡片**：设置面板显示当前版本号与仓库链接（读取本机 `package.json`，离线可用），并可用「检查更新」按钮主动查询新版本——**自动检查默认关闭**，全新安装不会发起任何对外请求；点击按钮才会让网关向公共 npm registry 查询一次 `latest` 标签（这是本插件唯一的对外请求，见 [安全模型](docs/zh/SECURITY.md)）。registry 不可达时显示「暂时无法检查更新」，绝不干扰认证与转发；
- **合规形态**：host-only 插件（零构建、零运行时依赖）+ 可选 client 半（设置面板，源码构建），主体全部经 dsh 官方扩展点（`ctx.effect`、`webServer.tapIndex`、`ctx.slots`）；唯有一项记录在案的安全例外——LAN trust（为域名/反代访问下模型设置页可用而对 connection 注册做最小介入，见 [TROUBLESHOOTING §1](docs/zh/TROUBLESHOOTING.md)）。

## 本插件不做的事情

以下需求在"单实例"前提下**无法真正实现**——它们的前提是进程/OS 强制的执行与存储隔离（独立 OS 账号、容器或沙盒），而本插件只是运行在 dsh 进程内的认证网关，提供不了这层隔离。列出它们是为了明确预期、避免误导：

- **多账号登录 / 多租户**：dsh 是单用户工具——一个 Home、一份模型凭据，全部会话与数据（`sessions/`、`workspace/`、`.credentials.yaml`）都以运行 dsh 的 OS 账号权限存放在本地。网关叠加"账号体系"只能区分**谁在登录**（访问控制 + 审计），无法隔离**谁能看到什么**：任何通过认证的用户都能经 dsh 的工具执行读取同一 Home 下的全部会话与凭据。**没有 OS/容器/沙盒隔离就没有真正的多租户**——本插件不做，也无法做到。
- **角色权限限制（用户/管理员）**：同理，角色只能在网关自身的 HTTP 路由层生效（例如限制网关管理功能），挡不住 dsh 内部的能力面——普通用户一旦通过认证门，即拥有该实例的完整能力（工具执行、会话读写、配置与凭据访问）。需要"普通用户受限"的场景请用 OS 级隔离的多实例部署并自行管理账号。本插件的职责是：**认证门禁（谁能进入）+ 拦截与审计（谁做了什么），不承担、也无法承担授权与隔离模型**。

## 工作原理

```
浏览器 ──> dsh-auth-gateway 网关（对外端口，运行在 dsh 进程内）
               │  每个请求先过认证检查（会话表 O(1)）
               ├─ 未认证 ─> /api/*: 401 ｜ 页面: 302 /login ｜ WS: 拒绝
               ├─ 未通过 2FA ─> /otp/verify
               └─ 已认证 ─> 转发（Host/Origin 改写为回环）──> dsh webserver（127.0.0.1:内部端口）
```

- 网关生命周期与 dsh 绑定：随 dsh 启动/退出，无独立进程；
- bundle patch 将 webserver 移到回环端口（对外 = `--port`，内部 = 对外 + 1），远程无法绕过网关直连后端；
- 网关在 DSH 的 `__ModuleLoader__` 加载 connection 模块时、Settings 等消费者启动前建立客户端 loopback trust——这是**唯一记录在案的安全例外**（仅拦截 connection 注册，其他插件原样通过）；该兼容层不替代登录、HTTP/WebSocket 门禁或服务端 fence，详见 [TROUBLESHOOTING §1](docs/zh/TROUBLESHOOTING.md)；
- 认证状态机：`首次部署 → 初始密码登录 → 引导（设置个人密码）→ 登录 →（可选）OTP 验证 → 会话`；未完成引导或 2FA 的会话仅能访问对应验证端点。

## 界面预览

<table>
<tr>
<td align="center"><img src="docs/assets/onboarding.png" width="480" alt="引导页（设置个人密码）"><br/>引导页（初始密码登录后）</td>
<td align="center"><img src="docs/assets/login.png" width="480" alt="登录（含 2FA 验证码）"><br/>登录（含 2FA 验证码）</td>
</tr>
<tr>
<td align="center"><img src="docs/assets/login-success.png" width="480" alt="2FA 登录成功"><br/>2FA 登录成功</td>
<td align="center"><img src="docs/assets/otp-setup.png" width="480" alt="OTP 设置（QR 码）"><br/>OTP 设置（QR 码）</td>
</tr>
<tr>
<td align="center"><img src="docs/assets/settings-menu.png" width="480" alt="设置菜单（含认证设置入口）"><br/>设置菜单（含"认证设置"入口）</td>
<td align="center"><img src="docs/assets/settings-auth.png" width="480" alt="认证设置面板"><br/>认证设置面板</td>
</tr>
</table>

## 快速开始

1. 启动 `dsh web`：首次部署自动生成**初始密码**并打印在控制台（醒目提示块）；请复制备用；
2. 打开 Web UI，用初始密码登录——将进入**引导页**：设置你自己的访问密码（至少 8 位，包含大小写字母或特殊字符；**强制**，设置完成前所有功能不可用）；初始密码为一次性凭据，设置后自动失效；
3. 登录后可访问 `/otp/setup` 启用 TOTP（扫码或手动输入密钥，输入验证码确认；同时生成备份代码请妥善保存）；
4. 已启用 OTP 后，登录需密码 + 验证码（或备份代码）；
5. 修改密码：访问 `/login`（已登录时显示改密表单），或经"认证设置"面板。

## 配置

以下字段为 bundle patch / profile patch 中 `dsh-auth-gateway` 行的 `config`（Standard Schema 校验）：

| 字段 | 默认 | 含义 |
|---|---|---|
| `listenHost` / `listenPort` | `0.0.0.0` / `3080` | 网关对外监听地址与端口 |
| `upstreamHost` / `upstreamPort` | `127.0.0.1` / `3081` | 内部 webserver 地址与端口 |
| `basePath` | `/` | 反向代理子路径前缀（如 `/dsh`）；**默认 `/`（根路径）**。字符集限 `A-Za-z0-9._~/-`，拒绝 `..`、`//`、引号、空白、尖括号（该值会内嵌进页面脚本与链接，故按白名单校验；不合规配置会在加载时被拒绝）。子路径部署时在**部署方 profile patch** 中配置，不随插件分发 |
| `cookieSecure` | `auto` | 会话 Cookie 是否携带 `Secure` 属性。`auto`（默认，向后兼容）：请求经 TLS（反向代理终结并透传 `X-Forwarded-Proto: https`，或网关自身建立 TLS）时自动附加——前置 HTTPS 后无需改配置即启用；`true`：强制附加（反代已终结 TLS 但未透传协议头的部署）；`false`：显式关闭（仅建议可信内网）。注意：Secure 只在 HTTPS 链路生效——纯 HTTP 下强制开启会使浏览器拒绝保存 Cookie、登录立即失效（显式失败，而非静默降级）。**面板可直接修改**（「认证设置 → Cookie 安全 → 保存/恢复为部署配置」）：面板覆盖持久化于凭据记录（记录写接口实测于 dsh 0.1.5-rc.2+；更早版本缺失时面板自动降级为只读），优先于部署配置，直至恢复
| `minPasswordLength` | `8` | 密码最小长度（4–128） |
| `requireMixedCase` / `requireSpecial` | `true` / `true` | 密码复杂度：大小写混合或特殊字符二选一满足 |
| `maxLoginFailures` / `lockMinutes` | `5` / `5` | 密码失败锁定阈值与时长 |
| `maxGlobalAuthAttemptsPerMinute` | `60` | 全局登录尝试速率上限 |
| `maxOtpAttemptsPerMinute` | `10` | 单来源 OTP/备份码验证速率上限 |
| `otpEnabled`（已废弃） | `false` | 不再作为启用开关——2FA 由用户登录后在「认证设置」中绑定激活；字段保留仅为兼容旧配置 |
| `otpRequired` | `false` | 2FA 激活后强制每次登录验证（无需任何配置） |
| `otpIssuer` / `otpPeriod` / `otpDigits` / `otpWindow` | `dsh-auth-gateway` / `30` / `6` / `1` | TOTP 参数（显示名、周期、位数、窗口） |
| `backupCodeCount` / `backupCodeLength` | `10` / `8` | 备份代码数量与长度 |
| `updateCheck` | `false` | 打开「认证设置 → 关于」时**自动**检查新版本。默认关闭：全新安装不发起任何对外请求。置 `true` 后在面板打开时自动查询一次公共 npm registry 的 `latest`（本插件唯一的对外请求，成功缓存 6h / 失败 15min，超时 3s，不含任何凭据）。**无论此项如何**，面板上的「检查更新」按钮都可手动发起一次检查 |

## 安全模型

认证状态变更（启用/禁用 OTP、修改密码）均要求完整验证：2FA 激活时禁用 OTP 需当前密码 + 验证码或备份代码；未完成 2FA 的会话不能访问敏感端点。OTP 验证防重放（记录已接受时间步）、防伪造（`x-forwarded-for` 不计入来源）。**OTP 密钥在落盘前以 AES-256-GCM 密封**，读取需主密钥——默认自动生成 `auth-gateway/otp-master.key`（0600），也可经环境变量 `DSH_AUTH_GATEWAY_MASTER_KEY`（hex/base64，32 字节）注入以隔离磁盘泄露。登录审计只记录事件种类、来源 IP 与失败原因，不落任何凭据。完整威胁模型、已知限制与恢复路径见 [docs/zh/SECURITY.md](docs/zh/SECURITY.md)。

## 文档

| 文档 | 内容 |
|---|---|
| [docs/zh/INSTALL.md](docs/zh/INSTALL.md)（[English](docs/en/INSTALL.md)） | 安装、更新、卸载、凭据重置的完整操作步骤 |
| [docs/zh/NGINX-DEPLOYMENT.md](docs/zh/NGINX-DEPLOYMENT.md)（[English](docs/en/NGINX-DEPLOYMENT.md)） | 配合 nginx 部署：裸金属直连 / 子域名 / 子路径 / Docker nginx 容器四种拓扑与配置示例 |
| [docs/zh/SECURITY.md](docs/zh/SECURITY.md)（[English](docs/en/SECURITY.md)） | 威胁模型、OTP 安全设计、已知限制与恢复路径 |
| [docs/zh/DEPLOYMENT.md](docs/zh/DEPLOYMENT.md)（[English](docs/en/DEPLOYMENT.md)） | 端口与监听、LAN 部署、HTTPS 建议、nginx 反向代理、故障排查 |
| [docs/zh/TROUBLESHOOTING.md](docs/zh/TROUBLESHOOTING.md)（[English](docs/en/TROUBLESHOOTING.md)） | 实机故障案例：域名下模型页不可用、原生依赖构建被拦、bundle 加载失败、版本线凭据格式、跨境超时优化 |
| [docs/zh/TESTING.md](docs/zh/TESTING.md)（[English](docs/en/TESTING.md)） | 单元测试、端到端（Playwright）、API/WebSocket 门禁验证 |
| [docs/DEVELOPMENT.md](docs/zh/DEVELOPMENT.md)（[English](docs/en/DEVELOPMENT.md)） | 架构说明、构建、开发统计 |

## 致谢

- **@adra2n** — 实现 OTP 双因素认证（[PR #1](https://github.com/xbzbing/dsh-auth-gateway/pull/1)），并添加 OTP 密钥 AES-256-GCM 静态加密存储与解密路径错误分类（[PR #6](https://github.com/xbzbing/dsh-auth-gateway/pull/6)）；
- **@meowtech** — 报告并初步实现了 dsh 新版本（rc8+ 配置平面收归 loopback）下 LAN 浏览器设置不可用问题的修复（[PR #7](https://github.com/xbzbing/dsh-auth-gateway/pull/7)）；该实现（loader 包装 + provide 劫持）随后被证实会破坏共存插件，本仓库已改用最小介入方案重写。
- **@LuckVd** — 修复 dsh ≥ 0.1.2 上游浏览器认证（BrowserAuth）导致的转发 401（[PR #12](https://github.com/xbzbing/dsh-auth-gateway/pull/12)）：经官方 `credentials` 通道读取密钥并为回环一跳铸造同构 cookie，经评审补齐官方通道读取、轮换即时重铸、部署清单与双语文档后合入。

## 验证概览

- 单元与契约测试：`npm test`（覆盖 basePath 路由/重定向/转发、PWA 元数据放行、登录审计、审计日志轮转/清理、OTP 安全回归、client 契约、patch 端口推导）
- 部署流水线：`npm run deploy`（语法检查 → 全量测试 → 同步到 DSH 安装目录 → 安装后验证）
- 实机端到端：`node scripts/e2e.mjs`（Playwright，登录/2FA/改密全流程）
- 门禁验证：`./scripts/verify.sh`（curl，401/302/WS 拒绝/锁定）

详见 [docs/zh/TESTING.md](docs/zh/TESTING.md)。

## Model Experience

None，本包是浏览器与内部 dsh webserver 之间的认证载体，不会进入任何模型请求。

#### KV Cache effect

None；本包既不组装也不发送 provider 请求。

## License

MIT
