Skip to content

AUTH-11 Implement Supabase SSR clients, OTP helpers, and auth callback - #3

Open
loganravin4 wants to merge 4 commits into
mainfrom
auth-11
Open

AUTH-11 Implement Supabase SSR clients, OTP helpers, and auth callback#3
loganravin4 wants to merge 4 commits into
mainfrom
auth-11

Conversation

@loganravin4

Copy link
Copy Markdown
Collaborator

Add server-side createServerSupabaseClient (cookies + @supabase/ssr), service-role
admin client, sendOtp / verifyOtp / getAuthUser, root middleware session refresh,
and PKCE exchange on /auth/callback.

@loganravin4
loganravin4 requested a review from b-at-neu March 30, 2026 14:15

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Just a few questions and minor suggestions. Also remember to prefix your commits with the linear tag!!

Comment thread src/app/auth/callback/route.ts Outdated
Comment thread src/app/auth/callback/route.ts Outdated
Comment thread src/lib/supabase/admin.ts
Comment thread src/lib/supabase/env.ts Outdated
Comment thread src/lib/supabase/env.ts Outdated
Comment thread src/lib/supabase/middleware.ts Outdated
Comment thread src/lib/supabase/otp.ts
Comment thread src/middleware.ts Outdated
@loganravin4 loganravin4 self-assigned this Apr 13, 2026
@loganravin4
loganravin4 requested a review from b-at-neu April 13, 2026 04:54
@pataniaeli
pataniaeli dismissed b-at-neu’s stale review August 2, 2026 22:23

User elevated to EVP

@pataniaeli
pataniaeli requested a review from mahikasharma August 2, 2026 22:24

@pataniaeli pataniaeli left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security review (auth/session/secret handling — mandatory pre-merge gate per repo policy).

Must fix before merge:

  • HIGH-1 — session cookies missing httpOnly/secure (server.ts, middleware.ts)
  • HIGH-2 — env.ts missing server-only guard, service-role key reachable from a client component if one ever imports it directly (verified via build: leaks into SSR HTML)
  • MEDIUM-4 — getSafeNextPath doesn't block backslash-based open-redirect payloads despite its docstring; not exploitable at today's single call site but exported for reuse by future adapters

Track for follow-up (not necessarily blocking this PR):

  • HIGH-3 — no RLS found on Session/Project tables (plaintext token/apiKey) in the Prisma migrations; needs confirmation against the live Supabase project before this holds real traffic
  • MEDIUM-5 — middleware matcher excludes any path ending in an image extension, unanchored to route structure — a bypass shape once this middleware does real authorization
  • MEDIUM-6 — sendOtp's data option writes to user-editable user_metadata/JWT claims; fine today (unused) but a trap for future authorization-relevant fields
  • MEDIUM-7 — no rate limiting on /auth/callback or OTP verify once verifyOtp gets a caller

Verified safe: service-role key never appears in any client bundle; admin.ts is tree-shaken out of the Edge middleware chunk; cookie get/set ordering matches Supabase's documented SSR pattern; CRLF/header injection via next is blocked by Next's URL parsing; .env.example has no real secrets; PKCE makes login-CSRF via a stolen code fail closed.

export async function createServerSupabaseClient() {
const cookieStore = await cookies();

return createServerClient(getSupabaseUrl(), getSupabaseAnonKey(), {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — session cookies missing httpOnly/secure

createServerClient() is called with only { cookies: {...} } — no cookieOptions. @supabase/ssr's DEFAULT_COOKIE_OPTIONS is { httpOnly: false, sameSite: "lax" } with no secure, so the sb-<ref>-auth-token cookies (access + refresh token) are written readable by JS and over plaintext HTTP, with a 400-day lifetime.

Any XSS on this origin can exfiltrate the refresh token and mint access tokens for all 6 downstream projects for over a year. The docstring above claims "HTTP-only cookies" — currently false.

Fix: pass cookieOptions: { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/" }.

request,
});

const supabase = createServerClient(getSupabaseUrl(), getSupabaseAnonKey(), {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — same missing cookieOptions as server.ts

Same issue as server.ts: no cookieOptions passed to createServerClient(), so session cookies default to httpOnly: false with no secure. Fix both call sites together.

Comment thread src/lib/supabase/env.ts
@@ -0,0 +1,28 @@
/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — missing server-only guard

Every other module in src/lib/supabase/ (admin.ts, otp.ts, server.ts) starts with import "server-only";. This file doesn't, even though it exports getSupabaseServiceRoleKey().

Verified with a real build: importing the barrel (./index) from a "use client" component fails closed (Turbopack errors on the transitive server-only import). But importing @/lib/supabase/env directly from a client component builds successfully, and the service-role key gets rendered into SSR HTML output (confirmed with a canary value in .next/server/app/*.html). Not exploited today since nothing does this yet, but it's a one-line mistake away from a full identity-service compromise.

Fix: add import "server-only"; to this file, or move getSupabaseServiceRoleKey into a separately-guarded module and drop it from the shared barrel.

if (trimmed === "") {
return fallback;
}
if (trimmed.includes("://") || trimmed.startsWith("//")) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM — doesn't actually block open redirects, despite the docstring

trimmed.includes("://") || trimmed.startsWith("//") misses backslash-based authority injection. Under WHATWG URL parsing, a backslash behaves like a forward slash for special schemes, so a value like /\evil.com (or \evil.com, which this function rewrites to /\evil.com) resolves with host = evil.com when passed to redirect()/NextResponse.redirect() without an origin prefix.

Not exploitable today — auth/callback/route.ts always prefixes ${origin} before this path, which pins the authority. But this function is exported from the shared barrel specifically for reuse by the other 5 project adapters, and its docstring promises same-origin-only redirects — the first adapter author who calls redirect(getSafeNextPath(x)) without an origin prefix gets a working open redirect on the login flow.

Fix: reject backslashes and control characters before the existing checks, or validate positively via new URL(trimmed, "https://placeholder.invalid") and require the resolved origin to match the placeholder.

pataniaeli added a commit that referenced this pull request Aug 16, 2026
…llision

Addresses the "must-fix regardless of sequencing" findings from the AUTH-7
security review:

- updateUser now destructures isAdmin explicitly instead of spreading the
  caller-supplied data object into Prisma, closing a mass-assignment path
  that could otherwise write unexpected fields (e.g. supabaseUserId).
- getUsers now builds an explicit { isAdmin } where clause instead of
  passing the caller's filter object straight to Prisma's query builder.
- getUser no longer returns raw Supabase Admin API error text to the
  caller; details are logged server-side instead.
- deleteUser now runs the Prisma transaction before deleting the Supabase
  auth identity, so a failed transaction can't leave live session/
  membership rows pointing at an identity that no longer resolves.
- Renamed src/lib/supabase.ts to src/lib/supabase-admin.ts to avoid
  colliding with the src/lib/supabase/ directory added by #3 (auth-11),
  and added autoRefreshToken/persistSession: false plus clear env-var
  errors to match that module's admin client pattern.

Caller-identity/authorization checks are intentionally deferred — this
codebase has no merged session mechanism yet (the only one, #3, is still
open) — and are tracked via a TODO in users.ts pending that follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants