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

xAI OAuth discovery returned untrusted {label}: {endpoint}

Error message

xAI OAuth discovery returned untrusted {label}: {endpoint}

What it means

After the HTTPS check, require_trusted_endpoint pins the host of every xAI OAuth endpoint to exactly x.ai or a subdomain of x.ai. It fires when the endpoint host is anything else, refusing to exchange codes or tokens against a foreign authorization server even over valid HTTPS. This blocks DNS-hijack and phishing discovery responses.

Source

Thrown at crates/zeroclaw-providers/src/auth/xai_oauth.rs:564

        .ok()
        .or_else(|| {
            base64::engine::general_purpose::URL_SAFE
                .decode(payload)
                .ok()
        })?;
    serde_json::from_slice(&bytes).ok()
}

fn require_trusted_endpoint(endpoint: &str, label: &str) -> Result<String> {
    let url = reqwest::Url::parse(endpoint).with_context(|| format!("Invalid xAI {label}"))?;
    if url.scheme() != "https" {
        anyhow::bail!("xAI OAuth discovery returned non-HTTPS {label}");
    }
    let host = url.host_str().unwrap_or_default();
    if host == "x.ai" || host.ends_with(".x.ai") {
        return Ok(endpoint.to_string());
    }
    anyhow::bail!("xAI OAuth discovery returned untrusted {label}: {endpoint}")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn authorize_url_contains_xai_oauth_params() {
        let pkce = PkceState {
            code_verifier: "verifier".into(),
            code_challenge: "challenge".into(),
            state: "state".into(),
        };
        let url = build_authorize_url("https://auth.x.ai/oauth2/authorize", &pkce);
        assert!(url.contains("client_id=b1a00492-073a-47ea-816f-4c329264a828"));
        assert!(url.contains(
            "scope=openid%20profile%20email%20offline_access%20grok-cli%3Aaccess%20api%3Aaccess"
        ));

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Confirm nothing repoints xAI endpoints at another domain; reset to the official x.ai endpoints
  2. Check DNS resolution of x.ai on this machine (hosts file, VPN split-DNS, corporate proxy)
  3. Retry the discovery fetch to rule out a transient poisoned response
  4. If you run a private xAI-compatible IdP, this client is intentionally unusable with it - use the generic OAuth flow instead
Defensive patterns

Strategy: try-catch

Validate before calling

// Optional pre-flight: pin the host allowlist in your own wiring too
let host = reqwest::Url::parse(endpoint)?
    .host_str()
    .unwrap_or_default()
    .to_string();
if !(host == "x.ai" || host.ends_with(".x.ai")) {
    anyhow::bail!("refusing xAI endpoint on foreign host: {host}");
}

Try / catch

match exchange_code_for_tokens(&ctx, &code, &verifier).await {
    Ok(tokens) => { /* store TokenSet */ }
    Err(e) if e.to_string().contains("untrusted") => {
        // do not retry; investigate DNS/proxy and surface to the user
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: The discovery document advertises an endpoint whose host is not x.ai / *.x.ai, and any discovery-consuming flow (token exchange, device-code start or poll) then rejects it before sending credentials.

Common situations: DNS hijack or hosts-file override for x.ai; corporate proxy rewriting discovery JSON; a stale or custom discovery cache pointing at old or test hosts; attempting to point the xAI client at a private xAI-compatible IdP on another domain.

Related errors


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