dsh-webui-oauth 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +197 -0
- package/README.md +214 -0
- package/cordis.patch.yml +6 -0
- package/index.js +2627 -0
- package/lib/client.js +402 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuuz12
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# dsh-webui-oauth
|
|
2
|
+
|
|
3
|
+
English | [中文](README.md)
|
|
4
|
+
|
|
5
|
+
A persistent WebUI authentication plugin for DeepSeek Harness. Once you create an account/password in **Settings → 身份认证 (Authentication)** or via the first-run login page, **unauthenticated browsers cannot load any WebUI resource, call any API, or open any realtime connection** — authentication is enforced at the HTTP/transport layer and cannot be bypassed through browser devtools. Both local password accounts and **OIDC/SSO single sign-on** are supported.
|
|
6
|
+
|
|
7
|
+
> **This plugin is an enhanced replacement for [Yuuz12/dsh-webui-auth](https://github.com/Yuuz12/dsh-webui-auth)** (adding OIDC SSO and more). If both are installed, **this plugin disables the original at startup** (see "Relationship to the original"), so you never end up with two gates; credentials and the data directory keep the original location, so **existing accounts and sessions keep working**.
|
|
8
|
+
|
|
9
|
+
## Relationship to the original (auto-displacement)
|
|
10
|
+
|
|
11
|
+
The original `dsh-webui-auth` and this plugin both enforce authentication by wrapping `webServer` routes at runtime. If both gates stay alive, every request is checked twice against two independent session stores, and the two logins do not recognise each other (logging in through one gate still leaves the other returning 302/401) — the visible symptom is a login page that keeps redirecting.
|
|
12
|
+
|
|
13
|
+
So at startup (before installing its own gate) this plugin walks the cordis plugin registry, finds any loaded original by plugin name, and **disposes all of its fibers**. The original is itself reversible: disposing it undoes its route wrapping and endpoint registration, after which this plugin owns the whole gate. The host log shows:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
[dsh-webui-oauth] displaced original plugin(s): dsh-webui-auth — this plugin takes over the auth gate; credentials/data directory are shared.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Matching is by plugin name only** — no other plugin is touched; if the original is absent, startup continues silently.
|
|
20
|
+
- **Data carries over seamlessly**: the data directory (`.dsh-webui-auth/`) and credentials file (`dsh-webui-auth.json`) **deliberately keep their original names**, so accounts created by the original work without migration.
|
|
21
|
+
- **Adopting the original's data directory**: when this plugin has no credentials of its own yet, it looks for an original data directory that already holds credentials and **reuses it directly** (not copies — copying would produce two credential sets that diverge as soon as one side changes the password):
|
|
22
|
+
1. the sibling directory `../dsh-webui-auth/` (the usual case for source / `link:` installs);
|
|
23
|
+
2. `$DSH_HOME/dsh-webui-auth/` (the original's fallback location).
|
|
24
|
+
If neither exists it uses its own directory (fresh install, driven by the setup token). **When it already has credentials it never adopts**, so data created under the new plugin is never overwritten.
|
|
25
|
+
- **OIDC config lives in its own file**: account credentials stay in `dsh-webui-auth.json`, while the OIDC config (including `clientSecret`) goes to this plugin's own `dsh-webui-oauth.json`. Older versions kept the `oidc` section inside the credentials file — it is still read from there (transparent upgrade) and migrated on the next save.
|
|
26
|
+
- Uninstalling the original and keeping only this plugin works too.
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
Authentication is enforced in four layers, all implemented by **wrapping the webServer routes at runtime — no DSH core package source is modified**:
|
|
31
|
+
|
|
32
|
+
| Layer | Mechanism | Unauthenticated behavior |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| WebUI resources (index.html, /assets/*, SPA routes) | Plugin registers a `prefix ''` catch-all route; after session validation it hands off to frontend-static | 302 → login page |
|
|
35
|
+
| Plugin bundles (/plugins/*) | Wraps the `/plugins` prefix route handler at runtime | 401 |
|
|
36
|
+
| /api RPC surface | Wraps the `/api` prefix route handler at runtime | 401 |
|
|
37
|
+
| WebSocket (`/api/remote.mux`; legacy cores `/api/events.mux`, `/api/events.host`) | Wraps the upgrade route handlers at runtime | 401 upgrade rejected |
|
|
38
|
+
|
|
39
|
+
- **No core patching**: a DSH upgrade never overwrites patches and never leaves `/api` exposed after an upgrade. Every startup re-wraps the route tables, with a 2s→10s rescan loop that catches late-registered routes. **On v0.1.2-alpha.2+ the event-stream WebSocket lives at `/api/remote.mux` (registered by dsh-api-gateway); the candidate list adapts automatically, so the plugin no longer falsely reports "upgrade route missing".**
|
|
40
|
+
- **Fail-closed**: if the expected routes are missing (DSH internals changed so wrapping can't apply), `setup`/`configure` **refuse to enable authentication** and the problem is reported both in the host log and on the settings page — better unusable than "login enabled with an unprotected /api".
|
|
41
|
+
- **Cooperation with the core's own browser auth (v0.1.2-alpha.2+)**: that core version ships launch-token exchange with an origin-bound signed cookie (`dsh-auth-*`) guarding `/` and `/api`. After a successful plugin login the browser is automatically sent to the core's token-bearing root URL so the core cookie exchange runs too; `/api` requests pass through **unchanged** after the plugin session check (no more Host/Origin rewrite, which would break the core's cookie/Host binding). Both gates apply: a browser must hold the plugin session cookie **and** the core cookie.
|
|
42
|
+
- **Privileged methods behind a reverse proxy / LAN (legacy cores ≤ alpha.1)**: after the session check the plugin hands authenticated requests to the core in a "loopback shape", so the core's **loopback-pinned privileged methods** (settings/credentials/agentPreset/llm.discoverModels) work in proxied deployments — the session-cookie gate is a strictly stronger identity proof than the Host-header heuristic it replaces.
|
|
43
|
+
- **WebSocket and `trustedHosts`**: the WS upgrade handshake still goes through the core's own `requestRejection` / `isTrustedApiRequest`, so **in reverse-proxy / LAN deployments (non-loopback Host) you must also add the public hostname to `client-connection.trustedHosts` in the DSH config**, otherwise even authenticated upgrades are rejected.
|
|
44
|
+
- **Unified "browser-influenced origin" switch (0.4.1)**: two redirect sites trust an origin **supplied by the requester (the browser)** — ① the OIDC callback base (the login page reports `location.origin`); ② the post-login core token redirect (authority comes from the request `Host`). Both **share one switch**, configured in the dsh settings `remote-web-ui` section (deployment level):
|
|
45
|
+
```yaml
|
|
46
|
+
remote-web-ui:
|
|
47
|
+
publicBaseUrl: 'https://dsh.example.com:8443' # deterministic origin (optional)
|
|
48
|
+
trustBrowserOrigin: false # off: both sites use only the deterministic origin
|
|
49
|
+
- 'https://dsh.example.com:8443'
|
|
50
|
+
```
|
|
51
|
+
- `trustBrowserOrigin` defaults to `true` when omitted (legacy behavior); an explicit `false` makes both sites **ignore** the browser-influenced origin and use `publicBaseUrl`. With no `publicBaseUrl` either, the token redirect falls back to the request Host / loopback, and OIDC fails closed with an error.
|
|
52
|
+
- The old `oidc.trustBrowserOrigin` is still accepted: when the settings section does not set the switch explicitly, it is used as the fallback, so existing deployments migrate smoothly.
|
|
53
|
+
- **Host-rewriting reverse proxies**: when the proxy rewrites `Host` to a loopback address (`127.0.0.1:3080`, `::1`, `localhost`, `0.0.0.0`) and sends nothing else, the plugin **cannot** recover the browser-side address from the request, so it treats the address as proxy-rewritten and uses the configured `publicBaseUrl` directly (the legacy "declare the public address in settings.yaml" advice was ineffective in that shape, because `publicBaseUrl` was only trusted on an exact Host match). If the proxy can send the original host, `X-Forwarded-Host` (or RFC 7239 `Forwarded: host=`) is preferred over the request Host for building the redirect.
|
|
54
|
+
- **Optional OIDC SSO (0.4.0)**: standards-based authorization-code + PKCE with a **confidential client (client_secret required)**, security-benchmarked against Logto. Once configured, the login page shows an "SSO single sign-on" button; after IdP authentication the plugin creates a local session keyed by the IdP `sub` (reusing the session TTL and persistence). The **redirect_uri base is set by `oidc.redirectBase`**; by default the browser is trusted (it trusts the front-end `location.origin` — under a host-rewriting reverse proxy the browser's public address is correct), ****constrained by the unifed switch above****; when disabled only the configured `redirectBase` is used; if neither is available it fails closed with an error. Supports RSA/EC/EdDSA-signed id_token validation (JWKS + iss/aud/exp/iat/nbf/nonce checks) and simple local logout (clears the local session only, no IdP SLO).
|
|
55
|
+
- **Scheme-adaptive post-login redirect (0.3.3, fixes #6 / #7)**: after a successful login the plugin sends the browser to the core's token-bearing root URL. Its **authority now comes from the request Host** (no longer hardcoded to `127.0.0.1`) and its **scheme follows the protocol the request actually used**, resolved in this order: ① the operator's explicit `remote-web-ui.publicBaseUrl` (**trusted only when its host/port matches the incoming Host** — otherwise a LAN direct hit would be redirected to the public address and lose the freshly issued session cookie to a cross-origin hop); ② the standard proxies headers `X-Forwarded-Proto` (leftmost value) or RFC 7239 `Forwarded: proto=`; ③ a TLS-terminating socket (`socket.encrypted`); ④ fallback `http`. It deliberately does **not** guess "non-IP hostname ⇒ https", which would turn plain-http intranet access (`http://nas.local:3080`) into a dead https link. The login page adds a one-way client-side fallback: an https page receiving a **same-origin** `http://` redirect upgrades it to `https://` (never downgrades, never rewrites another origin).
|
|
56
|
+
|
|
57
|
+
Sessions are **server-side and persisted to disk** (`sessions.jsonl`, survive a DSH restart, expire server-side), carried by an `HttpOnly; SameSite=Lax` cookie (`dsh_wua_session`) that JS cannot read; changing the password **revokes every other session**.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
This plugin is a standard **bundle** — the official `dsh plugin` command is the recommended way to install it. The manual method is kept as a fallback. Prerequisite: pnpm on the machine (Node ships corepack — run `corepack enable pnpm` to activate it).
|
|
62
|
+
|
|
63
|
+
### Method 1: GitHub install (recommended)
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npx @deepseek-ai/dsh plugin --profile web add github:WU-HINS/dsh-webui-oauth
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Fetches the repository source (plain JS — works directly, no build step).
|
|
70
|
+
|
|
71
|
+
> If you previously installed the original `github:Yuuz12/dsh-webui-auth`, you can switch straight to this repository — the plugin displaces the original automatically and reuses the same data directory, so no account needs to be recreated.
|
|
72
|
+
|
|
73
|
+
### Method 2: npm install
|
|
74
|
+
|
|
75
|
+
> Requires the package to be published on the npm registry. If `dsh-webui-oauth` is not published yet, use Method 1.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-webui-oauth
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Pulls the package from the npm registry, adds the dependency and appends it to the `dsh.profile.bundles` list; the plugin row is inserted automatically via the bundle layer.
|
|
82
|
+
|
|
83
|
+
### Method 3: manual (fallback)
|
|
84
|
+
|
|
85
|
+
1. Put the `dsh-webui-auth` directory into `profiles/web/node_modules/`
|
|
86
|
+
2. Add one row to the `insert` list in `profiles/web/cordis.patch.yml`:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
- id: dsh-webui-oauth
|
|
90
|
+
name: 'dsh-webui-oauth'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> Maintainer dev mode: `dsh plugin --profile web add ./dsh-webui-oauth` from a local source checkout (`link:` install) — edit code, restart DSH, done; no reinstall needed.
|
|
94
|
+
|
|
95
|
+
### Common to all methods
|
|
96
|
+
|
|
97
|
+
**No core-package patches are needed** (no `[dsh-webui-auth patch]` markers, no `node_modules` edits) — just restart DSH. On startup the host log prints `[dsh-webui-oauth] started, credentials file: ...`; if the route wrapping is incomplete it prints `ROUTE GATE INCOMPLETE` and authentication cannot be enabled (fail-closed).
|
|
98
|
+
|
|
99
|
+
## Uninstallation
|
|
100
|
+
|
|
101
|
+
### Method 1: `dsh plugin` command (for method-1 installs)
|
|
102
|
+
|
|
103
|
+
1. `npx @deepseek-ai/dsh plugin --profile web remove dsh-webui-oauth` (removes both the dependency and the bundle layer)
|
|
104
|
+
2. Restart DSH
|
|
105
|
+
|
|
106
|
+
### Method 2: manual (for method-2 installs)
|
|
107
|
+
|
|
108
|
+
1. **Delete the plugin directory** `profiles/web/node_modules/dsh-webui-oauth/` (since 0.3.1 runtime data lives outside the package, deleting it does not touch the account; to wipe the account too, also delete the `.dsh-webui-auth/` data directory — see "Data file locations")
|
|
109
|
+
2. **Remove the mount row** from `profiles/web/cordis.patch.yml`:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
- id: dsh-webui-oauth
|
|
113
|
+
name: 'dsh-webui-oauth'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This step is required — otherwise the loader fails at startup because the package is missing.
|
|
117
|
+
3. **Restart DSH**
|
|
118
|
+
|
|
119
|
+
With either method, after the restart authentication is fully disabled (**no core sources to restore** — the plugin never modified core files). To also clear persisted sessions, delete `sessions.jsonl` in the data directory. If you previously used the older pre-hardening build, the leftover `dsh-webui-auth.session` key in browser localStorage is harmless and may be removed optionally.
|
|
120
|
+
|
|
121
|
+
## Usage
|
|
122
|
+
|
|
123
|
+
- **First enable (setup token required)**: while no credentials exist, authentication is off (all requests pass), but creating the administrator account requires a **per-boot setup token** — open WebUI → Settings → 身份认证 (Authentication), or visit `/dsh-webui-oauth/login`, enter the token printed in the startup log as `[dsh-webui-oauth] setup token (...)` (or read the `setup-token` file in the data directory, mode 0600), then create an account/password (≥8 characters, must include uppercase, lowercase, digit and special character). The token is regenerated on every boot and deleted once setup succeeds, preventing someone from claiming the administrator account in the "exposed before configured" window.
|
|
124
|
+
- **Username rule**: 3-32 characters of letters, digits, underscore or hyphen (enforced on create/change; legacy accounts are unaffected and can still log in).
|
|
125
|
+
- **Afterwards**: any unauthenticated visit to any path redirects to the login page; after login you stay signed in for the chosen **session lifetime** (browser session / 1 hour / 12 hours (default) / 1 day / 3 days), enforced server-side by expiry. **Sessions are persisted to disk — after a DSH restart logged-in devices stay signed in** (the expiry still applies). "Browser session" mode: the 30-minute window slides with activity, and closing the browser logs you out.
|
|
126
|
+
- **Change / disable / log out**: Settings → 身份认证 (all require the current password); changing the password revokes every other logged-in session.
|
|
127
|
+
- **Forgot password**: delete `dsh-webui-auth.json` in the data directory — a background check every minute disables authentication within at most 1 minute (no restart needed), then create a new account with a fresh setup token.
|
|
128
|
+
|
|
129
|
+
## Where data files live (depends on install mode)
|
|
130
|
+
|
|
131
|
+
Credentials and security data are stored in the **runtime data directory**, chosen automatically by install mode:
|
|
132
|
+
|
|
133
|
+
- **npm / GitHub / tarball installs**: the package lives inside `node_modules`, which is wholesale replaced on upgrade, reinstall or cleanup — so data is stored in a `.dsh-webui-auth/` directory **next to that `node_modules`** (usually the profile root, e.g. `~/.dsh/profiles/web/.dsh-webui-auth/`). Upgrading the plugin, `pnpm clean`, or reinstalling DSH no longer loses the account or login sessions.
|
|
134
|
+
- **Local link / source installs** (`dsh plugin add ./dsh-webui-oauth`): the plugin source directory itself (managed with the repo and excluded from git via `.gitignore`; deleting the whole source checkout is what deletes the data).
|
|
135
|
+
- **Fallback**: when none of the above is writable, `$DSH_HOME/dsh-webui-auth/` (default `~/.dsh/dsh-webui-auth/`) is used.
|
|
136
|
+
|
|
137
|
+
Upgrading from 0.3.x: runtime data is **not migrated automatically**. If the legacy data (inside the package directory or `~/.dsh/dsh-webui-auth/`) still exists, copy `dsh-webui-auth.json`, `sessions.jsonl`, `audit-hmac-key` and `audit.jsonl` from the table below into the new data directory manually (for npm / GitHub / tarball installs that is `.dsh-webui-auth/` next to `node_modules`); otherwise just recreate the account with a fresh setup token (see "Forgot password").
|
|
138
|
+
|
|
139
|
+
Files in the data directory:
|
|
140
|
+
|
|
141
|
+
| File | Purpose | Permissions |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `dsh-webui-auth.json` | **Account credentials** (scrypt hash, v3 format; 0.2.x v2 credentials still verify and log in). Keeps the original filename so upgrades never lose accounts | — |
|
|
144
|
+
| `dsh-webui-oauth.json` | **This plugin's own config** (the OIDC section: `issuer` / `clientId` / `clientSecret` / `scope` / `redirectBase` / `trustBrowserOrigin`). A separate file keeps OIDC secrets apart from account credentials | — |
|
|
145
|
+
| `audit.jsonl` | Audit log (IPs pseudonymized, see "Audit log") | — |
|
|
146
|
+
| `sessions.jsonl` | Persisted sessions (restart recovery) | 0600 |
|
|
147
|
+
| `audit-hmac-key` | HMAC key for audit-IP pseudonymization (auto-generated once) | 0600 |
|
|
148
|
+
| `setup-token` | First-run setup token (deleted after setup succeeds) | 0600 |
|
|
149
|
+
|
|
150
|
+
The data directory is chosen by install mode (npm / GitHub / tarball → `.dsh-webui-auth/` next to `node_modules`; link / source → the plugin source directory; fallback `$DSH_HOME/dsh-webui-auth/`); the "forgot password", audit and session paths above refer to that data directory. You can also pin it explicitly with the `DSH_WEBUI_AUTH_DATA_DIR` environment variable (useful for containers and read-only package trees).
|
|
151
|
+
|
|
152
|
+
> **Upgrading from an older version (OIDC layout change)**: early builds stored the `oidc` section inside `dsh-webui-auth.json`; it now lives in `dsh-webui-oauth.json`. Reads still fall back to the old location (transparent upgrade), and the next save from the settings page migrates it to the new file while stripping the stale field from the credentials file.
|
|
153
|
+
|
|
154
|
+
## Audit log
|
|
155
|
+
|
|
156
|
+
Security events — login success/failure/rate-limit, setup, configure, disable, logout — are **appended as JSONL to `audit.jsonl`** in the data directory (timestamp, username, IP, user-agent, detail). **Client IPs are pseudonymized with HMAC-SHA256** (e.g. `hmac:5151e752|203.0.113.0/24`, with the /24 (IPv4) or /64 (IPv6) network prefix kept in cleartext for aggregation); raw addresses are never written to disk. Two ways to view:
|
|
157
|
+
|
|
158
|
+
- **CLI** (recommended): run `node index.js audit [--limit N]` (last 20 entries by default; run from the module path):
|
|
159
|
+
```sh
|
|
160
|
+
node index.js audit --limit 50
|
|
161
|
+
```
|
|
162
|
+
- **Settings page**: Settings → 身份认证 → "最近登录记录" (Recent activity) shows the last 8 entries.
|
|
163
|
+
|
|
164
|
+
Audit write failures never block authentication (only a host-log warning).
|
|
165
|
+
|
|
166
|
+
## Appearance
|
|
167
|
+
|
|
168
|
+
Both the login page and the "Settings → 身份认证 (Authentication)" settings page follow DSH's **built-in appearance setting** (Settings → General → Appearance: Light / Dark / System); no separate appearance switch is provided. The settings page lives inside the WebUI and consumes DSH's theme tokens directly, so it tracks light/dark automatically. The login page is a standalone page: the server reads the current appearance preference (settings `ui-theme.preference`), injects it into the page, and the page mirrors DSH's boot logic — `System` resolves via `prefers-color-scheme` and reacts live to OS changes. The login response is served with `cache-control: no-store`, so a refresh picks up any appearance change immediately.
|
|
169
|
+
|
|
170
|
+
## What to do after upgrading DSH
|
|
171
|
+
|
|
172
|
+
**Nothing.** The plugin never modifies core packages — after a DSH upgrade the runtime route wrapping is re-applied automatically on startup. **On v0.1.2-alpha.2+** the plugin adapts to the `/api/remote.mux` event-stream route and cooperates with the core's built-in browser auth (launch-token ↔ signed cookie): after logging into the plugin, the browser is automatically guided through the core authentication, after which the WebUI works normally. If the wrapping is incomplete (DSH internals changed), the host log prints `ROUTE GATE INCOMPLETE`, the settings page shows a red warning, and `setup`/`configure` refuse to enable authentication (fail-closed).
|
|
173
|
+
|
|
174
|
+
> **Note (v0.1.2-alpha.2+)**: the core's browser auth requires the browser to first exchange the launch token for the core cookie (the `?token=` URL printed by `dsh web` / DSH Desktop). The plugin performs that exchange automatically; if a browser has never visited that address, open the full URL printed at DSH startup once (or log in via the plugin's login page — the exchange happens automatically).
|
|
175
|
+
|
|
176
|
+
## Data & Security
|
|
177
|
+
|
|
178
|
+
- Passwords are hashed with **scrypt** (Node's built-in memory-hard KDF — GPU/ASIC resistant, zero dependencies) and stored in `dsh-webui-auth.json` in the data directory (location depends on install mode, see above); plaintext is never written to disk. Credentials format is v3 (same scrypt encoding as v2 — only the version marker and field semantics changed); **0.2.x v2 credentials still verify**. **Since 0.2.0 only scrypt hashes are accepted**: 0.1.x SHA-256 credentials can no longer be verified — delete the credentials file and recreate the account (see "Forgot password").
|
|
179
|
+
- Login rate limiting: **per client IP**, at most 5 failures per minute — a single attacker can no longer lock out other users (or the operator). Behind a reverse proxy the client IP is taken from `CF-Connecting-IP` / the leftmost `X-Forwarded-For`, and the proxy headers are trusted **only when the socket peer is loopback** (local caddy/cloudflared) — remote callers cannot spoof them. Failed verifications also run a dummy scrypt pass so "unknown account" and "wrong password" take the same time, defeating username enumeration via response timing.
|
|
180
|
+
- First-run setup requires a **per-boot setup token** (128-bit, printed to the host log and written to `setup-token` in the data directory, mode 0600), preventing account claiming in the "exposed before configured" window.
|
|
181
|
+
- Audit log: `audit.jsonl`, client IPs pseudonymized with HMAC (see "Audit log").
|
|
182
|
+
- Persisted sessions: `sessions.jsonl` (0600), restored on restart; a write failure never affects authentication — the settings page just warns that a restart will require re-login.
|
|
183
|
+
- Security headers on the login page and API responses: strict CSP, `nosniff`, `DENY` framing, `no-referrer`, `noindex`, `no-store`.
|
|
184
|
+
- Cookie `HttpOnly + SameSite=Lax`: not readable by JS, not sent on cross-site requests.
|
|
185
|
+
- The login/setup endpoints are intentionally public (the entry point of authentication): `/dsh-webui-oauth/login` and `/dsh-webui-oauth/setup` (the latter protected by the setup token).
|
|
186
|
+
- **OIDC SSO**: authorization-code + PKCE + client_secret (confidential client, no public-client PKCE); state guards CSRF, nonce guards replay, redirect_uri is exact-matched (base + fixed path), id_token is JWKS-verified with iss/aud/exp/iat/nbf/nonce checks, and only HTTPS endpoints are accepted. Only the `sanitizeSub`-filtered `sub` is recorded in the audit log. The OIDC client config (including the secret) lives in `dsh-webui-oauth.json` in the data directory, kept apart from the account credentials.
|
|
187
|
+
|
|
188
|
+
## Known limits
|
|
189
|
+
|
|
190
|
+
- **Inherent runtime-wrapping window**: between a route-object replacement (service hot-reload) and the next rescan (≤10s) there is an unprotected window; the fail-closed check on enabling covers the "initially exposed" case, so this window only affects hot-reload during runtime.
|
|
191
|
+
- **WebSocket and `trustedHosts`**: in reverse-proxy / LAN deployments (non-loopback Host), WS downlinks need the public hostname added to `client-connection.trustedHosts` in the DSH config (see "Architecture").
|
|
192
|
+
- **Proxy on a different host**: if the reverse proxy is not on the same machine as DSH (non-loopback peer), the proxy headers are not trusted and rate limiting aggregates per proxy IP (degrades to a global bucket).
|
|
193
|
+
- **HTTPS proxy that sends no protocol header**: the post-login redirect scheme depends on ① `remote-web-ui.publicBaseUrl` or ② an `X-Forwarded-Proto` / `Forwarded` header from the proxy. With neither, it can only fall back to `http` (the browser then handshakes against the TLS port and the page looks frozen after clicking Login). Pick one: declare the public address in `settings.yaml`, or make the proxy send `proxy_set_header X-Forwarded-Proto $scheme;`. Note `publicBaseUrl` only applies when it matches the request Host, which is what keeps LAN direct access from being rewritten to the public address.
|
|
194
|
+
- **`--trusted-host` is not optional**: after desktop password login, requests that hit `/api` directly depend on `--trusted-host <public-host:port>`; the remote-web-ui pairing flow (`/remote` channel) does not. Dropping the flag makes every `/api` call return 403.
|
|
195
|
+
- **Limits of audit pseudonymization**: the HMAC key lives in the same data directory (0600); a local attacker who can read it can brute-force the IP space — pseudonymization protects against "plaintext IPs at rest", not against an attacker with file access.
|
|
196
|
+
- Sessions live in `sessions.jsonl`: they survive restarts (expiry unchanged); uninstalling/disabling the plugin does not affect credentials.
|
|
197
|
+
- Threat model is "browser/network clients": local processes that can read/write the host's memory or files are out of scope.
|
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# dsh-webui-oauth
|
|
2
|
+
|
|
3
|
+
[English](README.en.md) | 中文
|
|
4
|
+
|
|
5
|
+
DSH WebUI 身份认证插件(持久化插件)。在「设置 → 身份认证」或首次访问登录页创建账号密码后,**未认证的浏览器无法加载 WebUI 的任何资源、调用任何接口或建立任何实时连接**——认证在 HTTP/传输层强制执行,不可通过浏览器开发者工具绕过。支持本地账号密码与 **OIDC/SSO 单点登录**两种方式。
|
|
6
|
+
|
|
7
|
+
> **本插件是 [Yuuz12/dsh-webui-auth](https://github.com/Yuuz12/dsh-webui-auth) 的增强替代版**(新增 OIDC SSO 等)。若两者同时安装,**本插件启动时会主动停用原版**(见「与原版的关系」),不会出现双闸门;凭据与数据目录沿用原版位置,**原有账号与登录会话继续有效**。
|
|
8
|
+
|
|
9
|
+
## 与原版的关系(自动顶掉原版)
|
|
10
|
+
|
|
11
|
+
原版 `dsh-webui-auth` 与本插件都通过运行时包装 `webServer` 路由实现认证。若两套闸门同时存活,同一请求会被两套会话各校验一次,且登录态互不相认(在一个闸门登录后,另一个仍返回 302/401),表现为「登录后反复跳转」。
|
|
12
|
+
|
|
13
|
+
因此本插件在启动时(安装自己的闸门之前)会遍历 cordis 插件注册表,按插件名找到已加载的原版并 **dispose 掉它的全部 fiber**。原版自身是可逆设计,卸载后其路由包装与端点注册会被完整撤销,随后由本插件接管全部闸门。宿主日志会出现:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
[dsh-webui-oauth] displaced original plugin(s): dsh-webui-auth — this plugin takes over the auth gate; credentials/data directory are shared.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **只按插件名匹配**,不会影响任何其他插件;找不到原版时静默继续。
|
|
20
|
+
- **数据无缝衔接**:数据目录(`.dsh-webui-auth/`)与凭据文件(`dsh-webui-auth.json`)**刻意沿用原名**,原版创建的账号无需迁移即可登录。
|
|
21
|
+
- **接管原版数据目录**:当本插件自身尚无凭据时,会依次查找含凭据的原版数据目录并**直接复用**(不是复制——复制会产生两份凭据,改密码只改一边而分叉):
|
|
22
|
+
1. 同级目录 `../dsh-webui-auth/`(源码 / `link:` 安装最常见);
|
|
23
|
+
2. `$DSH_HOME/dsh-webui-auth/`(原版的兜底位置)。
|
|
24
|
+
两处都没有则用自己的目录(全新安装,走 setup token)。**自身已有凭据时一律不接管**,避免覆盖你已在新插件里建立的数据。
|
|
25
|
+
- **OIDC 配置独立存放**:账号凭据留在 `dsh-webui-auth.json`,OIDC 配置(含 `clientSecret`)写入本插件自己的 `dsh-webui-oauth.json`;旧版曾把 `oidc` 段写在凭据文件里,读取时自动回落、保存时自动迁移。
|
|
26
|
+
- 卸载原版、只留本插件同样正常工作。
|
|
27
|
+
|
|
28
|
+
## 架构
|
|
29
|
+
|
|
30
|
+
认证由四层组成,全部通过**运行时包装 webServer 路由**实现,**不改动任何 DSH 核心包源码**:
|
|
31
|
+
|
|
32
|
+
| 层 | 机制 | 未认证行为 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| WebUI 资源(index.html、/assets/*、SPA 路由) | 插件注册 `prefix ''` 兜底路由,校验会话后转交 frontend-static | 302 → 登录页 |
|
|
35
|
+
| 插件 bundle(/plugins/*) | 运行时包装 `/plugins` 前缀路由 handler | 401 |
|
|
36
|
+
| /api RPC 接口 | 运行时包装 `/api` 前缀路由 handler | 401 |
|
|
37
|
+
| WebSocket(`/api/remote.mux`;旧核心 `/api/events.mux`、`/api/events.host`) | 运行时包装 upgrade 路由 handler | 401 拒绝升级 |
|
|
38
|
+
|
|
39
|
+
- **不修改核心包**:dsh 升级不会覆盖补丁、不会产生「升级后 /api 裸奔」的窗口。插件每次启动对路由表重做包装,并用 2s→10s 重扫捕获晚注册路由。**v0.1.2-alpha.2 及更新核心的事件流 WebSocket 位于 `/api/remote.mux`(由 dsh-api-gateway 注册),候选列表自动适配,不再误报「upgrade 路由缺失」。**
|
|
40
|
+
- **fail-closed**:若预期路由缺失(dsh 内部结构变化导致包装不上),`setup`/`configure` 会**拒绝启用认证**,并在宿主日志与设置页同时报错——宁可不可用,不可「开了登录却裸奔 /api」。
|
|
41
|
+
- **与核心自带浏览器认证(v0.1.2-alpha.2+)协作**:该版本核心自带 launch-token 交换的签名 Cookie(`dsh-auth-*`)认证 `/` 与 `/api`。插件登录成功后自动把浏览器引导到核心的带 token 根 URL 完成核心 Cookie 交换;`/api` 请求在插件会话校验后**原样转交**给核心(不再改写 Host/Origin,避免破坏核心 Cookie 与 Host 绑定)。两者叠加:浏览器须同时持有插件会话 Cookie 与核心 Cookie。
|
|
42
|
+
- **反代/局域网旧核心下的特权方法**(≤alpha.1):已认证请求由插件在会话校验后以「回环形状」转交核心,使核心中**回环钉死的特权方法**(settings/credentials/agentPreset/llm.discoverModels)在反代部署下可用——会话 Cookie 闸门是比 Host 启发式更强的身份证明。
|
|
43
|
+
- **WebSocket 与 trustedHosts**:WS 升级握手仍受核心自身 `requestRejection` / `isTrustedApiRequest` 限制,因此**反代/局域网(非回环 Host)部署下,WS 下行需要同时在 dsh 配置中把对外域名加入 `client-connection.trustedHosts`**,否则即使已登录也会被拒绝升级。
|
|
44
|
+
- **跳转目标:动态 vs 绝对(统一开关,0.4.1)**:有两处跳转会用到**请求方(浏览器)提供的 origin**——① OIDC 回调 base(登录页上报 `location.origin`);② 登录成功后交给核心的 token 跳转(authority 取自请求 `Host`)。两处**共用同一个开关**,配置在 dsh settings 的 `remote-web-ui` 段(部署级):
|
|
45
|
+
```yaml
|
|
46
|
+
remote-web-ui:
|
|
47
|
+
publicBaseUrl: 'https://dsh.example.com' # 绝对跳转的目标(可选)
|
|
48
|
+
trustBrowserOrigin: true # 省略即按 true:动态跳转
|
|
49
|
+
```
|
|
50
|
+
- **`trustBrowserOrigin: true`(默认)= 动态跳转**:跳转目标随本次请求的实际地址走(反代透传的 Host,或反代下发的 `X-Forwarded-Host`)。适配「同一份配置服务多个入口」。
|
|
51
|
+
- **`trustBrowserOrigin: false` = 绝对跳转**:一律用 `publicBaseUrl`,忽略请求里的地址。
|
|
52
|
+
- 旧配置 `oidc.trustBrowserOrigin` 仍被接受:settings 段未显式配置时回退它,便于已有部署平滑升级。
|
|
53
|
+
|
|
54
|
+
**判定顺序(重要:回环兜底会绕过开关)**:
|
|
55
|
+
|
|
56
|
+
| 序 | 条件 | 结果 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| 1 | 反代下发了原始主机头(`X-Forwarded-Host` / `Forwarded: host=`) | **按开关**:开 → 用该头;关 → 用 `publicBaseUrl` |
|
|
59
|
+
| 2 | 否则 Host 是回环/本机(`127.0.0.1`、`::1`、`localhost`、`0.0.0.0`) | **恒用 `publicBaseUrl`(不听开关)** |
|
|
60
|
+
| 3 | 否则(Host 已透传真实地址) | **按开关**:开 → 用请求 Host;关 → 用 `publicBaseUrl` |
|
|
61
|
+
|
|
62
|
+
第 2 行是**动态跳转不可用时的兜底**,不是第二个开关:Host 被改写成回环时,服务端从请求里**拿不到**任何浏览器侧地址,开关即使开着也没有可用值,只能回退 `publicBaseUrl`(该值来自 `settings.yaml`,运维可控、非攻击者可控,不构成开放重定向)。
|
|
63
|
+
|
|
64
|
+
**端口规则**:端口**只来自被选中那一方的字面**,不做任何补全。
|
|
65
|
+
- 动态路径:端口取自 Host / 原始主机头。不带端口就不带(浏览器按 scheme 推 443)。
|
|
66
|
+
- 绝对路径:端口取自 `publicBaseUrl`。**CDN 仅在 443 对外时应写不带端口**(`https://dsh.example.com`);写成 `:8443` 会让浏览器去撞 8443。
|
|
67
|
+
- `publicBaseUrl` 里的端口**只参与同源比对**(见下条),不会被注入到动态路径的跳转目标里。
|
|
68
|
+
|
|
69
|
+
**已知取舍**:本机浏览器直连 `http://127.0.0.1:3080` 也落进第 2 行,于是开关开着也会跳 `publicBaseUrl`。要同时支持本机直连,只能让反代下发 `X-Forwarded-Host`(走第 1 行)。
|
|
70
|
+
- **登录后跳转的协议自适应(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://`(反向不降级、异源不改写)。
|
|
71
|
+
- **可选 OIDC 单点登录(0.4.0)**:标准 OIDC 授权码 + PKCE(机密客户端,须 `client_secret`),以 Logto 为安全基准。配置后登录页出现「SSO 单点登录」按钮,认证通过后以 IdP 的 `sub` 建立本地会话(沿用会话有效期与持久化)。**redirect_uri 的 base 由 `oidc.redirectBase` 决定;默认交由浏览器处理(采信前端 `location.origin`,反代重写 Host 时浏览器看到的公网地址即正确),受上面统一开关约束;关闭时仅用配置的 `redirectBase`;两者都不可用则 fail-closed 报错。** 支持 RSA/EC/EdDSA 签名的 id_token 验证(JWKS + iss/aud/exp/iat/nbf/nonce 校验),简单版登出(仅清本地会话)。
|
|
72
|
+
|
|
73
|
+
会话为**服务端会话,持久化到磁盘**(`sessions.jsonl`,重启 DSH 不掉线,到期自动失效),由 `HttpOnly; SameSite=Lax` Cookie(`dsh_wua_session`)携带,JS 无法读取;修改密码会**吊销所有其他会话**。
|
|
74
|
+
|
|
75
|
+
## 安装
|
|
76
|
+
|
|
77
|
+
本插件是标准**组合包(bundle)**,推荐用 DSH 官方 `plugin` 命令安装;手动方式保留作备用。前提:机器上有 pnpm(Node 自带 corepack,执行 `corepack enable pnpm` 即可启用)。
|
|
78
|
+
|
|
79
|
+
### 方式一:GitHub 安装(推荐)
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx @deepseek-ai/dsh plugin --profile web add github:WU-HINS/dsh-webui-oauth
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
拉取仓库源码(纯 JS 包,直接可用,无需构建步骤)。
|
|
86
|
+
|
|
87
|
+
### 方式二:npm 安装
|
|
88
|
+
|
|
89
|
+
> 需要该包已发布到 npm registry。若 `dsh-webui-oauth` 尚未发布,请使用方式一。
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npx @deepseek-ai/dsh plugin --profile web add dsh-webui-oauth
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
从 npm registry 拉取,加入依赖并追加到 `dsh.profile.bundles` 列表,插件行随组合包层自动插入。
|
|
96
|
+
|
|
97
|
+
> 若你此前装的是原版 `github:Yuuz12/dsh-webui-auth`,可以直接改装本仓库——本插件会自动顶掉原版并沿用原数据目录,账号无需重建。
|
|
98
|
+
|
|
99
|
+
### 方式三:手动(备用)
|
|
100
|
+
|
|
101
|
+
1. 将 `dsh-webui-oauth` 目录放入 `profiles/web/node_modules/`
|
|
102
|
+
2. 在 `profiles/web/cordis.patch.yml` 的 `insert` 列表中加一行:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- id: dsh-webui-oauth
|
|
106
|
+
name: 'dsh-webui-oauth'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
> 维护者开发模式:在本地源码目录使用 `dsh plugin --profile web add ./dsh-webui-oauth`(`link:` 安装),改代码 → 重启 DSH 即生效,无需重新安装。
|
|
110
|
+
|
|
111
|
+
### 所有方式通用
|
|
112
|
+
|
|
113
|
+
安装后**无需任何核心包补丁**(无 `[dsh-webui-auth patch]` 标记、不改 `node_modules`),重启 DSH 即生效。插件启动时在宿主日志打印 `[dsh-webui-oauth] started, credentials file: ...`;若路由包装不完整会打印 `ROUTE GATE INCOMPLETE`,此时认证无法启用(fail-closed)。
|
|
114
|
+
|
|
115
|
+
## 卸载
|
|
116
|
+
|
|
117
|
+
### 方式一:`dsh plugin` 命令(对应方式一安装)
|
|
118
|
+
|
|
119
|
+
1. `npx @deepseek-ai/dsh plugin --profile web remove dsh-webui-oauth`(同时移除依赖与组合包层)
|
|
120
|
+
2. 重启 DSH
|
|
121
|
+
|
|
122
|
+
### 方式二:手动(对应方式二安装)
|
|
123
|
+
|
|
124
|
+
1. 删除插件目录 `profiles/web/node_modules/dsh-webui-oauth/`(0.3.1 起运行数据在包外,删它不影响账号;如需连账号一起清除,另删数据目录 `.dsh-webui-auth/`,见「数据文件位置」)
|
|
125
|
+
2. 从 `profiles/web/cordis.patch.yml` 移除挂载行:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: dsh-webui-oauth
|
|
129
|
+
name: 'dsh-webui-oauth'
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
此步必须做,否则重启时加载器找不到插件包会报错
|
|
133
|
+
3. 重启 DSH
|
|
134
|
+
|
|
135
|
+
两种方式重启后认证门禁完全关闭(**无需恢复任何核心包源码**——插件从未修改核心文件)。如需清除持久化会话,删除数据目录中的 `sessions.jsonl` 即可;如曾用旧版插件,可清除浏览器 localStorage 中的 `dsh-webui-auth.session` 残留(无害)。
|
|
136
|
+
|
|
137
|
+
## 使用
|
|
138
|
+
|
|
139
|
+
- **首次启用(需 setup token)**:未配置凭据时认证自动关闭(所有请求放行),但创建管理员账号需要**本次启动生成的 setup token**——打开 WebUI → 设置 → 身份认证,或访问 `/dsh-webui-oauth/login`,输入启动日志中打印的 `[dsh-webui-oauth] setup token (...)`(或数据目录 `setup-token` 文件内容,0600)后创建账号密码(≥8 位,含大小写字母、数字、特殊符号)。token 每次启动重新生成、创建成功后即删除,防止「先暴露、后配置」窗口内被他人抢先注册。
|
|
140
|
+
- **用户名规则**:3-32 位字母、数字、下划线或连字符(新建/修改时强制;旧账号不受影响,仍可正常登录)。
|
|
141
|
+
- **之后**:未登录访问任意路径 → 跳转登录页;登录后按「会话有效期」免登录(浏览器会话 / 1 小时 / 12 小时(默认)/ 1 天 / 3 天),服务端按到期时间强制失效。**会话持久化到磁盘,重启 DSH 后已登录设备无需重新登录**(到期时间仍生效)。「浏览器会话」模式:活跃使用期间自动续期(30 分钟窗口),关闭浏览器即失效。
|
|
142
|
+
- **修改 / 禁用 / 退出**:设置 → 身份认证(均需当前密码);修改密码会吊销其他所有已登录会话。
|
|
143
|
+
- **忘记密码**:删除数据目录的 `dsh-webui-auth.json` 即可——后台每分钟自动检测,最多 1 分钟内认证自动关闭(无需重启),之后用新的 setup token 重新创建账号即可。
|
|
144
|
+
- **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 单点登出。
|
|
145
|
+
|
|
146
|
+
## 数据文件位置(按安装方式区分)
|
|
147
|
+
|
|
148
|
+
凭据与安全数据存放在**运行时数据目录**,按安装方式自动选择:
|
|
149
|
+
|
|
150
|
+
- **npm / GitHub / tarball 安装**:插件包体位于 `node_modules` 内,会随升级、重装、清理被整目录替换——数据因此存放在**该 `node_modules` 的上级目录**下的 `.dsh-webui-auth/`(通常即 profile 根,如 `~/.dsh/profiles/web/.dsh-webui-auth/`)。升级插件、`pnpm clean`、重装 DSH 都不会丢失账号密码与登录会话。
|
|
151
|
+
- **本地 link / 源码安装**:为插件源码目录(随仓库管理、已被 `.gitignore` 排除出 git;删除整个源码目录才会连数据一起清除)。
|
|
152
|
+
- **兜底**:以上位置均不可写时回退到 `$DSH_HOME/dsh-webui-auth/`(默认 `~/.dsh/dsh-webui-auth/`)。
|
|
153
|
+
|
|
154
|
+
从 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 重建账号即可。
|
|
155
|
+
|
|
156
|
+
目录内文件:
|
|
157
|
+
|
|
158
|
+
| 文件 | 用途 | 权限 |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `dsh-webui-auth.json` | **账号凭据**(scrypt 哈希,v3 格式;0.2.x 的 v2 凭据仍可正常登录校验)。文件名沿用原版,保证升级不丢账号 | — |
|
|
161
|
+
| `dsh-webui-oauth.json` | **本插件独有配置**(OIDC 段:`issuer` / `clientId` / `clientSecret` / `scope` / `redirectBase` / `trustBrowserOrigin`)。独立成文件,避免 OIDC 密钥与账号凭据混在一起 | — |
|
|
162
|
+
| `audit.jsonl` | 审计日志(IP 已假名化,见「审计日志」节) | — |
|
|
163
|
+
| `sessions.jsonl` | 持久化会话(重启恢复用) | 0600 |
|
|
164
|
+
| `audit-hmac-key` | 审计 IP 假名化的 HMAC 密钥(首次自动生成) | 0600 |
|
|
165
|
+
| `setup-token` | 首次初始化的 setup token(创建成功后删除) | 0600 |
|
|
166
|
+
|
|
167
|
+
数据目录由安装方式决定(npm / GitHub / tarball → `node_modules` 上级的 `.dsh-webui-auth/`;link / 源码 → 插件源码目录;兜底 `$DSH_HOME/dsh-webui-auth/`),忘记密码、审计、会话路径均指该目录。也可用环境变量 `DSH_WEBUI_AUTH_DATA_DIR` 显式指定数据目录(容器 / 只读包体等部署适用)。
|
|
168
|
+
|
|
169
|
+
> **从旧版升级(OIDC 配置布局变更)**:早期版本把 `oidc` 段写在 `dsh-webui-auth.json` 里,现在改由 `dsh-webui-oauth.json` 承载。读取时**仍会回落到旧位置**(无感升级),并在你下次于设置页保存时自动迁移到新文件、同时从凭据文件剥离旧字段。
|
|
170
|
+
|
|
171
|
+
## 审计日志
|
|
172
|
+
|
|
173
|
+
登录成功/失败/限流、初始化、修改凭据、禁用、退出等安全事件**追加写入数据目录的 `audit.jsonl`**(JSONL 格式,含时间、用户名、IP、UA、详情)。**客户端 IP 以 HMAC-SHA256 假名化存储**(形如 `hmac:5151e752|203.0.113.0/24`,附 /24(IPv4)或 /64(IPv6)网络前缀用于聚合分析),原始 IP 不落盘。两种查看方式:
|
|
174
|
+
|
|
175
|
+
- **CLI**(推荐):运行 `node index.js audit [--limit N]`(默认最近 20 条,从模块所在路径运行即可):
|
|
176
|
+
```sh
|
|
177
|
+
node index.js audit --limit 50
|
|
178
|
+
```
|
|
179
|
+
- **设置页**:设置 → 身份认证 → 「最近登录记录」展示最近 8 条。
|
|
180
|
+
|
|
181
|
+
审计写入失败不影响认证主流程(仅记宿主日志)。
|
|
182
|
+
|
|
183
|
+
## 外观
|
|
184
|
+
|
|
185
|
+
登录页与「设置 → 身份认证」设置页都跟随 DSH **自带的外观设置**(设置 → 通用 → 外观:浅色 / 深色 / 跟随系统),不提供独立的外观开关。设置页运行在 WebUI 内,直接消费 DSH 的主题 token,天然随明暗切换;登录页是独立页面,由服务端读取当前外观偏好(settings `ui-theme.preference`)注入页面,并复刻 DSH 的 boot 逻辑:`跟随系统` 时按 `prefers-color-scheme` 解析、系统明暗切换时实时变化。登录页响应带 `cache-control: no-store`,外观变更后刷新即可生效。
|
|
186
|
+
|
|
187
|
+
## 升级 DSH 后的操作流程
|
|
188
|
+
|
|
189
|
+
**无需任何操作**:插件不修改核心包,dsh 升级后启动时自动重做路由包装。**v0.1.2-alpha.2 及更新核心**:插件自动适配 `/api/remote.mux` 事件流路由,并与核心自带浏览器认证(launch-token ↔ 签名 Cookie)协作——登录插件后浏览器会被自动引导完成核心认证,随后 WebUI 正常使用。若包装不完整(dsh 内部结构变化),宿主日志输出 `ROUTE GATE INCOMPLETE`、设置页显示红色警告,且 `setup`/`configure` 拒绝启用认证(fail-closed)。
|
|
190
|
+
|
|
191
|
+
> **注意(v0.1.2-alpha.2+)**:核心自带浏览器认证要求浏览器先通过 launch-token 换取核心 Cookie(`dsh web` / DSH Desktop 打印的带 `?token=` 的地址)。插件已自动处理该交换;如浏览器此前从未访问过该地址,请使用 DSH 启动时打印的完整 URL 打开一次(或从插件登录页登录,流程会自动完成交换)。
|
|
192
|
+
|
|
193
|
+
## 数据与安全
|
|
194
|
+
|
|
195
|
+
- 密码以 **scrypt**(Node 内置内存硬 KDF,抗 GPU/ASIC 爆破,零依赖)哈希保存在数据目录的 `dsh-webui-auth.json`,明文不落盘。凭据格式 v3(与 v2 同为 scrypt 编码,仅版本标记与字段语义不同);0.2.x 的 v2 凭据仍可校验登录。**0.1.x 的 SHA-256 凭据自 0.2.0 起不再可校验**,需删除凭据文件后重新创建账号(见「忘记密码」)。
|
|
196
|
+
- 登录失败限流:**按客户端 IP** 每分钟最多 5 次——单个攻击者无法锁死其他用户(操作者)。反代场景下客户端 IP 取自 `CF-Connecting-IP` / `X-Forwarded-For` 最左侧,且**仅当对端 socket 是回环**(本机 caddy/cloudflared)时才信任代理头,远程无法伪造;校验失败时还会空跑一次 scrypt,抹平「账号不存在=响应快」的用户名枚举时序差异。
|
|
197
|
+
- 首次初始化需要每启动生成的 **setup token**(128-bit,打印到宿主日志并写入数据目录 `setup-token`,0600),防止「先暴露、后配置」被抢占。
|
|
198
|
+
- 审计日志:`audit.jsonl`,客户端 IP 以 HMAC 假名化存储(见「审计日志」节)。
|
|
199
|
+
- 会话持久化:`sessions.jsonl`(0600),重启恢复;写失败时认证不受影响,设置页提示重启后需重新登录。
|
|
200
|
+
- 登录页与 API 响应均带安全头:严格 CSP、`nosniff`、`DENY` 防嵌框、`no-referrer`、`noindex`、`no-store`。
|
|
201
|
+
- Cookie `HttpOnly + SameSite=Lax`:JS 不可读、跨站请求不携带。
|
|
202
|
+
- 登录/初始化端点本身公开(认证的必然入口):`/dsh-webui-oauth/login`、`/dsh-webui-oauth/setup`(后者受 setup token 保护)。
|
|
203
|
+
- **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`,与账号凭据分离。
|
|
204
|
+
|
|
205
|
+
## 已知边界
|
|
206
|
+
|
|
207
|
+
- **运行时包装的固有窗口**:路由对象被替换(服务热重载)到下一次重扫之间(≤10s)存在未保护窗口;启用认证时的 fail-closed 已挡住「初始裸奔」,此窗口仅影响运行中的热重载场景。
|
|
208
|
+
- **WS 与 trustedHosts**:反代/局域网(非回环 Host)下,WS 下行需在 dsh 配置 `client-connection.trustedHosts` 中加入对外域名(见「架构」节)。
|
|
209
|
+
- **反代不同机**:若反代与 DSH 不在同一台机器(对端非回环),代理头不被信任,限流将按代理 IP 聚合(退化为全局桶)。
|
|
210
|
+
- **HTTPS 反代且未下发协议头**:登录后跳转的 scheme 依赖 ①`remote-web-ui.publicBaseUrl` 或 ②反代下发的 `X-Forwarded-Proto`/`Forwarded`。两者都没有时只能兜底 `http`(此时 https 端口会握手失败、页面「点了没反应」)。请二选一:在 `settings.yaml` 声明对外地址,或让反代 `proxy_set_header X-Forwarded-Proto $scheme;`。注意 `publicBaseUrl` 仅在与请求 Host 一致时生效,用来避免把局域网直连改写到公网。
|
|
211
|
+
- **`--trusted-host` 不能省**:桌面浏览器用密码登录后**直连 `/api`** 的请求依赖 `--trusted-host <对外域名:端口>`;而 remote-web-ui 的配对流(`/remote` 通道)不需要它。删掉该参数会导致 `/api` 全 403。
|
|
212
|
+
- **审计假名化的边界**:HMAC 密钥与审计日志同目录(0600),能读取密钥文件的本地攻击者可对 IP 空间暴力还原;假名化防的是「日志明文落盘」,不是防有文件权限的攻击者。
|
|
213
|
+
- 会话存于数据目录 `sessions.jsonl`:重启后仍生效(到期时间不变);关闭/卸载插件不影响凭据。
|
|
214
|
+
- 威胁模型为「浏览器/网络客户端」:能直接读写宿主进程内存或文件的本地进程不在防护范围内。
|