slopus/happy · error

State mismatch. Possible CSRF attack

Error message

State mismatch. Possible CSRF attack

What it means

In the Gemini/Google OAuth callback server, the received `state` query parameter is compared with the state generated when authenticateGemini() built the authorization URL. A mismatch is treated as a possible CSRF attack: the browser gets 'State mismatch. Possible CSRF attack' (HTTP 400) and the promise is rejected with 'Invalid state parameter'. This protects the local redirect endpoint from forged or replayed callbacks.

Source

Thrown at packages/happy-cli/src/commands/connect/authenticateGemini.ts:144

            const url = new URL(req.url!, `http://localhost:${port}`);
            
            if (url.pathname === '/oauth2callback') {
                const code = url.searchParams.get('code');
                const receivedState = url.searchParams.get('state');
                const error = url.searchParams.get('error');
                
                if (error) {
                    res.writeHead(302, { 
                        'Location': 'https://developers.google.com/gemini-code-assist/auth_failure_gemini' 
                    });
                    res.end();
                    server.close();
                    reject(new Error(`Authentication error: ${error}`));
                    return;
                }
                
                if (receivedState !== state) {
                    res.writeHead(400);
                    res.end('State mismatch. Possible CSRF attack');
                    server.close();
                    reject(new Error('Invalid state parameter'));
                    return;
                }
                
                if (!code) {
                    res.writeHead(400);
                    res.end('No authorization code received');
                    server.close();
                    reject(new Error('No authorization code received'));
                    return;
                }
                
                try {
                    // Exchange code for tokens
                    const tokens = await exchangeCodeForTokens(code, verifier, port);
                    

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Close all old Google auth tabs/windows, then rerun `happy` connect so the state in the URL matches the running callback server.
  2. Make sure only one connect flow is running at a time (don't run connect in two terminals concurrently).
  3. Open the exact auth URL the CLI just printed — do not reuse an earlier URL whose state has expired.
  4. If a rogue process is squatting on port 54545, kill it or let the CLI pick a fresh port, then retry.

Example fix

// before: completing an old consent tab
// old tab: http://localhost:54545/oauth2callback?code=...&state=<old-state>  → rejected
// after: close old tabs, rerun
code: await authenticateGemini(); // fresh state matches fresh callback
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the callback port is free and no other auth flow is running
const portInUse = !(await isPortFree(54545));
if (portInUse) console.warn('Port 54545 busy — another Gemini auth flow may be running; close it before reconnecting');

Try / catch

try {
  const tokens = await authenticateGemini();
} catch (err) {
  if (err instanceof Error && /Invalid state parameter|CSRF/i.test(err.message)) {
    console.error('State mismatch — an old or forged callback reached the server. Close old Google auth tabs and rerun connect.');
  } else throw err;
}

Prevention

When it happens

Trigger: http://localhost:<port>/oauth2callback is hit with a `state` that differs from this run's generated state — stale tab from a prior auth attempt completing the callback, two concurrent authenticateGemini() runs racing on the same port, or the state param dropped/rewritten in transit.

Common situations: User re-runs Gemini connect while an old Google consent tab is still open and completes the stale one; Google session already authorized so Google redirects immediately with an old cached flow; a bookmarked callback URL from a previous session; port 54545 occupied by another happy process handling its own state.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/8a36d9f56b560af9. Report an issue: GitHub.