{"record":{"id":"a52228fdac82b323","repo":"Hmbown/CodeWhale","slug":"oauth-error-callback-state-did-not-match-the-pending-login","errorCode":null,"errorMessage":"OAuth error callback state did not match the pending login","messagePattern":"OAuth error callback state did not match the pending login","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":1233,"sourceCode":"    Ok(CallbackOutcome::Success { code, state })\n}\n\npub fn accept_callback(expected_state: &str, outcome: CallbackOutcome) -> Result<String> {\n    match outcome {\n        CallbackOutcome::Success { code, state } => {\n            anyhow::ensure!(\n                state == expected_state,\n                \"OAuth callback state did not match the pending login\"\n            );\n            Ok(code)\n        }\n        CallbackOutcome::Error {\n            error,\n            description,\n            state,\n        } => {\n            if let Some(state) = state {\n                anyhow::ensure!(\n                    state == expected_state,\n                    \"OAuth error callback state did not match the pending login\"\n                );\n            }\n            let detail = description\n                .filter(|text| !text.trim().is_empty())\n                .unwrap_or(error);\n            bail!(\"sign-in was not completed: {detail}\")\n        }\n    }\n}\n\nfn parse_http_request_target(request_line: &str) -> Result<String> {\n    let mut parts = request_line.split_whitespace();\n    let method = parts.next().unwrap_or_default();\n    anyhow::ensure!(\n        method.eq_ignore_ascii_case(\"GET\"),\n        \"OAuth callback must be GET\"","sourceCodeStart":1215,"sourceCodeEnd":1251,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L1215-L1251","documentation":"When the OAuth provider redirects back with an error outcome, `accept_callback` still verifies the `state` parameter when the provider included one; if it does not match the pending login, this error is thrown before the error detail is surfaced. This ensures an attacker cannot fabricate error callbacks against an unrelated pending login, and distinguishes mismatched-state errors from genuine provider errors.","triggerScenarios":"`accept_callback` receives `CallbackOutcome::Error { error, description, state: Some(other_state) }` where `state` differs from `expected_state` — an error redirect carrying a state from a different login attempt.","commonSituations":"A user denies consent in a stale tab from a previous login while a newer one is pending; a provider that mangles/re-encodes the state on its error redirect; replaying an old denial URL.","solutions":["Restart the login flow and complete (or fail) it in the tab the flow opened","Verify the provider preserves the state parameter verbatim on error redirects; URL-encoding differences must be decoded before comparison","Discard the stale error callback and retry with a fresh state"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"fn error_callback_state_ok(expected: &str, url: &Url) -> Option<bool> {\n    url.query_pairs().find(|(k, _)| k == \"state\")\n        .map(|(_, v)| v == expected) // None => provider omitted state, allowed\n}","typeGuard":null,"tryCatchPattern":"match accept_callback(expected_state, outcome) {\n    Ok(_) => unreachable!(\"error outcome cannot succeed\"),\n    Err(e) if e.to_string().contains(\"error callback state did not match\") => {\n        // denial from a stale/different login: restart the flow\n        cancel_pending_login();\n        start_new_login()\n    }\n    Err(e) => return Err(e),  // genuine provider error: read its detail\n}","preventionTips":["Keep one pending login at a time; cancel the previous before starting a new one","Verify the provider echoes state verbatim on error redirects (some re-encode it)","Never trust an error callback whose state you cannot tie to your own request"],"tags":["oauth","csrf","state-mismatch","security"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}