fix: preserve original host through forward auth
This commit is contained in:
@@ -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");
|
||||
```
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user