diff --git a/frontend/app/api/payments/intents/[intentId]/confirm/route.ts b/frontend/app/api/payments/intents/[intentId]/confirm/route.ts index e686a564..55a9aab6 100644 --- a/frontend/app/api/payments/intents/[intentId]/confirm/route.ts +++ b/frontend/app/api/payments/intents/[intentId]/confirm/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -25,7 +25,7 @@ export async function POST( try { const body = await req.json(); const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/app/api/payments/intents/[intentId]/submit/route.ts b/frontend/app/api/payments/intents/[intentId]/submit/route.ts index 624ff527..3f47ef34 100644 --- a/frontend/app/api/payments/intents/[intentId]/submit/route.ts +++ b/frontend/app/api/payments/intents/[intentId]/submit/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -25,7 +25,7 @@ export async function POST( try { const body = await req.json(); const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/app/api/payments/intents/[intentId]/validate/route.ts b/frontend/app/api/payments/intents/[intentId]/validate/route.ts index ca66bff0..68b86dab 100644 --- a/frontend/app/api/payments/intents/[intentId]/validate/route.ts +++ b/frontend/app/api/payments/intents/[intentId]/validate/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -25,7 +25,7 @@ export async function POST( try { const body = await req.json(); const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/app/api/payments/intents/route.ts b/frontend/app/api/payments/intents/route.ts index 48b0d7f0..5a7d955d 100644 --- a/frontend/app/api/payments/intents/route.ts +++ b/frontend/app/api/payments/intents/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -36,7 +36,7 @@ export async function POST(req: NextRequest) { try { const body = await req.json(); const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/app/api/payments/reconcile-latest/route.ts b/frontend/app/api/payments/reconcile-latest/route.ts index 9d62e36f..d2e78f1d 100644 --- a/frontend/app/api/payments/reconcile-latest/route.ts +++ b/frontend/app/api/payments/reconcile-latest/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse } from "@/lib/api-proxy"; @@ -18,7 +18,7 @@ export async function POST(req: NextRequest) { try { const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const res = await fetch(`${API_BASE}/api/payments/reconcile-latest`, { method: "POST", diff --git a/frontend/app/api/payments/wallets/challenge/route.ts b/frontend/app/api/payments/wallets/challenge/route.ts index a993d792..6db5ec76 100644 --- a/frontend/app/api/payments/wallets/challenge/route.ts +++ b/frontend/app/api/payments/wallets/challenge/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -21,7 +21,7 @@ export async function POST(req: NextRequest) { try { const body = await req.json(); const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/app/api/payments/wallets/route.ts b/frontend/app/api/payments/wallets/route.ts index f6696760..0dfee0b4 100644 --- a/frontend/app/api/payments/wallets/route.ts +++ b/frontend/app/api/payments/wallets/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -45,7 +45,7 @@ export async function DELETE(req: NextRequest) { } try { const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/app/api/payments/wallets/verify/route.ts b/frontend/app/api/payments/wallets/verify/route.ts index 49f93e75..f691af27 100644 --- a/frontend/app/api/payments/wallets/verify/route.ts +++ b/frontend/app/api/payments/wallets/verify/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { applyAuthResponseCookies, buildBackendRequestHeaders, - requireBackendAuthUser, + requireBackendPaymentAuth, } from "@/lib/backend-auth"; import { buildProxyExceptionResponse, @@ -21,7 +21,7 @@ export async function POST(req: NextRequest) { try { const body = await req.json(); const auth = await buildBackendRequestHeaders(req); - const authError = requireBackendAuthUser(auth); + const authError = requireBackendPaymentAuth(auth); if (authError) return authError; const proxiedHeaders = new Headers(auth.headers); proxiedHeaders.set("Content-Type", "application/json"); diff --git a/frontend/components/account/__tests__/paymentSecurity.test.ts b/frontend/components/account/__tests__/paymentSecurity.test.ts index 1daf38ac..7fb13a57 100644 --- a/frontend/components/account/__tests__/paymentSecurity.test.ts +++ b/frontend/components/account/__tests__/paymentSecurity.test.ts @@ -100,6 +100,11 @@ export function runTests() { backendAuthSource.includes("requireBackendAuthUser"), "backend auth helper must expose a real-user requirement for payment mutations", ); + assert( + backendAuthSource.includes("requireBackendPaymentAuth") && + backendAuthSource.includes("hasBearerAuth"), + "backend auth helper must allow bearer-backed payment mutations to reach the backend verifier", + ); const middlewareSource = fs.readFileSync(middlewarePath, "utf8"); assert( @@ -205,8 +210,9 @@ export function runTests() { for (const route of paymentRoutes) { const routeSource = fs.readFileSync(path.join(projectRoot, route), "utf8"); assert( - routeSource.includes("requireBackendAuthUser"), - `${route} must reject payment mutations without a real Supabase user`, + routeSource.includes("requireBackendPaymentAuth") && + !routeSource.includes("requireBackendAuthUser(auth)"), + `${route} must allow bearer-backed payment mutations while still rejecting requests with no auth context`, ); } } diff --git a/frontend/lib/backend-auth.ts b/frontend/lib/backend-auth.ts index bb4d7294..40990efd 100644 --- a/frontend/lib/backend-auth.ts +++ b/frontend/lib/backend-auth.ts @@ -11,6 +11,7 @@ export type BackendHeaderBuildResult = { response: NextResponse | null; authUserId?: string | null; authEmail?: string | null; + hasBearerAuth?: boolean; }; type HeaderBuildOptions = { @@ -53,13 +54,25 @@ export async function buildBackendRequestHeaders( const incomingAuth = extractBearerToken(request.headers.get("authorization")); if (incomingAuth) { headers.set("Authorization", `Bearer ${incomingAuth}`); - return { headers, response: null, authUserId: null, authEmail: null }; + return { + headers, + response: null, + authUserId: null, + authEmail: null, + hasBearerAuth: true, + }; } const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false; if (hasSupabaseServerEnv() && includeSupabaseIdentity) { if (!hasSupabaseSessionCookie(request)) { - return { headers, response: null, authUserId: null, authEmail: null }; + return { + headers, + response: null, + authUserId: null, + authEmail: null, + hasBearerAuth: false, + }; } const passthroughResponse = new NextResponse(null, { status: 200 }); @@ -81,10 +94,22 @@ export async function buildBackendRequestHeaders( if (forwardedEmail) { headers.set(FORWARDED_SUPABASE_EMAIL_HEADER, forwardedEmail); } - return { headers, response: passthroughResponse, authUserId: forwardedUserId || null, authEmail: forwardedEmail || null }; + return { + headers, + response: passthroughResponse, + authUserId: forwardedUserId || null, + authEmail: forwardedEmail || null, + hasBearerAuth: Boolean(accessToken), + }; } - return { headers, response: null, authUserId: null, authEmail: null }; + return { + headers, + response: null, + authUserId: null, + authEmail: null, + hasBearerAuth: false, + }; } export function applyAuthResponseCookies( @@ -110,3 +135,8 @@ export function requireBackendAuthUser(auth: BackendHeaderBuildResult) { auth.response, ); } + +export function requireBackendPaymentAuth(auth: BackendHeaderBuildResult) { + if (auth.authUserId || auth.hasBearerAuth) return null; + return requireBackendAuthUser(auth); +}