{"record":{"id":"6448bbdd46bb1536","repo":"slopus/happy","slug":"invalid-state-parameter","errorCode":null,"errorMessage":"Invalid state parameter","messagePattern":"Invalid state parameter","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/happy-cli/src/commands/connect/authenticateClaude.ts","lineNumber":147,"sourceCode":"\n/**\n * Start local server to handle OAuth callback\n */\nasync function startCallbackServer(\n    state: string,\n    verifier: string,\n    port: number\n): Promise<ClaudeAuthTokens> {\n    return new Promise((resolve, reject) => {\n        const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n            const url = new URL(req.url!, `http://localhost:${port}`);\n\n            if (url.pathname === '/callback') {\n                const code = url.searchParams.get('code');\n                const receivedState = url.searchParams.get('state');\n\n                if (receivedState !== state) {\n                    res.writeHead(400);\n                    res.end('Invalid state parameter');\n                    server.close();\n                    reject(new Error('Invalid state parameter'));\n                    return;\n                }\n\n                if (!code) {\n                    res.writeHead(400);\n                    res.end('No authorization code received');\n                    server.close();\n                    reject(new Error('No authorization code received'));\n                    return;\n                }\n\n                try {\n                    // Exchange code for tokens\n                    const tokens = await exchangeCodeForTokens(code, verifier, port, state);\n","sourceCodeStart":129,"sourceCodeEnd":165,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-cli/src/commands/connect/authenticateClaude.ts#L129-L165","documentation":"During OAuth callback handling, startCallbackServer validates that the state query parameter returned by the authorization server matches the state originally generated. A mismatch causes a 400 response and rejects the authentication promise with 'Invalid state parameter' to prevent CSRF.","triggerScenarios":"The OAuth provider redirects to /callback with a state value differing from the generated one: stale callback URL reuse, multiple concurrent auth flows clobbering state, provider stripping/altering the query string, or a forged callback.","commonSituations":"Reusing an old authorize URL after restarting the flow; running two authentication attempts in parallel in different terminals; proxy/redirect middleware dropping query parameters; expired session state.","solutions":["Restart the connect flow to generate a fresh state and use only the newest authorization URL","Ensure only one authentication attempt runs at a time; complete or cancel the previous one","Verify no proxy or browser extension is stripping the state query parameter from the redirect URL","Retry — transient provider-side issues can mangle redirect parameters"],"exampleFix":"// before\nconst receivedState = url.searchParams.get('state');\nif (receivedState !== state) { ... }\n// after\nconst receivedState = url.searchParams.get('state');\nif (!receivedState || !timingSafeEqual(Buffer.from(receivedState), Buffer.from(state))) { ... }","handlingStrategy":"retry","validationCode":"// before opening the browser, ensure a single fresh flow:\nconst expectedState = generateState(); // must be the state passed into startCallbackServer\n// compare against the URL you open:\nconsole.assert(authorizeUrl.includes(`state=${expectedState}`), 'state missing from authorize URL');","typeGuard":"function hasValidState(url: URL, expected: string): boolean {\n  const s = url.searchParams.get('state');\n  return typeof s === 'string' && s.length > 0 && s === expected;\n}","tryCatchPattern":"try {\n  const tokens = await authenticateClaude();\n} catch (e) {\n  if (e.message === 'Invalid state parameter') {\n    console.error('Stale or duplicate OAuth callback; restart the connect flow and use the newest URL');\n  } else throw e;\n}","preventionTips":["Run only one auth flow at a time; cancel stale ones","Always open the freshly generated authorization URL, never a bookmarked one","Don't re-open or refresh the callback URL after it has been consumed","Verify proxies/extensions don't strip query parameters"],"tags":["oauth","security","csrf","authentication"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"b824cd0a4681d41af631a8e422a813873e4455b0","analyzedAt":"2026-08-31T23:12:36.205Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}