You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
require a Supabase bearer token for chat, title, billing, and private conversation routes
derive user_id and email from the verified token instead of trusting request bodies or query strings
scope conversation CRUD/share/report and billing usage to the authenticated user
send the Supabase access token from the frontend and stop sending client-supplied user IDs
Security impact
Closes the audit findings where callers could impersonate users, bypass billing with anonymous chat requests, and access or mutate other users' conversation records.
Reviewed the diff and a couple of neighbouring files. The direction is clearly right and I'd merge this after the blocker below is fixed — without it, production (and local dev without NEXT_PUBLIC_BACKEND_URL) will 401 on every authenticated call.
🔴 Blocker — Next.js proxy drops the Authorization header
frontend/src/app/api/proxy/[...slug]/route.ts line 59 hardcodes the forwarded headers:
In frontend/src/utils/backend.ts, getBackendBase() falls back to /api/proxy for any hostname that doesn't match the Vercel preview regex. That means:
Preview (policyengine-uk-chat-git-*-policy-engine.vercel.app) → direct to Modal, Authorization passes through, tests green. ✅
Production / any host not matching the regex / local dev without NEXT_PUBLIC_BACKEND_URL → /api/proxy, proxy strips Authorization, backend returns 401 on every protected route. ❌
That's why this PR's preview checks pass despite the bug — the preview URL bypasses the proxy. Suggested fix:
routes/auth.py:require_user calls get_supabase().auth.get_user(token) — the supabase-py auth client uses a sync httpx.Client. chat_message is async def, so every request now blocks the event loop for the round-trip to Supabase (~50–200 ms) before any chat work starts. Under concurrency this serialises the whole streaming endpoint.
Options, roughly cheapest first:
await asyncio.to_thread(get_supabase().auth.get_user, token) inside require_user (make it async).
Verify the Supabase-issued JWT locally with PyJWT + SUPABASE_JWT_SECRET — avoids the network call entirely and is what most production backends do. Happy to scope this as a follow-up since it's an optimisation, not a correctness bug.
If existing.user_id is None (a pre-PR anonymous row), the check short-circuits and the row gets reassigned to whoever authenticates with that session_id first. share/report/delete/get all use the stricter row.user_id != current_user.id (None → 403). Risk is low — session_id is a UUID — but the asymmetry is surprising, and a legacy row's original anonymous author can't complain. I'd either:
apply the same strict check everywhere, or
if the intent is "first authenticated saver claims their own legacy thread", leave a comment explaining the migration intent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
user_idand email from the verified token instead of trusting request bodies or query stringsSecurity impact
Closes the audit findings where callers could impersonate users, bypass billing with anonymous chat requests, and access or mutate other users' conversation records.
Validation
python -m py_compile routes/auth.py routes/supabase_client.py routes/billing.py routes/chatbot.py routes/conversations.py tests/test_api.pypydantic_aiin the checkout environment.