{"record":{"id":"2a55d8290430dc29","repo":"lbjlaq/Antigravity-Manager","slug":"oauth-state-mismatch","errorCode":null,"errorMessage":"OAuth state mismatch","messagePattern":"OAuth state mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/src/modules/oauth_server.rs","lineNumber":231,"sourceCode":"                            received_state.as_ref() == Some(&s.state)\n                        } else {\n                            false\n                        }\n                    } else {\n                        false\n                    }\n                };\n\n                let (result, response_html) = match (code, state_valid) {\n                    (Some(code), true) => {\n                        crate::modules::logger::log_info(\n                            \"Successfully captured OAuth code from IPv4 listener\",\n                        );\n                        (Ok(code), oauth_success_html())\n                    }\n                    (Some(_), false) => {\n                        crate::modules::logger::log_error(\n                            \"OAuth callback state mismatch (CSRF protection)\",\n                        );\n                        (Err(\"OAuth state mismatch\".to_string()), oauth_fail_html())\n                    }\n                    (None, _) => (\n                        Err(\"Failed to get Authorization Code in callback\".to_string()),\n                        oauth_fail_html(),\n                    ),\n                };\n\n                let _ = stream.write_all(response_html.as_bytes()).await;\n                let _ = stream.flush().await;\n\n                if let Some(h) = app_handle {\n                    use tauri::Emitter;\n                    let _ = h.emit(\"oauth-callback-received\", ());\n                }\n                let _ = tx.send(result).await;\n            }","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/lbjlaq/Antigravity-Manager/blob/90a2b6b20797e2e593a83d0c54ab86037863fe70/src-tauri/src/modules/oauth_server.rs#L213-L249","documentation":"This error is produced by the local loopback OAuth callback server started in ensure_oauth_flow_prepared (src-tauri/src/modules/oauth_server.rs). When the browser redirects back to the IPv4 listener with an authorization code, the handler compares the callback's `state` query parameter against the state stored in OAUTH_FLOW_STATE; if a code is present but the state does not match (or no flow state exists), the CSRF check fails and 'OAuth state mismatch' is sent back through the code channel and returned to the caller of prepare_oauth_url/start_oauth_flow/complete_oauth_flow. It is a deliberate CSRF-protection rejection, not a network fault.","triggerScenarios":"A callback request hits the IPv4 listener (127.0.0.1:<port>/oauth-callback) carrying a `code` parameter but a `state` value that differs from the stored OAuthFlowState.state — e.g. the flow was re-prepared/cancelled (state reset to None) between when the auth URL was opened and when the browser redirected back, the user reused an old auth-URL tab, or two OAuth flows were started concurrently.","commonSituations":"User opens the authorization page, restarts the OAuth flow in the app (which calls ensure_oauth_flow_prepared again and regenerates state), then completes login in the stale tab; a second client key was requested, cancelling the in-flight flow (s.client_key != requested_key drops the old state); the browser cached/replayed an old callback URL; a slow authorization finished after the flow was force-cancelled because code_rx had already been taken.","solutions":["Start a fresh OAuth flow (prepare_oauth_url/start_oauth_flow again) and open the NEW auth URL — do not reuse a previously opened authorization tab.","Only click 'Start OAuth' once per attempt and complete the login in the tab opened from that same auth URL.","Check the app log for 'OAuth callback state mismatch (CSRF protection)' and confirm whether another flow was started concurrently; serialize OAuth flows in the UI so only one is active.","If switching accounts/clients, cancel and restart the flow rather than mixing auth URLs across client keys.","Verify system clock/browser session sanity; if the mismatch is reproducible with a single fresh flow, report it since state is generated in-memory with uuid::Uuid::new_v4."],"exampleFix":"// before: reusing a stale auth URL tab after restarting the flow\nopen_browser(old_auth_url); // state already regenerated -> mismatch\n// after: always fetch a fresh URL from the current flow\nlet url = start_oauth_flow(client_key).await?;\nopen_browser(url);","handlingStrategy":"try-catch","validationCode":"// Before completing the flow, confirm a fresh auth URL was obtained in THIS attempt:\nconst url = await invoke('prepare_oauth_url', { clientKey });\nif (!url.includes('state=')) throw new Error('auth URL missing state param');\n// Always open this exact URL; never reuse a previously opened tab.","typeGuard":"function isStateMismatchError(e: unknown): e is string {\n  return typeof e === 'string' && e === 'OAuth state mismatch';\n}","tryCatchPattern":"try {\n  const code = await invoke('complete_oauth_flow', { clientKey });\n} catch (e) {\n  if (isStateMismatchError(e)) {\n    // stale flow: restart with a brand-new auth URL\n    const freshUrl = await invoke('start_oauth_flow', { clientKey });\n    openBrowser(freshUrl);\n  } else throw e;\n}","preventionTips":["Always open the auth URL returned by the current flow invocation; discard old tabs/URLs.","Never start a second OAuth flow while one is pending; cancel explicitly first.","Don't change the client key mid-flow; restarting invalidates the old state by design.","Complete login promptly; long delays increase the chance the flow was reset in between.","Log which listener (IPv4/IPv6) reported the mismatch to distinguish stale-tab vs concurrency causes."],"tags":["oauth","csrf","state-mismatch","callback"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"90a2b6b20797e2e593a83d0c54ab86037863fe70","analyzedAt":"2026-09-19T08:34:01.322Z","contentChangedAt":"2026-09-19T08:34:01.322Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}