Uh oh!
There was an error while loading. Please reload this page.
fix(auth): stop the /api proxy shadowing the session BFF - #578
fix(auth): stop the /api proxy shadowing the session BFF#578Darkest-Teddy wants to merge 1 commit into
Conversation
Real Google sign-in got all the way home and died on the last request.
The OAuth callback lands on /auth/callback with a valid handoff token, and
the page exchanges it at POST /api/auth/session — the app's own route
handler, and the only thing that can mint `sapling_session`. That request
404'd, so the popup broadcast `signin_failed`, the modal showed "Sign-in
failed. Please try again.", and no session cookie was ever set. Observed
end to end in the dev server log:
backend GET /api/auth/google/callback -> 307 (consent OK, hd=bu.edu)
frontend GET /auth/callback?...&is_approved=true&auth_token=... 200
frontend POST /api/auth/session 404
next.config.ts returned its rewrites as a bare ARRAY, which means the
`afterFiles` phase. Next 16 runs that phase before app-router route
handlers, so `/api/:path*` swallowed the app's own /api routes and proxied
them to a backend that has no such endpoint. Proved with a scratch route:
GET /api/__probe came back from uvicorn, never from Next.
The `{ source: "/api/auth/session", destination: "/api/auth/session" }`
self-rewrite above it was the attempted exemption. A rewrite whose
destination is its own source does not re-enter route matching — it
resolves to not-found — so it 404'd exactly like the proxy it was meant to
dodge.
Fix: move the proxy to `fallback`, the phase that runs only after
filesystem and dynamic routes have all missed (rewrites doc, step 8).
Anything the app serves itself wins; everything else proxies. That holds
for any future BFF route without a per-path exemption, and the self-rewrite
goes away.
Verified on both dev and a production build: POST /api/auth/session returns
200 with `set-cookie: sapling_session=...`, /api/health and /api/auth/me
still proxy to the backend, and a browser driven through the real popup
flow — modal, Google consent, callback — lands on /dashboard with the
cookie set and every authed call 200.
Guards, because this failed silently in a place no test looked:
- next.config.test.ts fails if the proxy returns to array/afterFiles
form, lands in beforeFiles/afterFiles, or if a self-rewrite reappears.
vitest.config.ts's include gains root-level *.test.ts to run it.
- e2e/auth-session.spec.ts gains a journey that mints a handoff token and
asserts the BFF answers 200 with a cookie. The existing journey minted
its cookie through the backend directly, which is why nothing caught
this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>This pull request has been ignored for the connected project Preview Branches by Supabase. |
Deploying with |
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs | frontend-staging | de950e7 | Commit Preview URL Branch Preview URL | Aug 23 2026, 05:19 AM |
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Real Google sign-in currently fails on
mainat the very last step.The bug
The OAuth callback lands on
/auth/callbackwith a valid handoff token, and the page exchanges it atPOST /api/auth/session— the app's own route handler, and the only thing that can mintsapling_session. That request 404s. The popup broadcastssignin_failed, the modal says "Sign-in failed. Please try again.", and no session cookie is ever set.Caught end to end in a local dev log, signing in with a real @bu.edu account:
Cause
next.config.tsreturned its rewrites as a bare array, which means theafterFilesphase. Next 16 runs that phase before app-router route handlers, so/api/:path*swallowed the app's own/apiroutes and proxied them to a backend that has no such endpoint. Proved with a scratch route —GET /api/__probecame back withserver: uvicorn, never from Next.The
{ source: "/api/auth/session", destination: "/api/auth/session" }self-rewrite sitting above it was the attempted exemption. A rewrite whose destination is its own source doesn't re-enter route matching — it resolves to not-found — so it 404'd exactly like the proxy it was meant to dodge.Fix
Move the proxy to
fallback, the phase that runs only after filesystem and dynamic routes have all missed (per Next's bundled rewrites doc, step 8). Anything the app serves itself wins; everything else proxies. That holds for any future BFF route without a per-path exemption, and the self-rewrite goes away.Verification
On both the dev server and a production build:
POST /api/auth/session→ 200 withset-cookie: sapling_session=…(was 404)/api/health→ 200 and/api/auth/me→ 401, i.e. still proxied to the backend/dashboardwith the cookie set, and every authed API call returns 200/dashboardwith the cookie is admitted by middleware; without it, redirected to sign-inGuards
This failed silently in a place no test looked, so it gets two:
next.config.test.ts(new) fails if the proxy returns to array/afterFilesform, lands inbeforeFiles/afterFiles, or if a self-rewrite reappears.vitest.config.ts'sincludegains root-level*.test.tsso it runs.e2e/auth-session.spec.tsgains a journey that mints a handoff token via/api/auth/test-loginand asserts the BFF answers 200 with asapling_sessioncookie. The existing journey minted its cookie through the backend directly — which is exactly why nothing caught this.Note this also affects the deployed build: the same config ships to Cloudflare, so production would break the same way once it is built from a Next 16 tree.
🤖 Generated with Claude Code