DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Webui Oauth — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-webui-oauth

Webui Oauth

DeepSeek Harness 的 WebUI 身份验证插件:在 HTTP/传输层强制执行密码或 OIDC/SSO 登录,支持服务端会话、HttpOnly Cookie 和审计日志。零依赖。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-webui-oauth@0.6.6
README兼容性版本

兼容性与来源证明

Webui Oauth 以 dsh-webui-oauth 发布,当前版本为 0.6.6。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
npm
Registry 更新时间
2026/10/16

版本

0.6.6stable
2026/9/17
0.6.5stable
2026/9/17
0.6.4stable
2026/9/17
查看其余 7 个版本收起版本
0.6.3stable
2026/9/17
0.6.2stable
2026/9/17
0.6.1stable
2026/9/17
0.6.0stable
2026/9/17
0.5.1stable
2026/9/17
0.5.0stable
2026/9/17
0.4.3stable
2026/9/16

相关插件

正在加载相关插件…

最新版
0.6.6
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
265.7 kB
文件数
8
Surface
web
许可证
MIT
发布源
npm
周下载
19
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 security-access 分类下经过校验的插件。

Cc Safety Netcc-safety-net编码代理 CLI 钩子——阻止破坏性命令和访问机密文件Doctor@linxin666/dsh-doctorDSH 配置档案的事务性救援模式,配备受监督的启动器、隔离的恢复容器、确定性修复、健康监控以及本地 Web 恢复控制台Mobiledsh-mobileDeepSeek Harness 移动端适配与安全访问插件,支持局域网、远程连接、Android App 和手机浏览器。Codex Subscriptiondsh-codex-subscription通过 OAuth 在 DeepSeek Harness 中使用 ChatGPT 和 Codex 订阅,支持配额管理、安全重置、网页搜索、图像和快速模式

README

dsh-webui-oauth

English | 中文

DSH WebUI 身份认证插件(持久化插件)。在「设置 → 身份认证」或首次访问登录页创建账号密码后,未认证的浏览器无法加载 WebUI 的任何资源、调用任何接口或建立任何实时连接——认证在 HTTP/传输层强制执行,不可通过浏览器开发者工具绕过。支持本地账号密码与 OIDC/SSO 单点登录两种方式。

本插件是 Yuuz12/dsh-webui-auth 的增强替代版(新增 OIDC SSO 等)。安装本插件后必须从 profile 的 bundle 列表里移除原版(见「与原版的关系」)——两者同时启用会抢占同一批路由,实测无法共存;本插件检测到这种冲突会直接拒绝启动(dsh 以退出码 1 结束并打印修复指令),而不是降级运行。凭据与数据目录沿用原版位置,原有账号与登录会话继续有效。

与原版的关系(必须二选一,不能同时启用)

原版 dsh-webui-auth 与本插件都用 ctx.webServer.register() 抢占同一批路由(前缀 ""、/api、/plugins 与 /api/remote.mux 升级路由)。webServer 的路由表是先到先得:对同一 (kind, path) 重复注册会直接抛 duplicate ... route,而且路由条目里不含属主信息(只存 {kind, path, handler}),事后无法判断某条路由是谁注册的。

因此运行时接管在架构上不可行——本插件曾经尝试过两种方式,都被隔离实例证伪:

尝试的做法实测结果
运行时 dispose 原版 fiber原版 apply 撞 INACTIVE_EFFECT,整个 dsh 进程启动失败
运行时 disable 原版条目原版路由已注册,本插件注册时撞 duplicate prefix route,进程启动失败
什么都不做、两套共存进程能起,但只有原版生效,本插件端点全部 404(静默失效,最难排查)
配置层移除原版(推荐)正常启动,本插件完整生效
profile 里只装本插件(推荐)正常启动,本插件完整生效

前两种与加载顺序无关(两种顺序都复现):谁先注册谁赢,晚到者抛异常并被启动审计判为失败。

正确做法:安装本插件后,把原版从 profile 的依赖与 bundle 列表里去掉。编辑 $DSH_HOME/profiles/<名字>/package.json:

{
  "dependencies": {
    // 删掉这一行:
    // "dsh-webui-auth": "^0.3.5",
    "dsh-webui-oauth": "^0.4.4"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "@deepseek-ai/dsh-web-app",
        // 删掉这一行:
        // "dsh-webui-auth",
        "dsh-webui-oauth"
      ]
    }
  }
}

不想动依赖、只想临时屏蔽,也可以在 profile 的 cordis.patch.yml 里按 id 禁用原版条目:

- id: dsh-webui-auth
  disabled: true

双装会直接启动失败(fail-fast):若检测到原版仍处于启用状态,本插件在 apply 一开始就抛错拒绝加载,dsh 进程随即以退出码 1 结束,并在 stderr 打印一行可读的修复指令:

[dsh-webui-oauth] 检测到原版 dsh-webui-auth 同时启用,拒绝启动。两者抢占同一批路由(webServer 路由表先到先得),
实测无法共存:本插件的登录页与闸门会完全失效(端点 404),或直接导致启动失败。
修复:从 profile 的 dsh.profile.bundles 与 dependencies 中移除 dsh-webui-auth,
或在其补丁层写 disabled: true,然后重启。

