zeroclaw-labs/zeroclaw · error · anyhow::Error

Missing OAuth state in callback

Error message

Missing OAuth state in callback

What it means

Thrown when `parse_code_from_redirect` was given an `expected_state`, the input looks like a real callback (has `?`, or carries `code`/`state`/`error` params), but the query has no `state` parameter at all. The library refuses to process a callback that cannot prove it originated from your authorize request. Raw-code input without callback shape is still accepted; only callback-shaped payloads must carry state.

Source

Thrown at crates/zeroclaw-providers/src/auth/openai_oauth.rs:325

        || params.contains_key("code")
        || params.contains_key("state")
        || params.contains_key("error");

    if let Some(err) = params.get("error") {
        let desc = params
            .get("error_description")
            .cloned()
            .unwrap_or_else(|| "OAuth authorization failed".to_string());
        anyhow::bail!("OpenAI OAuth error: {err} ({desc})");
    }

    if let Some(expected_state) = expected_state {
        if let Some(got) = params.get("state") {
            if got != expected_state {
                anyhow::bail!("OAuth state mismatch");
            }
        } else if is_callback_payload {
            anyhow::bail!("Missing OAuth state in callback");
        }
    }

    if let Some(code) = params.get("code").cloned() {
        return Ok(code);
    }

    if !is_callback_payload {
        return Ok(trimmed.to_string());
    }

    anyhow::bail!("Missing OAuth code in callback")
}

pub fn extract_account_id_from_jwt(token: &str) -> Option<String> {
    let payload = token.split('.').nth(1)?;
    let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
        .decode(payload)

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Build the authorize URL with `build_authorize_url(&pkce)` — it always sets `state` (and PKCE fields)
  2. Restart the flow and let the browser complete the full redirect; never hand-type the callback URL
  3. Keep the redirect's query string intact — no rewriting middleware that drops parameters
  4. If parsing redirects yourself, pass the state value unchanged from the authorize request

Example fix

// before: hand-built authorize URL without state
let url = format!("{}?response_type=code&client_id={}", OPENAI_OAUTH_AUTHORIZE_URL, OPENAI_OAUTH_CLIENT_ID);
// callback arrives with no state -> "Missing OAuth state in callback"

// after: helper embeds state and PKCE correctly
let pkce = generate_pkce_state();
let url = build_authorize_url(&pkce);
Defensive patterns

Strategy: validation

Validate before calling

fn callback_has_state(input: &str) -> bool {
    input.split_once('?').map_or(false, |(_, q)| {
        q.split('&').any(|pair| pair.starts_with("state="))
    })
}

if callback_has_state(path) {
    let code = parse_code_from_redirect(path, Some(&pkce.state))?;
}

Try / catch

match parse_code_from_redirect(path, Some(&pkce.state)) {
    Ok(code) => exchange(code),
    Err(e) if e.to_string().contains("Missing OAuth state") => restart_login_flow().await,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `parse_code_from_redirect("/auth/callback?code=abc", Some("xyz"))` — a `code` param present but no `state` — reaching the `else if is_callback_payload` branch. Via `receive_loopback_code` when the redirect from auth.openai.com arrives without state because the authorize request never included one, or a callback-shaped request hits the listener from another source.

Common situations: Authorize URL hand-built instead of using `build_authorize_url` (which always embeds state); test fixtures or custom integrations constructing the callback path by hand; a proxy or manual edit stripping query parameters from the redirect.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/23d4c406ec13402c. Report an issue: GitHub.