Kuberwastaken/claurst · error · anyhow::Error

Missing code or state in OAuth callback

Error message

Missing code or state in OAuth callback

What it means

During the Codex OAuth login flow, claurst runs a temporary local HTTP server to receive the provider's redirect. When the browser hits the callback URL, wait_for_callback parses the query string and requires both `code` and `state` parameters. If either is absent or empty after URL-decoding, the flow is aborted because an authorization code is required to exchange for tokens and the state value is required to validate the redirect against the original request.

Solutions

  1. Restart the login flow (`claurst auth login` / run_oauth_flow_with_label) and complete the sign-in in the browser window that opens, without editing the URL
  2. Check that the provider's redirect actually reached the local server: verify the full callback URL in the browser address bar contains both `code=` and `state=` before it is served
  3. Clear provider session/cookies and retry — some providers redirect without parameters when the SSO session is stale
  4. If running behind a proxy or firewall, ensure the localhost callback port is reachable and the query string is not being rewritten or stripped
  5. If the provider consistently omits `state`, verify the authorization URL built by run_oauth_flow_with_label includes state and that the provider config preserves query parameters on redirect

Example fix

// before: pasting/truncating the callback URL in the browser
http://localhost:1455/auth/callback?

// after: let the flow redirect the browser automatically, or paste the full URL
http://localhost:1455/auth/callback?code=abc123&state=xyz789
Defensive patterns

Strategy: validation

Validate before calling

// Before starting the flow, confirm the callback URL template requires code & state,
// and validate any manually supplied callback URL before serving/processing it:
fn validate_callback_url(url: &str) -> anyhow::Result<()> {
    let q = url.split('?').nth(1).unwrap_or("");
    let has = |k: &str| q.split('&').any(|p| p.starts_with(&format!("{}=", k)) && p.len() > k.len() + 1);
    anyhow::ensure!(has("code") && has("state"), "callback URL lacks code/state");
    Ok(())
}

Prevention

When it happens

Trigger: The local callback server receives an HTTP request whose query string contains no `code` parameter or no `state` parameter (either missing entirely or URL-decoding to an empty string), and the query also lacks an `error`/`error_description` parameter (that case bails earlier with "OAuth error"). E.g. the user pastes the bare redirect URL `http://localhost:PORT/callback?` or a URL with only `state` into the browser.

Common situations: The user manually navigates to or bookmarks the localhost callback URL; the auth server redirects with only some parameters; the user copies the callback URL but truncates the query string; a proxy or browser extension strips query parameters; the OAuth provider redirects to the base redirect_uri without parameters after a session timeout.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/16cacd82241a0704. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/cli/src/codex_oauth_flow.rs:178

        "<html><body style='background:#131010;color:#f1ecec;display:flex;justify-content:center;align-items:center;height:100vh;font-family:system-ui'>\
         <div style='text-align:center'><h1 style='color:#fc533a'>Authorization Failed</h1><p>Check the terminal for details.</p></div></body></html>"
    };
    let response = format!(
        "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        html.len(),
        html
    );
    // Drop the BufReader so we can write back on the socket
    drop(reader);
    let _ = socket.write_all(response.as_bytes()).await;
    let _ = socket.shutdown().await;

    if !error.is_empty() {
        bail!("OAuth error: {}", error);
    }

    if code.is_empty() || state.is_empty() {
        bail!("Missing code or state in OAuth callback");
    }

    Ok((code, state))
}

/// Exchange authorization code for access tokens.
async fn exchange_code_for_tokens(code: &str, verifier: &str) -> anyhow::Result<CodexTokens> {
    let client = reqwest::Client::new();
    let params = [
        ("client_id", CODEX_CLIENT_ID),
        ("code", code),
        ("code_verifier", verifier),
        ("grant_type", "authorization_code"),
        ("redirect_uri", CODEX_REDIRECT_URI),
    ];

    let resp = client
        .post(CODEX_TOKEN_URL)

View on GitHub (pinned to b0637c97ec)