diff --git a/apps/cursor/src/app/auth/callback/route.ts b/apps/cursor/src/app/auth/callback/route.ts index c820a3c9..207d72c5 100644 --- a/apps/cursor/src/app/auth/callback/route.ts +++ b/apps/cursor/src/app/auth/callback/route.ts @@ -1,10 +1,23 @@ import { NextResponse } from "next/server"; import { createClient } from "@/utils/supabase/server"; +// `next` comes from the query string and is appended to a base URL, so values +// like `@evil.example` or `//evil.example` would otherwise leave the site. +function safeNextPath(next: string | null, origin: string): string { + if (!next) return "/"; + try { + const url = new URL(next, origin); + if (url.origin !== origin) return "/"; + return `${url.pathname}${url.search}${url.hash}`; + } catch { + return "/"; + } +} + export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); - const next = searchParams.get("next") ?? "/"; + const next = safeNextPath(searchParams.get("next"), origin); if (code) { const supabase = await createClient(); diff --git a/apps/cursor/src/utils/supabase/auth.ts b/apps/cursor/src/utils/supabase/auth.ts index 77f4b304..4c933fa1 100644 --- a/apps/cursor/src/utils/supabase/auth.ts +++ b/apps/cursor/src/utils/supabase/auth.ts @@ -1,9 +1,38 @@ import { createClient } from "./server"; -export async function getSession() { +export type VerifiedSession = { + user: { + id: string; + email: string | undefined; + user_metadata: Record; + }; +}; + +/** + * The signed-in user, read from the access token's verified claims. + * + * `supabase.auth.getSession()` returns whatever the session cookie contains + * without checking the token signature, so its `user` must never back an + * authorization decision on the server. `getClaims()` verifies the JWT + * (against the project's JWKS, or via the Auth server for symmetric keys) + * before we trust `sub`. + */ +export async function getSession(): Promise { const supabase = await createClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - return session; + // getClaims throws, rather than returning an error, for an expired token. + const result = await supabase.auth.getClaims().catch(() => null); + + if (!result || result.error || !result.data?.claims.sub) { + return null; + } + + const { claims } = result.data; + + return { + user: { + id: claims.sub, + email: claims.email, + user_metadata: claims.user_metadata ?? {}, + }, + }; }