From b6b53b702901890fca3c62824663a64f5c30ae3f Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:21:45 +0200 Subject: [PATCH] Move forward auth redirect URI from query string to HttpOnly cookie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ?rd= query parameter in the Caddy→portal redirect with a _cpm_rd HttpOnly cookie (Secure, SameSite=Lax, Path=/portal, 10min TTL). The portal server component reads and immediately deletes the cookie, then processes it through the existing validation and redirect intent flow. This removes the redirect URI from the browser URL bar while maintaining all existing security properties (domain validation, server-side storage, one-time opaque rid). Co-Authored-By: Claude Opus 4.6 (1M context) --- app/(auth)/portal/page.tsx | 15 +++++++++++---- src/lib/caddy.ts | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/(auth)/portal/page.tsx b/app/(auth)/portal/page.tsx index 7720d68f..ac73488b 100644 --- a/app/(auth)/portal/page.tsx +++ b/app/(auth)/portal/page.tsx @@ -1,20 +1,27 @@ +import { cookies } from "next/headers"; import { auth } from "@/src/lib/auth"; import { getEnabledOAuthProviders } from "@/src/lib/config"; import { isForwardAuthDomain, createRedirectIntent } from "@/src/lib/models/forward-auth"; import PortalLoginForm from "./PortalLoginForm"; interface PortalPageProps { - searchParams: Promise<{ rd?: string; rid?: string }>; + searchParams: Promise<{ rid?: string }>; } export default async function PortalPage({ searchParams }: PortalPageProps) { const params = await searchParams; - const redirectUri = params.rd ?? ""; // After OAuth callback, the portal is loaded with ?rid= (the opaque ID we created earlier) const existingRid = params.rid ?? ""; + // Read redirect URI from HttpOnly cookie set by Caddy, then clear it + const cookieStore = await cookies(); + const redirectUri = cookieStore.get("_cpm_rd")?.value ?? ""; + if (redirectUri) { + cookieStore.delete("_cpm_rd"); + } + // Two entry modes: - // 1. Fresh from Caddy redirect: ?rd= → validate, store server-side, create rid + // 1. Fresh from Caddy redirect: _cpm_rd cookie → validate, store server-side, create rid // 2. Returning from OAuth: ?rid= → reuse the existing rid (redirect already stored) let targetDomain = ""; let rid = existingRid; @@ -27,7 +34,7 @@ export default async function PortalPage({ searchParams }: PortalPageProps) { ) { targetDomain = parsed.hostname; // Store the redirect URI server-side. The client only gets an opaque ID, - // so a tampered ?rd= parameter cannot influence the final redirect target. + // so a tampered cookie cannot influence the final redirect target. rid = await createRedirectIntent(redirectUri); } } catch { diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 07403a5a..431be694 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -1185,7 +1185,10 @@ async function buildProxyRoutes( status_code: 302, headers: { Location: [ - `${config.baseUrl}/portal?rd={http.request.scheme}://{http.request.host}{http.request.uri}` + `${config.baseUrl}/portal` + ], + "Set-Cookie": [ + `_cpm_rd={http.request.scheme}://{http.request.host}{http.request.uri}; Path=/portal; HttpOnly; Secure; SameSite=Lax; Max-Age=600` ] } }