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

Missing OAuth code in callback

Error message

Missing OAuth code in callback

What it means

`parse_code_from_redirect` classified the input as a callback payload (contains `?` or has `code`/`state`/`error` params) but found no `code` parameter, so there is nothing to exchange for tokens. Non-callback raw input is returned as-is; only callback-shaped input without a code fails. This prevents an empty or partial redirect from being treated as a successful login.

Source

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

    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)
        .ok()?;
    let claims: serde_json::Value = serde_json::from_slice(&decoded).ok()?;

    // Prefer the flat chatgpt_account_id claim when present.
    if let Some(value) = claims.get("chatgpt_account_id").and_then(|v| v.as_str())
        && !value.trim().is_empty()
    {
        return Some(value.to_string());
    }

    // Real OpenAI OAuth tokens namespace custom claims under
    // https://api.openai.com/auth as a JSON object, not a flat dotted key.

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Restart the login flow and complete consent in the browser so the redirect carries `code`
  2. Do not navigate to the loopback callback URL manually
  3. Ignore callback-shaped requests that lack `code` (favicon/prefetch noise) instead of failing the flow
  4. If integrating, require the full authorize step to finish before parsing the callback path

Example fix

// before: first request to the listener aborts the whole login
let code = parse_code_from_redirect(path, Some(&state))?; // "Missing OAuth code in callback"

// after: keep listening when the hit carries no code
let code = match parse_code_from_redirect(path, Some(&state)) {
    Ok(code) => code,
    Err(e) if e.to_string().contains("Missing OAuth code") => continue_listening().await,
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: validation

Validate before calling

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

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

Try / catch

match parse_code_from_redirect(path, Some(&state)) {
    Ok(code) => code,
    Err(e) if e.to_string().contains("Missing OAuth code") => continue_listening().await, // stray hit, keep the listener up
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `parse_code_from_redirect("/auth/callback?state=xyz", Some("xyz"))` — state validates but no `code` key exists in the parsed query. Any query-bearing path that reaches the final bail without a `code` param (the `error` and state checks already passed).

Common situations: User manually opens http://localhost:1455/auth/callback before completing consent; the IdP redirects after an aborted consent carrying only state; browser prefetch or a stray request lands on the loopback listener with a query string.

Related errors


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