dsh-webui-oauth 0.6.5 → 0.6.6
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/CHANGELOG.md +50 -0
- package/README.en.md +30 -0
- package/README.md +26 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录每个版本的**用户可见**变更。破坏性变更与升级动作单独标注。
|
|
4
4
|
|
|
5
|
+
## 0.6.6
|
|
6
|
+
|
|
7
|
+
对 **OIDC 授权跳转**做一次全量验证(固定路径 / 反代 / 浏览器动态三路),
|
|
8
|
+
并把结论固化成回归测试。本版**没有行为变更**,只补测试与文档。
|
|
9
|
+
|
|
10
|
+
### 验证:三条跳转路径在真实服务器上全部走通
|
|
11
|
+
|
|
12
|
+
用一台真实 IdP(HTTPS + 自签 CA)、一台反向代理(复刻 Caddy 的两种 Host 改写模式)
|
|
13
|
+
和一个真实 dsh 实例,端到端跑完「发起授权 → IdP → 回调 → 换 token → 建会话 → 跳回应用」,
|
|
14
|
+
共 56 项断言:
|
|
15
|
+
|
|
16
|
+
- **固定路径跳转**(配置 `redirectBase`,20 项):`redirect_uri` 恒为
|
|
17
|
+
`redirectBase + /dsh-webui-oauth/oidc/callback`。浏览器上报异源 origin、
|
|
18
|
+
base 里夹带路径、反代改写 Host 都带不偏它。绑定与登录两条路径结束时均为 302,
|
|
19
|
+
回执分别是 `oidcbound=1` 与 launch token。
|
|
20
|
+
- **反代跳转**(17 项):`X-Forwarded-Host` / RFC 7239 `Forwarded: host=` 优先于
|
|
21
|
+
被改写的 Host;反代什么都不下发时按回环判定,回退配置的确定性 origin,
|
|
22
|
+
回跳**绝不指向内部上游地址**;https 对外地址不被降级为 http。
|
|
23
|
+
- **浏览器动态跳转**(19 项):`redirectBase` 留空时采信登录页上报的
|
|
24
|
+
`location.origin`;经直通反代时端到端回到浏览器看到的地址(含会话不丢)。
|
|
25
|
+
开关关闭或两者皆缺时 **fail-closed**。非法值全部被拒:
|
|
26
|
+
`javascript:`、带 path/query/hash/userinfo、以及含 CRLF 的注入尝试。
|
|
27
|
+
|
|
28
|
+
### 澄清:绑定路径为什么不下发新会话 Cookie
|
|
29
|
+
|
|
30
|
+
绑定流程结束时只清一次性 state Cookie、**不再下发会话 Cookie**——这是刻意的:
|
|
31
|
+
绑定发生在管理员已登录之后,用的是同一个会话;重新下发会话等于无谓地轮换凭据。
|
|
32
|
+
登录路径则相反,必须下发新会话。两者的差异在测试里显式锁定,避免被"顺手统一"掉。
|
|
33
|
+
|
|
34
|
+
### 修复:测试偶发失败(曾导致 v0.6.6 发布中断)
|
|
35
|
+
|
|
36
|
+
- **现象**:`test/oidc.test.mjs` 的「坏签名」用例偶发 FAIL
|
|
37
|
+
(`got null want "bad-signature"`),本地约 2% 概率触发,表现为 CI 上
|
|
38
|
+
"Verify tests pass before publish" 失败、npm publish 被跳过。
|
|
39
|
+
- **原因**:该用例用 `token.slice(0,-2) + 'aa'` 篡改签名末两位。但 RS256 签名是
|
|
40
|
+
256 字节,base64url 编码后 342 字符 = 2052 bit,而数据只需 2048 bit——
|
|
41
|
+
**末位字符的低 4 位是填充位,解码时被丢弃**。于是当末位落在 `{A,Q,g,w}` 时
|
|
42
|
+
(实测撞到 `aQ` → `aa`),解码出的签名字节与原值**完全一致**,签名压根没被改动,
|
|
43
|
+
验签自然通过。这是测试的前提错误,不是验签逻辑的问题。
|
|
44
|
+
- **修复**:改为翻转末位字符,并**校验解码后的字节确实发生变化**,否则换下一个候选;
|
|
45
|
+
另加一条断言锁死"篡改必须改变字节",防止该用例退化成假阳性。
|
|
46
|
+
修后连续 400 次运行零失败(修前约 2% 失败率)。
|
|
47
|
+
|
|
48
|
+
### 测试
|
|
49
|
+
|
|
50
|
+
- 新增 `test/oidc-redirect-matrix.test.mjs`(37 项):把上述三路决策固化为
|
|
51
|
+
无需外部依赖的单元回归。已确认能抓出回归——去掉 `isValidOrigin` 的路径校验后
|
|
52
|
+
有 2 项立刻 FAIL。
|
|
53
|
+
- 全量 16 个测试文件、381 项断言通过。
|
|
54
|
+
|
|
5
55
|
## 0.6.5
|
|
6
56
|
|
|
7
57
|
补全 0.6.4 遗漏的路径:**SSO 登录成功后同样停在 JSON 页面上**。
|
package/README.en.md
CHANGED
|
@@ -337,6 +337,36 @@ Preconditions: **no** `remote-web-ui.publicBaseUrl` configured, `trustBrowserOri
|
|
|
337
337
|
Note that `publicBaseUrl` is **only honoured when its authority matches the request Host** (to prevent an open redirect). When Host has already been rewritten to loopback the two do not match, so **`publicBaseUrl` cannot rescue this case either** — you must use the first option. This is an easy misconfiguration, hence documenting it here.
|
|
338
338
|
|
|
339
339
|
You also need `--trusted-host <public host:port>`, otherwise a desktop browser hitting `/api` directly gets 403 (see "Known limits").
|
|
340
|
+
### D. The three OIDC redirect paths, measured end-to-end (0.6.6)
|
|
341
|
+
|
|
342
|
+
Using a real IdP (HTTPS with a self-signed CA), a real reverse proxy (both Host-rewriting
|
|
343
|
+
modes) and a real dsh instance, the full round trip — "start authorization → IdP →
|
|
344
|
+
callback → token exchange → local session → back to the app" — was run end to end.
|
|
345
|
+
All 56 assertions pass. The table below lists the measured values for `redirect_uri`
|
|
346
|
+
and for the post-callback redirect target:
|
|
347
|
+
|
|
348
|
+
| Path | Key config | redirect_uri seen by the IdP | Target after callback |
|
|
349
|
+
| --- | --- | --- | --- |
|
|
350
|
+
| Fixed path | `redirectBase=http://127.0.0.1:14080` | `http://127.0.0.1:14080/dsh-webui-oauth/oidc/callback` | `http://127.0.0.1:14080/?…&oidcbound=1` ✅ |
|
|
351
|
+
| Fixed path + hostile origin | same, with `base=https://evil.example.com` | still the configured 14080 ✅ (not swayed) | same ✅ |
|
|
352
|
+
| Reverse proxy (Host rewritten + XFH) | same, via 14080 | the configured 14080 ✅ | 14080 (the browser-side address) ✅ |
|
|
353
|
+
| Reverse proxy (original Host kept) | same, via 14081 | the configured 14080 ✅ | 14080, **never the internal 13081** ✅ |
|
|
354
|
+
| Browser-dynamic | `redirectBase` empty + `trustBrowserOrigin=true` | the origin reported by the browser ✅ | back to that origin ✅ |
|
|
355
|
+
| Dynamic + pass-through proxy | same, via 14081 | `http://127.0.0.1:14081/dsh-webui-oauth/oidc/callback` ✅ | `http://127.0.0.1:14081/?…` (session preserved) ✅ |
|
|
356
|
+
| Dynamic but switch off | `trustBrowserOrigin=false`, no `redirectBase` | **no authorization started**, `oidc-base-unresolvable` ✅ | — |
|
|
357
|
+
|
|
358
|
+
Rejected inputs (no authorization started, no injection):
|
|
359
|
+
`javascript:alert(1)`, origins carrying a path / query / hash / userinfo, and
|
|
360
|
+
CRLF-injection attempts such as `https://evil.test/\r\nX-Injected: 1`.
|
|
361
|
+
|
|
362
|
+
**Why the bind path sends no session cookie**: on completion the bind flow only clears
|
|
363
|
+
the one-shot state cookie. That is deliberate — binding happens *after* the administrator
|
|
364
|
+
is already signed in and reuses the same session, so re-issuing one would rotate
|
|
365
|
+
credentials for no reason. The login path is the opposite and must send a new session
|
|
366
|
+
(the response carries two `Set-Cookie` headers). This difference is locked down by tests.
|
|
367
|
+
|
|
368
|
+
Reproduce with `test/oidc-redirect-matrix.test.mjs` (dependency-free unit regression,
|
|
369
|
+
wired into `npm test`).
|
|
340
370
|
|
|
341
371
|
## Known limits
|
|
342
372
|
|
package/README.md
CHANGED
|
@@ -355,6 +355,32 @@ IdP 侧日志确认完整往返:`discovery → jwks → authorize(issued code)
|
|
|
355
355
|
|
|
356
356
|
另需 `--trusted-host <对外域名:端口>`,否则桌面浏览器直连 `/api` 会 403(见「已知边界」)。
|
|
357
357
|
|
|
358
|
+
### D. OIDC 授权跳转三路实测(0.6.6 全量验证)
|
|
359
|
+
|
|
360
|
+
用真实 IdP(HTTPS + 自签 CA)、真实反向代理(两种 Host 改写模式)与真实 dsh 实例,
|
|
361
|
+
端到端跑完「发起授权 → IdP → 回调 → 换 token → 建会话 → 跳回应用」,共 56 项断言全通过。
|
|
362
|
+
下表是三条路径下 `redirect_uri` 与「回调后回跳目标」的实测值:
|
|
363
|
+
|
|
364
|
+
| 路径 | 关键配置 | IdP 收到的 redirect_uri | 回调后回跳目标 |
|
|
365
|
+
| --- | --- | --- | --- |
|
|
366
|
+
| 固定路径跳转 | `redirectBase=http://127.0.0.1:14080` | `http://127.0.0.1:14080/dsh-webui-oauth/oidc/callback` | `http://127.0.0.1:14080/?…&oidcbound=1` ✅ |
|
|
367
|
+
| 固定路径 + 浏览器谎报异源 | 同上,且 `base=https://evil.example.com` | 仍为配置的 14080 ✅(不被带偏) | 同上 ✅ |
|
|
368
|
+
| 反代(Host 改写 + XFH) | 同上,经 14080 | 配置的 14080 ✅ | 指向 14080(浏览器侧)✅ |
|
|
369
|
+
| 反代(保留原始 Host) | 同上,经 14081 | 配置的 14080 ✅ | 14080,**不指向内网 13081** ✅ |
|
|
370
|
+
| 浏览器动态跳转 | `redirectBase` 留空 + `trustBrowserOrigin=true` | 浏览器上报的 origin ✅ | 回到该 origin ✅ |
|
|
371
|
+
| 动态 + 直通反代 | 同上,经 14081 | `http://127.0.0.1:14081/dsh-webui-oauth/oidc/callback` ✅ | `http://127.0.0.1:14081/?…`(会话不丢)✅ |
|
|
372
|
+
| 动态但开关关闭 | `trustBrowserOrigin=false` 且无 `redirectBase` | **不发起授权**,返回 `oidc-base-unresolvable` ✅ | — |
|
|
373
|
+
|
|
374
|
+
被明确拒绝的非法输入(均不发起授权、不产生注入):
|
|
375
|
+
`javascript:alert(1)`、带 path / query / hash / userinfo 的 origin、
|
|
376
|
+
以及含 CRLF 的注入尝试(`https://evil.test/\r\nX-Injected: 1`)。
|
|
377
|
+
|
|
378
|
+
**关于绑定路径不下发会话 Cookie**:绑定结束时只清一次性 state Cookie。
|
|
379
|
+
这是刻意的——绑定发生在管理员**已登录之后**,复用同一会话即可;重新下发等于无谓轮换凭据。
|
|
380
|
+
登录路径相反,必须同时下发新会话(响应含 2 条 `Set-Cookie`)。此差异已在测试中锁定。
|
|
381
|
+
|
|
382
|
+
复现方式见 `test/oidc-redirect-matrix.test.mjs`(无需外部依赖的单元回归,已接入 `npm test`)。
|
|
383
|
+
|
|
358
384
|
## 已知边界
|
|
359
385
|
|
|
360
386
|
- **运行时包装的固有窗口**:路由对象被替换(服务热重载)到下一次重扫之间(≤10s)存在未保护窗口;启用认证时的 fail-closed 已挡住「初始裸奔」,此窗口仅影响运行中的热重载场景。
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-webui-oauth",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs"
|
|
7
|
+
"test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|