diff --git a/frontend/components/account/__tests__/paymentShell.test.ts b/frontend/components/account/__tests__/paymentShell.test.ts index 11207c9d..df054e90 100644 --- a/frontend/components/account/__tests__/paymentShell.test.ts +++ b/frontend/components/account/__tests__/paymentShell.test.ts @@ -343,7 +343,7 @@ export function runTests() { assert( backendAuthSource.includes("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") && backendAuthSource.includes("headers.set(FORWARDED_SUPABASE_EMAIL_HEADER") && - backendAuthSource.indexOf("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") > + backendAuthSource.lastIndexOf("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER") > backendAuthSource.indexOf("const sessionUser = session?.user"), "backend proxy must forward Supabase session user id/email with the backend token so Python can skip duplicate /auth/v1/user validation", ); diff --git a/frontend/components/ops/__tests__/backendAuthHeaders.test.ts b/frontend/components/ops/__tests__/backendAuthHeaders.test.ts index 2c5fe3b5..42c49b18 100644 --- a/frontend/components/ops/__tests__/backendAuthHeaders.test.ts +++ b/frontend/components/ops/__tests__/backendAuthHeaders.test.ts @@ -37,4 +37,17 @@ export function runTests() { helperSource.includes('result.set("Content-Type", "application/json")'), "backend auth must expose a safe JSON header builder", ); + + assert( + helperSource.includes("getVerifiedBearerIdentity") && + helperSource.includes("/auth/v1/user") && + helperSource.includes("apikey: anonKey") && + helperSource.includes("incomingAuth") && + helperSource.includes("const identity = await getVerifiedBearerIdentity(incomingAuth)") && + helperSource.includes("headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, identity.userId)") && + helperSource.includes("headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, identity.email)") && + helperSource.includes("authUserId: identity?.userId || null") && + helperSource.includes("authEmail: identity?.email || null"), + "backend auth helper must verify incoming Supabase bearer tokens and forward trusted user identity headers to backend services", + ); } diff --git a/frontend/lib/backend-auth.ts b/frontend/lib/backend-auth.ts index 96752bc3..7f35a427 100644 --- a/frontend/lib/backend-auth.ts +++ b/frontend/lib/backend-auth.ts @@ -18,6 +18,11 @@ type HeaderBuildOptions = { includeSupabaseIdentity?: boolean; }; +type VerifiedBearerIdentity = { + email: string; + userId: string; +}; + function extractBearerToken(headerValue: string | null) { if (!headerValue) return ""; const parts = headerValue.trim().split(/\s+/); @@ -27,6 +32,40 @@ function extractBearerToken(headerValue: string | null) { return ""; } +async function getVerifiedBearerIdentity( + accessToken: string, +): Promise { + const token = String(accessToken || "").trim(); + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim(); + const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY?.trim(); + if (!token || !supabaseUrl || !anonKey) return null; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 4000); + try { + const res = await fetch(`${supabaseUrl.replace(/\/+$/, "")}/auth/v1/user`, { + cache: "no-store", + headers: { + apikey: anonKey, + Authorization: `Bearer ${token}`, + }, + signal: controller.signal, + }); + if (!res.ok) return null; + const user = await res.json(); + const userId = String(user?.id || "").trim(); + if (!userId) return null; + return { + email: String(user?.email || "").trim(), + userId, + }; + } catch { + return null; + } finally { + clearTimeout(timeoutId); + } +} + function hasSupabaseSessionCookie(request: NextRequest) { return request.cookies.getAll().some((cookie) => { const name = cookie.name.toLowerCase(); @@ -54,11 +93,18 @@ export async function buildBackendRequestHeaders( const incomingAuth = extractBearerToken(request.headers.get("authorization")); if (incomingAuth) { headers.set("Authorization", `Bearer ${incomingAuth}`); + const identity = await getVerifiedBearerIdentity(incomingAuth); + if (identity?.userId) { + headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, identity.userId); + if (identity.email) { + headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, identity.email); + } + } return { headers, response: null, - authUserId: null, - authEmail: null, + authUserId: identity?.userId || null, + authEmail: identity?.email || null, hasBearerAuth: true, }; }