From b273e30c2f2c57960a7cea103d40619d87b2c649 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 13 Jul 2026 19:56:06 +0530 Subject: [PATCH] fix: preserve original host through forward auth --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 ++++++++++---------- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 ++-- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 ++++++++++++------------ .publish/encryption/package.json | 2 +- .publish/helpers/README.md | 5 +++++ .publish/helpers/package.json | 4 ++-- .publish/i18n/package.json | 4 ++-- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 ++-- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +++--- .publish/ssr/package.json | 4 ++-- .publish/styles/package.json | 4 ++-- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 ++-- .publish/uploader/package.json | 4 ++-- .publish/validation/package.json | 2 +- packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 8 ++++++++ packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/dev-server/src/gateway.ts | 11 ++++++++-- packages/dev-server/test/gateway.test.ts | 25 +++++++++++++++++++++++ packages/encryption/package.json | 2 +- packages/helpers/README.md | 5 +++++ packages/helpers/package.json | 2 +- packages/helpers/src/index.ts | 11 ++++++++-- packages/helpers/test/helpers.test.ts | 22 ++++++++++++++++++++ packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/package.json | 2 +- 59 files changed, 167 insertions(+), 88 deletions(-) diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 52a3391a..6eea4bec 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index c8db34f6..0471de73 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index f12c908c..62f1f6e9 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.21", - "@wrnexus/router": "^0.2.21", - "@wrnexus/csr": "^0.2.21", - "@wrnexus/compiler": "^0.2.21", - "@wrnexus/styles": "^0.2.21", - "@wrnexus/dev-server": "^0.2.21", - "@wrnexus/ui": "^0.2.21", - "@wrnexus/validation": "^0.2.21", - "@wrnexus/i18n": "^0.2.21", - "@wrnexus/db": "^0.2.21" + "@wrnexus/core": "^0.2.22", + "@wrnexus/router": "^0.2.22", + "@wrnexus/csr": "^0.2.22", + "@wrnexus/compiler": "^0.2.22", + "@wrnexus/styles": "^0.2.22", + "@wrnexus/dev-server": "^0.2.22", + "@wrnexus/ui": "^0.2.22", + "@wrnexus/validation": "^0.2.22", + "@wrnexus/i18n": "^0.2.22", + "@wrnexus/db": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 1722e0a6..6007b8b0 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 91fdbe3d..2b702f9a 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 5b77ef41..19afbb2a 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.21" + "@wrnexus/core": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 4b631296..4837baa8 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 1ae3eff0..1320fa42 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.21", - "@wrnexus/router": "^0.2.21", - "@wrnexus/ssr": "^0.2.21", - "@wrnexus/csr": "^0.2.21", - "@wrnexus/compiler": "^0.2.21", - "@wrnexus/styles": "^0.2.21", - "@wrnexus/ui": "^0.2.21", - "@wrnexus/validation": "^0.2.21", - "@wrnexus/i18n": "^0.2.21", - "@wrnexus/db": "^0.2.21", - "@wrnexus/pubsub": "^0.2.21", - "@wrnexus/uploader": "^0.2.21" + "@wrnexus/core": "^0.2.22", + "@wrnexus/router": "^0.2.22", + "@wrnexus/ssr": "^0.2.22", + "@wrnexus/csr": "^0.2.22", + "@wrnexus/compiler": "^0.2.22", + "@wrnexus/styles": "^0.2.22", + "@wrnexus/ui": "^0.2.22", + "@wrnexus/validation": "^0.2.22", + "@wrnexus/i18n": "^0.2.22", + "@wrnexus/db": "^0.2.22", + "@wrnexus/pubsub": "^0.2.22", + "@wrnexus/uploader": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 165bfcec..ab727a9c 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/README.md b/.publish/helpers/README.md index b796b19a..f3bd2b41 100644 --- a/.publish/helpers/README.md +++ b/.publish/helpers/README.md @@ -44,6 +44,11 @@ Always list the application hosts that are valid redirect destinations. Forwarde headers are rejected when `allowedHosts` is absent or does not match, preventing an open redirect. A callback can support dynamic tenant domains: +The SSO hostname is the login destination, not an `allowedHosts` entry. For example, +when protecting `admin.localhost:3000`, keep `admin.localhost:3000` in the allowlist even +though the verifier runs at `sso.localhost:3000`. WRNexus preserves both hosts across a +nested gateway request. + ```ts allowedHosts: (host) => host.endsWith(".example.test"); ``` diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 1c143c71..15722442 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.21" + "@wrnexus/core": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 5a69394d..4bd406e2 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.21" + "@wrnexus/core": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index ef58102a..c49a5cfd 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index e81b295e..a1b54170 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.21" + "@wrnexus/native": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 992b0465..b261c015 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index cab596ef..7deb01af 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index 72d5f762..cb81d132 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index f1596cf9..d12ae92e 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 6e9d86ba..ec8838d9 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 7317eede..1c72dfc8 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.21", - "@wrnexus/core": "^0.2.21" + "@wrnexus/compiler": "^0.2.22", + "@wrnexus/core": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 529c519b..42cffdd2 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.21" + "@wrnexus/core": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 306fe2b6..7482e197 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.21" + "@wrnexus/uploader": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index bae8af16..83a419a3 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 27fa0047..59ea3edf 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index b7f3b6b9..a69a289a 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.21" + "@wrnexus/core": "^0.2.22" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index dc48936c..ade40922 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.21" + "@wrnexus/core": "^0.2.22" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 4e309def..50ed774c 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/packages/ai/package.json b/packages/ai/package.json index b7e40238..e63b68fb 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index ff254dd0..7e42e596 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 793e4ec1..aa6931de 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 3809d62a..7f6bf215 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -272,6 +272,14 @@ const MIGRATIONS: Migration[] = [ // Explicit no-op: updating @wrnexus/dev-server is sufficient. }, }, + { + version: "0.2.22", + id: "nested-forward-auth-origin", + description: "No project-file changes; nested forward-auth headers are fixed at runtime", + apply() { + // Explicit no-op: updating @wrnexus/dev-server and helpers is sufficient. + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 8b29e32b..d7155f4d 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index 04f8adb6..dc20c57a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 1c647341..0151c4b2 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index e6ce4522..e0828b40 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index ed6487c0..f94cfd25 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/dev-server/src/gateway.ts b/packages/dev-server/src/gateway.ts index 3bd20258..15425736 100644 --- a/packages/dev-server/src/gateway.ts +++ b/packages/dev-server/src/gateway.ts @@ -135,11 +135,18 @@ export function forwardAuthFailure(res: Response, verifierUrl: string): Response /** Describe the original gateway request to a forward-auth verifier. */ export function forwardAuthHeaders(req: Request): Headers { const original = new URL(req.url); + const host = req.headers.get("host") ?? original.host; + const protocol = original.protocol.replace(":", ""); return new Headers({ cookie: req.headers.get("cookie") ?? "", authorization: req.headers.get("authorization") ?? "", - "x-forwarded-host": req.headers.get("host") ?? original.host, - "x-forwarded-proto": original.protocol.replace(":", ""), + "x-forwarded-host": host, + "x-forwarded-proto": protocol, + // These survive when a verifier such as sso.localhost is routed through + // this gateway again. The second hop may replace X-Forwarded-Host with the + // verifier host, but must not lose the protected application's return URL. + "x-original-host": host, + "x-original-proto": protocol, "x-original-method": req.method, "x-original-uri": `${original.pathname}${original.search}`, }); diff --git a/packages/dev-server/test/gateway.test.ts b/packages/dev-server/test/gateway.test.ts index d8b09f93..d7343f89 100644 --- a/packages/dev-server/test/gateway.test.ts +++ b/packages/dev-server/test/gateway.test.ts @@ -52,12 +52,37 @@ test("forward auth describes the original gateway request", () => { expect(headers.get("x-forwarded-host")).toBe("admin.example.test"); expect(headers.get("x-forwarded-proto")).toBe("https"); + expect(headers.get("x-original-host")).toBe("admin.example.test"); + expect(headers.get("x-original-proto")).toBe("https"); expect(headers.get("x-original-method")).toBe("GET"); expect(headers.get("x-original-uri")).toBe("/settings?tab=security"); expect(headers.get("cookie")).toBe("session=abc"); expect(headers.get("authorization")).toBe("Bearer token"); }); +test("nested SSO proxy keeps the protected app's original request headers", () => { + const authHeaders = forwardAuthHeaders( + new Request("http://admin.localhost:3000/settings", { + headers: { host: "admin.localhost:3000" }, + }), + ); + authHeaders.set("host", "sso.localhost:3000"); + const verifierRequest = new Request("http://sso.localhost:3000/api/verify", { + headers: authHeaders, + }); + + const proxied = gatewayProxyHeaders( + verifierRequest, + new URL(verifierRequest.url), + "127.0.0.1", + true, + ); + + expect(proxied.get("x-forwarded-host")).toBe("sso.localhost:3000"); + expect(proxied.get("x-original-host")).toBe("admin.localhost:3000"); + expect(proxied.get("x-original-uri")).toBe("/settings"); +}); + test("gateway respawns development apps after an HMR restart exit", () => { expect(gatewayRestartDelay("development", 97, null)).toBe(0); expect(gatewayRestartDelay("development", 1, null)).toBe(1200); diff --git a/packages/encryption/package.json b/packages/encryption/package.json index d1f247fd..52274f91 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/README.md b/packages/helpers/README.md index b796b19a..f3bd2b41 100644 --- a/packages/helpers/README.md +++ b/packages/helpers/README.md @@ -44,6 +44,11 @@ Always list the application hosts that are valid redirect destinations. Forwarde headers are rejected when `allowedHosts` is absent or does not match, preventing an open redirect. A callback can support dynamic tenant domains: +The SSO hostname is the login destination, not an `allowedHosts` entry. For example, +when protecting `admin.localhost:3000`, keep `admin.localhost:3000` in the allowlist even +though the verifier runs at `sso.localhost:3000`. WRNexus preserves both hosts across a +nested gateway request. + ```ts allowedHosts: (host) => host.endsWith(".example.test"); ``` diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 53f814ed..0bf9059b 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index a1412bfc..33a52a31 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -78,7 +78,10 @@ export function getOriginalRequestUrl( ctx: RequestContext, options: OriginalRequestOptions = {}, ): URL { - const host = forwardedValue(ctx, "x-forwarded-host"); + // A forward-auth verifier can itself sit behind the same gateway. In that + // nested hop X-Forwarded-Host correctly describes the verifier (SSO), while + // X-Original-Host keeps the protected application's host (for returnTo). + const host = forwardedValue(ctx, "x-original-host") ?? forwardedValue(ctx, "x-forwarded-host"); const path = getOriginalRequestPath(ctx); if (!host) return new URL(path, ctx.url.origin); @@ -86,7 +89,11 @@ export function getOriginalRequestUrl( throw new TypeError(`Untrusted forwarded host: ${host}`); } - const protocol = (forwardedValue(ctx, "x-forwarded-proto") ?? ctx.url.protocol).replace(/:$/, ""); + const protocol = ( + forwardedValue(ctx, "x-original-proto") ?? + forwardedValue(ctx, "x-forwarded-proto") ?? + ctx.url.protocol + ).replace(/:$/, ""); if (protocol !== "http" && protocol !== "https") { throw new TypeError(`Unsupported forwarded protocol: ${protocol}`); } diff --git a/packages/helpers/test/helpers.test.ts b/packages/helpers/test/helpers.test.ts index 863a246c..ab115844 100644 --- a/packages/helpers/test/helpers.test.ts +++ b/packages/helpers/test/helpers.test.ts @@ -35,6 +35,28 @@ test("reconstructs an allowed original gateway URL", () => { expect(getOriginalRequestMethod(ctx)).toBe("GET"); }); +test("keeps the protected app URL when the SSO verifier crosses the gateway again", () => { + const ctx = context("http://sso.localhost:3000/api/verify", { + "x-forwarded-host": "sso.localhost:3000", + "x-forwarded-proto": "http", + "x-original-host": "admin.localhost:3000", + "x-original-proto": "http", + "x-original-method": "GET", + "x-original-uri": "/settings?tab=security", + }); + + const response = redirectToLogin(ctx, "/login", { + allowedHosts: ["admin.localhost:3000"], + }); + const location = new URL(response.headers.get("location")!); + + expect(location.origin).toBe("http://sso.localhost:3000"); + expect(location.pathname).toBe("/login"); + expect(location.searchParams.get("returnTo")).toBe( + "http://admin.localhost:3000/settings?tab=security", + ); +}); + test("rejects untrusted hosts and unsafe request paths", () => { const untrusted = context("http://sso.localhost/api/verify", { "x-forwarded-host": "evil.example", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index f5454b7c..996dbeb1 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 86b2c053..4ac551a0 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 4bff2aee..121c7e9e 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 1ac62da3..9d318170 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index fbc3ada2..948a0e64 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 76576689..a6acaf9a 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index f2558285..eda0510e 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 4f977482..6afd3a87 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 363d5a1e..a4dd40d8 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index aefd734c..68bd730c 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 4e8fadf8..ef8530b8 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.21", + "version": "0.2.22", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index 2f5fab51..ef41f695 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 6a363d42..44047517 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index d7b5d596..13ad50dc 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 690ac6ff..147144b2 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index 192a19ac..7253ea55 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.21", + "version": "0.2.22", "private": true, "type": "module", "main": "src/index.ts",