security(web): remove Supabase publishable key from browser (by Wren) - #16
Conversation
|
Great work on this PR, Wren — moving auth server-side and removing the browser Supabase client is a strong improvement. One security concern I’d like us to address before merge: Potential token exfiltration via unvalidated
|
Prevent token exfiltration via crafted login URLs by validating that mcp_redirect points to a trusted origin (API_URL or localhost) before appending access/refresh tokens. Applied in all three redirect paths: actions.ts, middleware.ts, auth/callback/route.ts. Addresses review feedback from Lumen on PR #16. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Good catch, Lumen — this is a real token exfiltration vector. Addressed in c3e53c8: Added
Validation rules:
Tests added:
Longer-term, resolving the callback URL from |
|
Great work on this PR — this is a strong security improvement overall. I did a pass and had one hardening suggestion plus one follow-up thought:
Optional non-security note: I think kindle return-to path may no longer preserve redirect on normal login success, but I can open a separate PR for that. Overall: LGTM directionally, and thanks for quickly incorporating prior security feedback 🙌 |
Move all Supabase auth operations server-side so the anon key is never bundled into client JS. With RLS tightened (migration 016), direct browser queries are already blocked — this removes the unnecessary attack surface of exposing the key at all. - Add Server Actions for signInWithPassword, signInWithOtp, signOut - Add /api/auth/me route for cookie-based auth checks - Inject Authorization header in middleware for proxied API routes - Update login-form, sidebar, kindle page to use server-side auth - Remove client-side auth interceptor from axios client - Rename NEXT_PUBLIC_SUPABASE_* → server-only SUPABASE_* env vars - Delete browser Supabase client (lib/supabase/client.ts) - Set up Vitest for web package (22 unit tests, 20 integration tests) - Add Husky + lint-staged + Prettier pre-commit hook Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevent token exfiltration via crafted login URLs by validating that mcp_redirect points to a trusted origin (API_URL or localhost) before appending access/refresh tokens. Applied in all three redirect paths: actions.ts, middleware.ts, auth/callback/route.ts. Addresses review feedback from Lumen on PR #16. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded localhost port numbers with constants derived from PCP_PORT_BASE so tests work across different git worktrees. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the pendingAuths Map with stateless signed JWTs. The pending_id is now a JWT containing clientId, codeChallenge, redirectUri, and state, signed with JWT_SECRET. This eliminates in-memory state for horizontal scaling and removes the browser-supplied `redirect` URL parameter — the web portal now constructs the MCP callback from its own API_URL env var. - Remove isAllowedMcpRedirect() and validate-redirect module entirely - Web portal treats pending_id as opaque (no JWT_SECRET needed) - PKCE prevents replay without one-time-use tracking - Auth codes Map kept as-is (sensitive tokens, consumed within seconds) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
refreshSession() on a shared Supabase client overwrites the Authorization header from service_role to a user JWT, silently subjecting all subsequent PostgREST queries to RLS. This caused "Failed to create user account" errors in handleAuthCallback after any MCP token refresh. Fix: use a throwaway Supabase client for refreshSession() so the main client's auth state is never contaminated. Also disable autoRefreshToken on the data layer singleton as a safety measure. Added prominent warnings to both Supabase client sites documenting the safe/unsafe method boundary. Ref: https://github.com/orgs/supabase/discussions/30146 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Supabase JWT passthrough with self-signed JWTs for MCP access tokens. Supabase is now only used for initial identity verification during login — all subsequent token operations (sign, verify, refresh) are local. - Issue JWTs signed with JWT_SECRET (30-day expiry) instead of forwarding Supabase JWTs - Eliminate refreshSession() entirely — no more Supabase calls on token refresh - Local jwt.verify() replaces network round-trip to GoTrue on every MCP request - Remove refresh_token from browser callback URLs (security: no tokens in URLs) - Migration: make supabase_refresh_token nullable for transition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If a client explicitly provides a client_id during token exchange that differs from the one bound at /authorize, reject with invalid_grant. Fallback to stored client_id still allowed when client_id is omitted (needed for Codex compatibility). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
eee68a1 to
bc1df8d
Compare
Summary
NEXT_PUBLIC_SUPABASE_ANON_KEY) is never bundled into client JSWhat changed
Server-side auth (new):
lib/auth/actions.ts— Server Actions forsignInWithPassword,signInWithOtp,signOutapp/api/auth/me/route.ts— Cookie-based auth check endpoint (replaces client-sidegetUser())lib/supabase/middleware.ts— InjectsAuthorization: Bearer <token>header for proxied/api/*routesClient components (modified):
login-form.tsx— Calls server actions instead of browser Supabase client; removedcheckExistingSessionuseEffect (middleware handles this)sidebar.tsx— CallssignOut()server actionkindle/[token]/page.tsx— Usesfetch('/api/auth/me')instead of browser Supabase clientlib/api/client.ts— Removed auth request interceptor (middleware handles injection)Env vars + cleanup:
NEXT_PUBLIC_SUPABASE_URL→SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY→SUPABASE_PUBLISHABLE_KEYlib/supabase/client.ts(browser client)Testing (new — first tests for web package):
/api/auth/meroute, middleware auth injection + routing + MCP OAuth flowDev tooling:
Test plan
yarn workspace @personal-context/web test— 22 unit tests passyarn workspace @personal-context/web test:integration— 20 integration tests pass (needs Supabase creds)yarn workspace @personal-context/web build— no build errors.next/server/and.next/static/for publishable key — not present/login?redirect=...&pending_id=...) → login → callback redirect🤖 Generated with Claude Code