为什么是拒绝启动而不是降级运行:实测已证明双装没有一种可用形态(见上表后两行)。"进程能起但闸门归属不确定"是最危险的形态——运维以为已经上锁,实际本插件的端点全是 404。与其带着半残状态对外服务,不如把问题挡在启动阶段,并给出明确修复指令。

(cordis 默认 logger 只写内存缓冲区、不落 stdout/stderr,所以这里额外直写 stderr 兜底,保证 docker logs / journalctl 一定看得到这条唯一线索。)

  • 只按插件名匹配,不会影响任何其他插件;未装原版时完全安静。
  • 绝不改动别人的生命周期:本插件不 dispose、不 disable 任何兄弟插件的条目或 fiber。
  • 数据无缝衔接:数据目录(.dsh-webui-auth/)与凭据文件(dsh-webui-auth.json)刻意沿用原名,原版创建的账号无需迁移即可登录。
  • 接管原版数据目录:当本插件自身尚无凭据时,会依次查找含凭据的原版数据目录并直接复用(不是复制——复制会产生两份凭据,改密码只改一边而分叉):
    1. 同级目录 ../dsh-webui-auth/(源码 / link: 安装最常见);
    2. $DSH_HOME/dsh-webui-auth/(原版的兜底位置)。 两处都没有则用自己的目录(全新安装,走 setup token)。自身已有凭据时一律不接管,避免覆盖你已在新插件里建立的数据。
  • OIDC 配置独立存放:账号凭据留在 dsh-webui-auth.json,OIDC 配置(含 clientSecret)写入本插件自己的 dsh-webui-oauth.json;旧版曾把 oidc 段写在凭据文件里,读取时自动回落、保存时自动迁移。
  • 卸载原版、只留本插件同样正常工作。

架构

认证由四层组成,全部通过运行时包装 webServer 路由实现,不改动任何 DSH 核心包源码:

