From bb62a6517461ece8e583a40d2b7e18bf172ab231 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 9 Sep 2026 11:26:57 +0100 Subject: [PATCH] fix(confidence-engine): preserve external auth callback origin --- app/auth/callback/route.js | 17 +++++++++++++++- tests/auth-boundary.test.js | 40 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/app/auth/callback/route.js b/app/auth/callback/route.js index 33925fa..bccf391 100644 --- a/app/auth/callback/route.js +++ b/app/auth/callback/route.js @@ -4,7 +4,22 @@ import { NextResponse } from "next/server"; export async function GET(request) { const requestUrl = new URL(request.url); const code = requestUrl.searchParams.get("code"); - const response = NextResponse.redirect(new URL("/", requestUrl.origin)); + + // Derive redirect origin from proxy-forwarded headers when present, + // falling back to the direct request origin for local/direct access. + const forwardedHost = request.headers.get("x-forwarded-host"); + const forwardedProto = request.headers.get("x-forwarded-proto"); + let origin; + + if (forwardedHost && forwardedProto) { + // Nginx Proxy Manager (and similar proxies) set these headers. + // x-forwarded-host may contain host:port or just hostname; use as-is. + origin = `${forwardedProto}://${forwardedHost}`; + } else { + origin = requestUrl.origin; + } + + const response = NextResponse.redirect(new URL("/", origin)); if (code) { const supabase = createServerClient( diff --git a/tests/auth-boundary.test.js b/tests/auth-boundary.test.js index c26c38f..27fa9ee 100644 --- a/tests/auth-boundary.test.js +++ b/tests/auth-boundary.test.js @@ -19,9 +19,47 @@ vi.mock("@/lib/config", () => ({ })); vi.mock("@supabase/ssr", () => ({ - createServerClient: () => ({ auth: { getUser: () => mockGetUser() } }), + createServerClient: () => ({ + auth: { + getUser: () => mockGetUser(), + exchangeCodeForSession: vi.fn().mockResolvedValue(undefined), + }, + }), })); +describe("auth callback redirect origin", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("uses forwarded host/proto for redirect when behind proxy", async () => { + const { GET } = await import("@/app/auth/callback/route.js"); + + const request = new Request("http://0.0.0.0:3000/auth/callback?code=abc123", { + headers: { + "x-forwarded-host": "confidence.rdbcloud.co.uk", + "x-forwarded-proto": "https", + }, + }); + + const response = await GET(request); + + expect(response.status).toBe(307); + expect(response.headers.get("location")).toBe("https://confidence.rdbcloud.co.uk/"); + }); + + it("falls back to request origin when no forwarded headers", async () => { + const { GET } = await import("@/app/auth/callback/route.js"); + + const request = new Request("http://localhost:3000/auth/callback?code=xyz"); + + const response = await GET(request); + + expect(response.status).toBe(307); + expect(response.headers.get("location")).toBe("http://localhost:3000/"); + }); +}); + describe("authenticated product boundary", () => { beforeEach(() => { vi.clearAllMocks();