Auth Bug Fix: poller_secret in URL + Logout Cookie Not Cleared
TLDR: The VS Code polling secret was appearing in server access logs as a URL query param. And clicking Sign Out left the HttpOnly session cookie valid for up to an hour. Both fixed with single-line-to-small-function changes.
Bug 1: poller_secret in URL Query Parameter
Symptom
The VS Code extension polls /api/auth/vscode-poll?state=X&secret=Y every 5 seconds during a sign-in handshake. That URL is written to Cloudflare access logs on every poll request. Anyone with access to those logs — including any future SIEM, log aggregator, or analytics tool — had live polling secrets for the duration of every VS Code sign-in.
Root Cause
The primary proxy (functions/auth/vscode-poll.js) was already correct: it reads the secret from Authorization: Bearer <secret> and forwards it the same way. But the backup proxy (functions/api/auth/vscode-poll.js) was copied from an earlier draft before the header-based pattern was adopted. It used url.searchParams.get('secret') and forwarded the secret as a URL query parameter to the Supabase edge function.
// BEFORE — wrong: secret in URL
const secret = url.searchParams.get('secret');
target.searchParams.set('secret', secret);
The poller_secret field in the session row is the only protection against token hijacking on the poll endpoint. Knowing the state UUID alone is not enough — you also need the poller_secret to retrieve the token. Having the secret in logs breaks that protection.
Fix
// AFTER — correct: secret in Authorization header
const secret = request.headers.get('Authorization')?.replace(/^Bearer\s+/i, '').trim();
// ...
return fetch(target.toString(), {
headers: {
Authorization: Bearer ${secret},
'x-supabase-anon-key': env.SUPABASE_ANON_KEY,
},
});
Headers are not written to standard access logs. The edge function receives the secret via req.headers.get('authorization') — same as before, just now routed correctly.
Guard
If a new proxy function is created for this endpoint, always grep the resulting code for searchParams.set.*secret before deploying.
Lesson
Secondary or backup endpoints must go through the same security review as primary ones. The primary file was correct; the backup was not. "We fixed the main path" is not the same as "we fixed all paths."
Bug 2: Logout Did Not Clear the Server-Side Cookie
Symptom
After clicking Sign Out, the user appeared logged out (auth chip showed Sign In, localStorage was empty). But /api/me with the old gx_session cookie still returned 200 with the user's email and tier. The session was alive server-side for up to one hour.
Root Cause
_signOut() in os-shell.js did three things:
- Called
_sb.auth.signOut()— clears Supabase client state. - Iterated localStorage and deleted all
sb-*-auth-tokenkeys. - Reset
_authUser = nulland re-rendered the UI.
POST /api/auth/session { action: 'clear' }.
The gx_session HttpOnly cookie is set by the server. JavaScript cannot read or delete it directly (HttpOnly is enforced by the browser). The only way to clear it is to ask the server to set Max-Age=0 — which is exactly what the clear action on /api/auth/session does.
Without that call, the cookie remained valid. Any request to /api/me with the old cookie would authenticate successfully.
Fix
function _signOut() {
// Clear the HttpOnly gx_session cookie server-side first.
fetch('/api/auth/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'clear' }),
}).catch(() => {});
if (_sb) _sb.auth.signOut().catch(() => {}); // ... rest of localStorage cleanup }
The server responds with Set-Cookie: gx_session=; Max-Age=0 which tells the browser to expire the cookie immediately.
Guard
Any future sign-out path — in any auth surface — must call the server-side clear before doing anything else. Test by: sign in, open DevTools → Application → Cookies, click Sign Out, verify gx_session disappears from the cookie list.
Lesson
HttpOnly cookies are invisible to JavaScript by design. That invisibility is the security feature. But it means you cannot rely on client-side cleanup to clear them — every logout path must make a server round-trip. The architecture was correct; the implementation forgot the final step.