层机制未认证行为
WebUI 资源(index.html、/assets/*、SPA 路由)插件注册 prefix '' 兜底路由,校验会话后转交 frontend-static302 → 登录页
插件 bundle(/plugins/*)运行时包装 /plugins 前缀路由 handler401
/api RPC 接口运行时包装 /api 前缀路由 handler401
WebSocket(/api/remote.mux;旧核心 /api/events.mux、/api/events.host)运行时包装 upgrade 路由 handler401 拒绝升级
  • 不修改核心包:dsh 升级不会覆盖补丁、不会产生「升级后 /api 裸奔」的窗口。插件每次启动对路由表重做包装,并用 2s→10s 重扫捕获晚注册路由。v0.1.2-alpha.2 及更新核心的事件流 WebSocket 位于 /api/remote.mux(由 dsh-api-gateway 注册),候选列表自动适配,不再误报「upgrade 路由缺失」。

  • fail-closed:若预期路由缺失(dsh 内部结构变化导致包装不上),setup/configure 会拒绝启用认证,并在宿主日志与设置页同时报错——宁可不可用,不可「开了登录却裸奔 /api」。

  • 与核心自带浏览器认证(v0.1.2-alpha.2+)协作:该版本核心自带 launch-token 交换的签名 Cookie(dsh-auth-*)认证 / 与 /api。插件登录成功后自动把浏览器引导到核心的带 token 根 URL 完成核心 Cookie 交换;/api 请求在插件会话校验后原样转交给核心(不再改写 Host/Origin,避免破坏核心 Cookie 与 Host 绑定)。两者叠加:浏览器须同时持有插件会话 Cookie 与核心 Cookie。

  • 反代/局域网旧核心下的特权方法(≤alpha.1):已认证请求由插件在会话校验后以「回环形状」转交核心,使核心中回环钉死的特权方法(settings/credentials/agentPreset/llm.discoverModels)在反代部署下可用——会话 Cookie 闸门是比 Host 启发式更强的身份证明。

  • WebSocket 与 trustedHosts:WS 升级握手仍受核心自身 requestRejection / isTrustedApiRequest 限制,因此反代/局域网(非回环 Host)部署下,WS 下行需要同时在 dsh 配置中把对外域名加入 client-connection.trustedHosts,否则即使已登录也会被拒绝升级。

  • 跳转目标:动态 vs 绝对(统一开关,0.4.1):有两处跳转会用到请求方(浏览器)提供的 origin——① OIDC 回调 base(登录页上报 location.origin);② 登录成功后交给核心的 token 跳转(authority 取自请求 Host)。两处共用同一个开关,配置在 dsh settings 的 remote-web-ui 段(部署级):

    remote-web-ui:
      publicBaseUrl: 'https://dsh.example.com'   # 绝对跳转的目标(可选)
      trustBrowserOrigin: true                   # 省略即按 true:动态跳转
    
    • trustBrowserOrigin: true(默认)= 动态跳转:跳转目标随本次请求的实际地址走(反代透传的 Host,或反代下发的 X-Forwarded-Host)。适配「同一份配置服务多个入口」。
    • trustBrowserOrigin: false = 绝对跳转:一律用 publicBaseUrl,忽略请求里的地址。
    • 旧配置 oidc.trustBrowserOrigin 仍被接受:settings 段未显式配置时回退它,便于已有部署平滑升级。

    判定顺序(重要:回环兜底会绕过开关):

    序条件结果
    1反代下发了原始主机头(X-Forwarded-Host / Forwarded: host=)按开关:开 → 用该头;关 → 用 publicBaseUrl

会话为服务端会话,持久化到磁盘(sessions.jsonl,重启 DSH 不掉线,到期自动失效),由 HttpOnly; SameSite=Lax Cookie(dsh_wua_session)携带,JS 无法读取;修改密码会吊销所有其他会话。

安装

本插件是标准组合包(bundle),推荐用 DSH 官方 plugin 命令安装;手动方式保留作备用。前提:机器上有 pnpm(Node 自带 corepack,执行 corepack enable pnpm 即可启用)。

方式一:GitHub 安装(推荐)

npx @deepseek-ai/dsh plugin --profile web add github:WU-HINS/dsh-webui-oauth

拉取仓库源码(纯 JS 包,直接可用,无需构建步骤)。

方式二:npm 安装

需要该包已发布到 npm registry。若 dsh-webui-oauth 尚未发布,请使用方式一。

npx @deepseek-ai/dsh plugin --profile web add dsh-webui-oauth

从 npm registry 拉取,加入依赖并追加到 dsh.profile.bundles 列表,插件行随组合包层自动插入。

若你此前装的是原版 github:Yuuz12/dsh-webui-auth,可以直接改装本仓库并沿用原数据目录,账号无需重建——但请务必把原版从依赖与 dsh.profile.bundles 中移除(见「与原版的关系」),否则两者抢占同一批路由,本插件无法生效。

方式三:手动(备用)

  1. 将 dsh-webui-oauth 目录放入 profiles/web/node_modules/
  2. 在 profiles/web/cordis.patch.yml 的 insert 列表中加一行:
    - id: dsh-webui-oauth
      name: 'dsh-webui-oauth'

维护者开发模式:在本地源码目录使用 dsh plugin --profile web add ./dsh-webui-oauth(link: 安装),改代码 → 重启 DSH 即生效,无需重新安装。

所有方式通用

安装后无需任何核心包补丁(无 [dsh-webui-auth patch] 标记、不改 node_modules),重启 DSH 即生效。插件启动时在宿主日志打印 [dsh-webui-oauth] started, credentials file: ...;若路由包装不完整会打印 ROUTE GATE INCOMPLETE,此时认证无法启用(fail-closed)。

卸载

方式一:dsh plugin 命令(对应方式一安装)

  1. npx @deepseek-ai/dsh plugin --profile web remove dsh-webui-oauth(同时移除依赖与组合包层)
  2. 重启 DSH

方式二:手动(对应方式二安装)

  1. 删除插件目录 profiles/web/node_modules/dsh-webui-oauth/(0.3.1 起运行数据在包外,删它不影响账号;如需连账号一起清除,另删数据目录 .dsh-webui-auth/,见「数据文件位置」)
  2. 从 profiles/web/cordis.patch.yml 移除挂载行:
    - id: dsh-webui-oauth
      name: 'dsh-webui-oauth'

此步必须做,否则重启时加载器找不到插件包会报错 3. 重启 DSH

两种方式重启后认证门禁完全关闭(无需恢复任何核心包源码——插件从未修改核心文件)。如需清除持久化会话,删除数据目录中的 sessions.jsonl 即可;如曾用旧版插件,可清除浏览器 localStorage 中的 dsh-webui-auth.session 残留(无害)。

使用

  • 首次启用(需 setup token):未配置凭据时认证自动关闭(所有请求放行),但创建管理员账号需要本次启动生成的 setup token——打开 WebUI → 设置 → 身份认证,或访问 /dsh-webui-oauth/login,输入启动日志中打印的 [dsh-webui-oauth] setup token (...)(或数据目录 setup-token 文件内容,0600)后创建账号密码(≥8 位,含大小写字母、数字、特殊符号)。token 每次启动重新生成、创建成功后即删除,防止「先暴露、后配置」窗口内被他人抢先注册。
  • 用户名规则:3-32 位字母、数字、下划线或连字符(新建/修改时强制;旧账号不受影响,仍可正常登录)。
  • 之后:未登录访问任意路径 → 跳转登录页;登录后按「会话有效期」免登录(浏览器会话 / 1 小时 / 12 小时(默认)/ 1 天 / 3 天),服务端按到期时间强制失效。会话持久化到磁盘,重启 DSH 后已登录设备无需重新登录(到期时间仍生效)。「浏览器会话」模式:活跃使用期间自动续期(30 分钟窗口),关闭浏览器即失效。
  • 修改 / 禁用 / 退出:设置 → 身份认证(均需当前密码);修改密码会吊销其他所有已登录会话。
  • 忘记密码:删除数据目录的 dsh-webui-auth.json 即可——后台每分钟自动检测,最多 1 分钟内认证自动关闭(无需重启),之后用新的 setup token 重新创建账号即可。
  • OIDC 单点登录(可选):设置 → 身份认证 → 勾选「启用 OIDC 单点登录」,填写 Issuer(IdP 地址,须 https)、Client ID、Client Secret(机密客户端必填)、Scope、Redirect Base(可选)。保存后登录页出现「SSO 单点登录」按钮:点按后跳转 IdP 授权,回调中插件验证 state/nonce/PKCE/签名后建立本地会话(用户名 = IdP sub)。redirect_uri 的 base:trustBrowserOrigin 默认开启时采信浏览器 location.origin(反代重写 Host 时正确);关闭时仅用 redirectBase;两者皆无则报错不跳转。反代重写 Host 场景请配置 redirectBase(部署者已知对外真实地址,是唯一不受反代影响的确定性答案)。登出为简单版:仅清本地会话,不调 IdP 单点登出。

数据文件位置(按安装方式区分)

凭据与安全数据存放在运行时数据目录,按安装方式自动选择:

  • npm / GitHub / tarball 安装:插件包体位于 node_modules 内,会随升级、重装、清理被整目录替换——数据因此存放在该 node_modules 的上级目录下的 .dsh-webui-auth/(通常即 profile 根,如 ~/.dsh/profiles/web/.dsh-webui-auth/)。升级插件、pnpm clean、重装 DSH 都不会丢失账号密码与登录会话。
  • 本地 link / 源码安装:为插件源码目录(随仓库管理、已被 .gitignore 排除出 git;删除整个源码目录才会连数据一起清除)。
  • 兜底:以上位置均不可写时回退到 $DSH_HOME/dsh-webui-auth/(默认 ~/.dsh/dsh-webui-auth/)。

从 0.3.x 升级:运行数据不自动迁移。若旧位置(包内目录或 ~/.dsh/dsh-webui-auth/)的数据还在,可把下表中的 dsh-webui-auth.json、sessions.jsonl、audit-hmac-key、audit.jsonl 手动拷入新数据目录(npm / GitHub / tarball 安装即 node_modules 上级的 .dsh-webui-auth/);否则按「忘记密码」流程用新 setup token 重建账号即可。

目录内文件:

文件用途权限
dsh-webui-auth.json账号凭据(scrypt 哈希,v3 格式;0.2.x 的 v2 凭据仍可正常登录校验)。文件名沿用原版,保证升级不丢账号—
dsh-webui-oauth.json本插件独有配置(OIDC 段:issuer / clientId / clientSecret / scope / redirectBase / trustBrowserOrigin)。独立成文件,避免 OIDC 密钥与账号凭据混在一起—
audit.jsonl审计日志(IP 已假名化,见「审计日志」节)—
sessions.jsonl持久化会话(重启恢复用)0600
audit-hmac-key审计 IP 假名化的 HMAC 密钥(首次自动生成)0600
setup-token首次初始化的 setup token(创建成功后删除)0600

数据目录由安装方式决定(npm / GitHub / tarball → node_modules 上级的 .dsh-webui-auth/;link / 源码 → 插件源码目录;兜底 $DSH_HOME/dsh-webui-auth/),忘记密码、审计、会话路径均指该目录。也可用环境变量 DSH_WEBUI_AUTH_DATA_DIR 显式指定数据目录(容器 / 只读包体等部署适用)。

从旧版升级(OIDC 配置布局变更):早期版本把 oidc 段写在 dsh-webui-auth.json 里,现在改由 dsh-webui-oauth.json 承载。读取时仍会回落到旧位置(无感升级),并在你下次于设置页保存时自动迁移到新文件、同时从凭据文件剥离旧字段。

审计日志

登录成功/失败/限流、初始化、修改凭据、禁用、退出等安全事件追加写入数据目录的 audit.jsonl(JSONL 格式,含时间、用户名、IP、UA、详情)。客户端 IP 以 HMAC-SHA256 假名化存储(形如 hmac:5151e752|203.0.113.0/24,附 /24(IPv4)或 /64(IPv6)网络前缀用于聚合分析),原始 IP 不落盘。两种查看方式:

  • CLI(推荐):运行 node index.js audit [--limit N](默认最近 20 条,从模块所在路径运行即可):
    node index.js audit --limit 50
    
  • 设置页:设置 → 身份认证 → 「最近登录记录」展示最近 8 条。

审计写入失败不影响认证主流程(仅记宿主日志)。

外观

登录页与「设置 → 身份认证」设置页都跟随 DSH 自带的外观设置(设置 → 通用 → 外观:浅色 / 深色 / 跟随系统),不提供独立的外观开关。设置页运行在 WebUI 内,直接消费 DSH 的主题 token,天然随明暗切换;登录页是独立页面,由服务端读取当前外观偏好(settings ui-theme.preference)注入页面,并复刻 DSH 的 boot 逻辑:跟随系统 时按 prefers-color-scheme 解析、系统明暗切换时实时变化。登录页响应带 cache-control: no-store,外观变更后刷新即可生效。

升级 DSH 后的操作流程

无需任何操作:插件不修改核心包,dsh 升级后启动时自动重做路由包装。v0.1.2-alpha.2 及更新核心:插件自动适配 /api/remote.mux 事件流路由,并与核心自带浏览器认证(launch-token ↔ 签名 Cookie)协作——登录插件后浏览器会被自动引导完成核心认证,随后 WebUI 正常使用。若包装不完整(dsh 内部结构变化),宿主日志输出 ROUTE GATE INCOMPLETE、设置页显示红色警告,且 setup/configure 拒绝启用认证(fail-closed)。

注意(v0.1.2-alpha.2+):核心自带浏览器认证要求浏览器先通过 launch-token 换取核心 Cookie(dsh web / DSH Desktop 打印的带 ?token= 的地址)。插件已自动处理该交换;如浏览器此前从未访问过该地址,请使用 DSH 启动时打印的完整 URL 打开一次(或从插件登录页登录,流程会自动完成交换)。

数据与安全

  • 密码以 scrypt(Node 内置内存硬 KDF,抗 GPU/ASIC 爆破,零依赖)哈希保存在数据目录的 dsh-webui-auth.json,明文不落盘。凭据格式 v3(与 v2 同为 scrypt 编码,仅版本标记与字段语义不同);0.2.x 的 v2 凭据仍可校验登录。0.1.x 的 SHA-256 凭据自 0.2.0 起不再可校验,需删除凭据文件后重新创建账号(见「忘记密码」)。
  • 登录失败限流:按客户端 IP 每分钟最多 5 次——单个攻击者无法锁死其他用户(操作者)。反代场景下客户端 IP 取自 CF-Connecting-IP / X-Forwarded-For 最左侧,且仅当对端 socket 是回环(本机 caddy/cloudflared)时才信任代理头,远程无法伪造;校验失败时还会空跑一次 scrypt,抹平「账号不存在=响应快」的用户名枚举时序差异。
  • 首次初始化需要每启动生成的 setup token(128-bit,打印到宿主日志并写入数据目录 setup-token,0600),防止「先暴露、后配置」被抢占。
  • 审计日志:audit.jsonl,客户端 IP 以 HMAC 假名化存储(见「审计日志」节)。
  • 会话持久化:sessions.jsonl(0600),重启恢复;写失败时认证不受影响,设置页提示重启后需重新登录。
  • 登录页与 API 响应均带安全头:严格 CSP、nosniff、DENY 防嵌框、no-referrer、noindex、no-store。
  • Cookie HttpOnly + SameSite=Lax:JS 不可读、跨站请求不携带。
  • 登录/初始化端点本身公开(认证的必然入口):/dsh-webui-oauth/login、/dsh-webui-oauth/setup(后者受 setup token 保护)。
  • OIDC 单点登录:authorization_code + PKCE + client_secret(机密客户端,不做纯 PKCE public client);state 防 CSRF、nonce 防重放、redirect_uri 精确匹配(base + 固定路径)、id_token 经 JWKS 验签并校验 iss/aud/azp/exp/iat/nbf/nonce、仅接受 HTTPS 端点;审计只记录经 sanitizeSub 过滤的 sub。OIDC 客户端配置(含 secret)存于数据目录的 dsh-webui-oauth.json,与账号凭据分离。

签名算法支持(id_token 验签)

算法族具体算法JWKS kty状态
RSARS256 / RS384 / RS512RSA✅
RSA-PSSPS256 / PS384 / PS512RSA✅
ECDSAES256 (P-256) / ES384 (P-384) / ES512 (P-521)EC✅
EdDSAEd25519OKP✅

EC 是完整可用的,不只是"写在文档里"。三个曲线均已用真实签名端到端验证:

  • 单元级(test/oidc-ec.test.mjs,16 项):各曲线用 Node 原生生成密钥并真实签名 → 验签通过;篡改 payload、异密钥伪造 → 拒绝;JWKS 按 kid 选中 EC key;JWKS 未声明 alg 时同样可用(部分 IdP 会省略该字段)。
  • 端到端:以自建 IdP 分别用 ES256 / ES384 / ES512 签发 id_token,走完整授权码 + PKCE 流程,三次均登录成功(审计 oidc_login_success)。

实现要点:ECDSA 的 JWT 签名是 JOSE 原始 R||S(ieee-p1363),而 Node 的 crypto.verify 默认按 DER 解析。若不显式传 dsaEncoding: 'ieee-p1363',ES384/ES512 会直接验签失败;ES256 因曲线较短,偶发的错误解析有时"看起来能过",极难排查。本插件对该路径有明确处理与回归测试兜底。

算法混淆防护(0.5.0 加固):

  1. 验签算法只由 token 头部决定,JWKS 的 alg 仅作一致性约束。早期实现写成 (jwk.alg || header.alg),而 IdP 几乎都会在 JWKS 写 alg——等于让 jwk.alg 覆盖 header.alg,header.alg 形同虚设。实测后果:用 P-256 密钥真实签名,只把头部改成 ES384/ES512,token 照样通过验证("自称 ES512、实为 P-256")。现改为以 header.alg 为唯一真源,jwk.alg 与之冲突即拒。
  2. 算法名走显式白名单(不再用 startsWith 前缀匹配)。此前 ES999/RS999 这类编造的名字能命中对应 kty 的 key——今天被上层白名单挡住,但等于把安全性押在"另一个函数恰好兜底"上。
  3. kid 命中后仍校验 kty 匹配,不匹配即拒且不退化为"随便挑一把"(指定了 kid 却无匹配直接返回 null → no-signing-key)。
  4. 校验 JWKS 的 use / key_ops:use=enc 或 key_ops=[encrypt] 的加密专用 key 不会被选中验签。
  5. 拒绝 alg=none 与 HS*(含"用公钥当 HMAC secret"的真实伪造尝试)。

上述每条都有回归测试;其中前两条的用例已确认能在修复前的版本上稳定失败(7 项 FAIL),不是"恒真断言"。

选型建议:IdP 默认多为 RS256;若你的 IdP 支持 EC,ES256 签名更短、验签更快,适合移动端与高并发场景。

全流程实测结果(隔离实例)

以下结果来自隔离 dsh 实例(独立 $DSH_HOME + 独立 profile,与生产镜像完全隔离),插件为本仓库版本,配合一个自建的简易 OIDC 平台(discovery / jwks / authorize / token,RS256 签名 + PKCE S256 校验)实测。

A. 本地账号流程

步骤请求实测结果
未认证访问 /GET302 → /dsh-webui-oauth/login(闸门生效)
未认证访问 /apiGET401
登录页GET /dsh-webui-oauth/login200(唯一放行的公开页)
错误 setup tokenPOST /setup{"ok":false,"error":"setup-token-required"}
弱密码POST /setup{"ok":false,"error":"weak-password","reason":"length"}
正确 setup tokenPOST /setup{"ok":true} + 下发 dsh_wua_session(HttpOnly; SameSite=Lax; Max-Age=43200)
重复 setupPOST /setup{"ok":false,"error":"already-configured"}
错误密码登录POST /login{"ok":false,"error":"invalid"}
正确密码登录POST /login{"ok":true} + 会话 Cookie

双 Cookie 交接(重要):拿到插件会话后访问 /,插件会 302 到核心的带 token 根 URL(/?token=…),完成核心自己的 dsh-auth-* Cookie 交换。实测:

  • 只带插件 Cookie 访问 /api → 401(核心仍未认领该浏览器);
  • 完成 token 交换后再访问 /api → 404(已通过认证,仅是该路径不存在)。

也就是说插件闸门与核心 BrowserAuth 是两道独立的门,缺一不可。这是设计行为,不是故障。

B. OIDC / SSO 流程

用一个自建虚拟 IdP(https://127.0.0.1:14443,自签 CA)完整跑通:

  1. GET /dsh-webui-oauth/oidc/login?base=<浏览器origin> → 302 到 IdP /authorize,参数含 code_challenge + code_challenge_method=S256,并下发 dsh_wua_oidc_state Cookie(HttpOnly,600s);
  2. IdP 校验 client_id / response_type / PKCE 后 302 回 redirect_uri?code=…&state=…;
  3. GET /dsh-webui-oauth/oidc/callback → 插件用 code_verifier 换 token、验签 id_token → 200 + 下发会话 Cookie、清除 state Cookie;
  4. 审计记录 oidc_login_success,身份为 IdP 的 sub(实测 lab-user-001)。

IdP 侧日志确认完整往返:discovery → jwks → authorize(issued code) → token(issued id_token)。

安全属性同时验证(均被正确拒绝):

攻击实测
state 不匹配{"ok":false,"error":"oidc-invalid-state"}
缺 state Cookie(登录 CSRF / 会话固定){"ok":false,"error":"oidc-invalid-state"}
授权码重放{"ok":false,"error":"oidc-invalid-state"}

C. 反代改写 Host 时的跳转(重点)

前提:未配置 remote-web-ui.publicBaseUrl,trustBrowserOrigin 为默认 true。假设浏览器实际访问 http://127.0.0.1:14080,反代把 Host 改写成回环 127.0.0.1:13081(Caddy 的默认行为)。

场景插件给出的跳转目标是否正确
1. 直连(Host=13081)http://127.0.0.1:13081/?token=…✅
2. Host 被改写 + 下发 X-Forwarded-Host=14080http://127.0.0.1:14080/?token=…✅
3. Host 被改写 + XFH=公网域名:8443 + X-Forwarded-Proto=httpshttps://dsh.example.com:8443/?token=…✅
4. Host 被改写 且不下发 XFHhttp://127.0.0.1:13081/?token=…❌ 指向内网端口

结论:Host 被反代改写且不下发原始主机头时,未配置 publicBaseUrl 无法得到正确跳转——服务端从请求里拿不到任何浏览器侧地址,这不是可以靠推断解决的问题(猜错会变成开放重定向)。此时浏览器会被送到 127.0.0.1:13081,若那是内网/未暴露端口,就表现为「登录成功但页面打不开」。

两种解法,任选其一:

  • 让反代下发原始主机头(推荐,配置最少):

    proxy_set_header X-Forwarded-Host $host;      # 或 $http_host(含端口)
    proxy_set_header X-Forwarded-Proto $scheme;
    

    Caddy 默认就会传 X-Forwarded-Host;若用 DSH_PRESERVE_HOST 类开关保留 Host,则场景直接退化为「直连」,同样正确。

  • 或在 settings.yaml 显式声明对外地址:

    remote-web-ui:
      publicBaseUrl: 'https://dsh.example.com:8443'
    

    注意 publicBaseUrl 仅在其 authority 与请求 Host 一致时才被采信(防开放重定向)。Host 已被改写为回环时二者不一致,因此该场景下 publicBaseUrl 也救不了——必须走上面第一条。这是实测踩到的细节,容易误配。

    另需 --trusted-host <对外域名:端口>,否则桌面浏览器直连 /api 会 403(见「已知边界」)。

D. OIDC 授权跳转三路实测(0.6.6 全量验证)

用真实 IdP(HTTPS + 自签 CA)、真实反向代理(两种 Host 改写模式)与真实 dsh 实例, 端到端跑完「发起授权 → IdP → 回调 → 换 token → 建会话 → 跳回应用」,共 56 项断言全通过。 下表是三条路径下 redirect_uri 与「回调后回跳目标」的实测值:

路径关键配置IdP 收到的 redirect_uri回调后回跳目标
固定路径跳转redirectBase=http://127.0.0.1:14080http://127.0.0.1:14080/dsh-webui-oauth/oidc/callbackhttp://127.0.0.1:14080/?…&oidcbound=1 ✅
固定路径 + 浏览器谎报异源同上,且 base=https://evil.example.com仍为配置的 14080 ✅(不被带偏)同上 ✅
反代(Host 改写 + XFH)同上,经 14080配置的 14080 ✅指向 14080(浏览器侧)✅
反代(保留原始 Host)同上,经 14081配置的 14080 ✅14080,不指向内网 13081 ✅
浏览器动态跳转redirectBase 留空 + trustBrowserOrigin=true浏览器上报的 origin ✅回到该 origin ✅
动态 + 直通反代同上,经 14081http://127.0.0.1:14081/dsh-webui-oauth/oidc/callback ✅http://127.0.0.1:14081/?…(会话不丢)✅
动态但开关关闭trustBrowserOrigin=false 且无 redirectBase不发起授权,返回 oidc-base-unresolvable ✅—

被明确拒绝的非法输入(均不发起授权、不产生注入): javascript:alert(1)、带 path / query / hash / userinfo 的 origin、 以及含 CRLF 的注入尝试(https://evil.test/\r\nX-Injected: 1)。

关于绑定路径不下发会话 Cookie:绑定结束时只清一次性 state Cookie。 这是刻意的——绑定发生在管理员已登录之后,复用同一会话即可;重新下发等于无谓轮换凭据。 登录路径相反,必须同时下发新会话(响应含 2 条 Set-Cookie)。此差异已在测试中锁定。

复现方式见 test/oidc-redirect-matrix.test.mjs(无需外部依赖的单元回归,已接入 npm test)。

已知边界

  • 运行时包装的固有窗口:路由对象被替换(服务热重载)到下一次重扫之间(≤10s)存在未保护窗口;启用认证时的 fail-closed 已挡住「初始裸奔」,此窗口仅影响运行中的热重载场景。
  • WS 与 trustedHosts:反代/局域网(非回环 Host)下,WS 下行需在 dsh 配置 client-connection.trustedHosts 中加入对外域名(见「架构」节)。
  • 反代不同机:若反代与 DSH 不在同一台机器(对端非回环),代理头不被信任,限流将按代理 IP 聚合(退化为全局桶)。
  • HTTPS 反代且未下发协议头:登录后跳转的 scheme 依赖 ①remote-web-ui.publicBaseUrl 或 ②反代下发的 X-Forwarded-Proto/Forwarded。两者都没有时只能兜底 http(此时 https 端口会握手失败、页面「点了没反应」)。请二选一:在 settings.yaml 声明对外地址,或让反代 proxy_set_header X-Forwarded-Proto $scheme;。注意 publicBaseUrl 仅在与请求 Host 一致时生效,用来避免把局域网直连改写到公网。
  • 反代改写 Host 且不下发原始主机头:此时服务端拿不到浏览器侧地址,未配置 publicBaseUrl 得不到正确跳转(会指向内网 upstream 端口);而 publicBaseUrl 因 authority 不匹配同样不生效。唯一可行的解法是让反代下发 X-Forwarded-Host(Caddy 默认下发)或保留原始 Host。详见「全流程实测结果」C 节的实测总表。
  • --trusted-host 不能省:桌面浏览器用密码登录后直连 /api 的请求依赖 --trusted-host <对外域名:端口>;而 remote-web-ui 的配对流(/remote 通道)不需要它。删掉该参数会导致 /api 全 403。
  • JWKS 缓存与密钥轮换:discovery + JWKS 有 1 小时内存缓存。IdP 轮换签名密钥后,缓存里那把旧 key 会匹配不上,此时插件自动绕过缓存重取一次并记 oidc_jwks_refreshed 审计(0.5.1 起)。代价是每次密钥轮换会有一次额外的 JWKS 请求;若 IdP 在极短时间内反复轮换,可能表现为登录偶发变慢。
  • 审计假名化的边界:HMAC 密钥与审计日志同目录(0600),能读取密钥文件的本地攻击者可对 IP 空间暴力还原;假名化防的是「日志明文落盘」,不是防有文件权限的攻击者。
  • 会话存于数据目录 sessions.jsonl:重启后仍生效(到期时间不变);关闭/卸载插件不影响凭据。
  • 威胁模型为「浏览器/网络客户端」:能直接读写宿主进程内存或文件的本地进程不在防护范围内。
2
否则 Host 是回环/本机(127.0.0.1、::1、localhost、0.0.0.0)
恒用 publicBaseUrl(不听开关)
3否则(Host 已透传真实地址)按开关:开 → 用请求 Host;关 → 用 publicBaseUrl

第 2 行是动态跳转不可用时的兜底,不是第二个开关:Host 被改写成回环时,服务端从请求里拿不到任何浏览器侧地址,开关即使开着也没有可用值,只能回退 publicBaseUrl(该值来自 settings.yaml,运维可控、非攻击者可控,不构成开放重定向)。

端口规则:端口只来自被选中那一方的字面,不做任何补全。

  • 动态路径:端口取自 Host / 原始主机头。不带端口就不带(浏览器按 scheme 推 443)。
  • 绝对路径:端口取自 publicBaseUrl。CDN 仅在 443 对外时应写不带端口(https://dsh.example.com);写成 :8443 会让浏览器去撞 8443。
  • publicBaseUrl 里的端口只参与同源比对(见下条),不会被注入到动态路径的跳转目标里。

已知取舍:本机浏览器直连 http://127.0.0.1:3080 也落进第 2 行,于是开关开着也会跳 publicBaseUrl。要同时支持本机直连,只能让反代下发 X-Forwarded-Host(走第 1 行)。

  • 登录后跳转的协议自适应(0.3.3,修 #6 / #7):插件登录成功后要把浏览器引导到核心的带 token 根地址,其 authority 取自本次请求的 Host(不再写死 127.0.0.1),scheme 按请求实际协议解析,优先级:① 操作者显式声明 remote-web-ui.publicBaseUrl(仅当其 host/port 与本次请求 Host 一致时采信——否则局域网直连会被重定向到公网地址、跨源丢掉刚下发的会话 Cookie);② 标准代理头 X-Forwarded-Proto(取最左值)或 RFC 7239 Forwarded: proto=;③ socket 自身是 TLS(插件直接终结 TLS);④ 兜底 http。刻意不做「非 IP 域名即 https」的猜测——那会把纯 http 的内网域名访问(http://nas.local:3080)打成 https 死链。前端登录页另有一层单向兜底:页面在 https 下收到同源 http:// 跳转时自动升级为 https://(反向不降级、异源不改写)。

  • 可选 OIDC 单点登录(0.4.0;0.6.0 起需绑定身份):标准 OIDC 授权码 + PKCE(机密客户端,须 client_secret),以 Logto 为安全基准。配置 OIDC ≠ 谁都能登录:必须先绑定一个 IdP 身份,绑定前 OIDC 登录一律拒绝、登录页也不显示 SSO 按钮(见下条)。绑定后登录页出现「SSO 单点登录」按钮,认证通过后以 IdP 的 sub 建立本地会话(沿用会话有效期与持久化)。

  • OIDC 身份绑定 / 解绑(0.6.0):绑定 = 本地账号登录后在设置 → 身份认证 → OIDC 身份绑定点「绑定 OIDC 身份」,走一次 IdP 授权,校验通过的 sub 存入凭据文件(boundSub);此后只有该身份能通过 SSO 登录。解绑 = 点「解除绑定」(需当前密码),清空后 SSO 登录立即恢复为「一律拒绝」。设置页只显示掩码后的 sub(如 su*****ce)。旧凭据文件无 boundSub 字段时视为未绑定(升级不会放开访问)。redirect_uri 的 base 由 oidc.redirectBase 决定;默认交由浏览器处理(采信前端 location.origin,反代重写 Host 时浏览器看到的公网地址即正确),受上面统一开关约束;关闭时仅用配置的 redirectBase;两者都不可用则 fail-closed 报错。 id_token 签名支持 RSA(RS256/384/512、PS256/384/512)、EC(ES256/384/512)、EdDSA(Ed25519),配合 iss/aud/exp/iat/nbf/nonce 与 JWKS(kid) 校验(见下节「签名算法支持」)。登出为简单版:仅清本地会话。