{"record":{"id":"ef323e7f1b67ca85","repo":"actualbudget/actual","slug":"authorization-failed-missing-state-parameter","errorCode":null,"errorMessage":"Authorization failed: missing state parameter.","messagePattern":"Authorization failed: missing state parameter\\.","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/sync-server/src/app-enablebanking/app-enablebanking.ts","lineNumber":116,"sourceCode":"// Auth callback from bank redirect — must be before validateSessionMiddleware\n// since the bank redirects here directly (no auth token available)\napp.get('/auth_callback', async (req: Request, res: Response) => {\n  const code = typeof req.query.code === 'string' ? req.query.code : undefined;\n  const state =\n    typeof req.query.state === 'string' ? req.query.state : undefined;\n\n  if (!code) {\n    res\n      .status(400)\n      .send(\n        '<html><body><p>Authorization failed: missing code.</p></body></html>',\n      );\n    return;\n  }\n\n  if (!state) {\n    res\n      .status(400)\n      .send(\n        '<html><body><p>Authorization failed: missing state parameter.</p></body></html>',\n      );\n    return;\n  }\n\n  try {\n    const session = await enableBankingService.createSession(code);\n    debug(\n      'Callback session created: %s with %d accounts',\n      session.session_id,\n      session.accounts.length,\n    );\n\n    const result = await buildSessionResult(session, extractPsuHeaders(req));\n\n    // Always cache the result so retries within TTL can read it\n    completedAuths.set(state, result);","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-enablebanking/app-enablebanking.ts#L98-L134","documentation":"The Enable Banking OAuth callback requires a `state` query parameter to correlate the redirect with a pending authorization session. When `state` is absent the handler returns HTTP 400 with this HTML page. The state parameter is the CSRF/session correlation token for the pending auth, so the flow cannot proceed without it.","triggerScenarios":"A request hits the Enable Banking callback route with a `code` but no `state` query parameter — typically a hand-crafted or replayed callback URL, or a provider that drops the state on redirect.","commonSituations":"Testing the callback URL manually in a browser; third-party tools that strip query parameters; misconfigured authorization request that omitted state from the outbound URL while the callback still expects it.","solutions":["Restart the authorization flow so a fresh state is generated and echoed back by the provider","Ensure the outbound authorization request includes a state parameter","Do not invoke the callback URL directly; always complete the provider-hosted authorization step","Check for middleware/proxies that may strip query parameters from the redirect"],"exampleFix":"// before\nGET /enablebanking-handler?code=abc  -> 400 missing state\n// after\nGET /enablebanking-handler?code=abc&state=<pending-session-state>","handlingStrategy":"validation","validationCode":"const url = new URL(callbackUrl);\nif (!url.searchParams.get('state')) {\n  throw new Error('callback URL missing state; restart the OAuth flow');\n}","typeGuard":"function hasState(q: Record<string, unknown>): q is { state: string } {\n  return typeof q.state === 'string' && q.state.length > 0;\n}","tryCatchPattern":"null","preventionTips":["Always include state in the outbound authorization request","Complete the flow in one browser session without manually editing URLs","Check proxies/middleware don't strip query params","Store state client-side so you can detect a dropped state before calling the server"],"tags":["oauth","http-400","enablebanking","csrf","missing-parameter"],"backgroundTag":"oauth-missing-state-parameter","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}