Kuberwastaken/claurst · error

Login succeeded but could not obtain a usable credential

Error message

Login succeeded but could not obtain a usable credential

What it means

This error is raised in finalize_login after the OAuth token exchange succeeded but no credential usable for API calls could be assembled. The flow grants either an inference-capable access token (bearer auth) or, for the Console flow, an API key minted via create_api_key. If the token response lacks the inference scope AND the API key creation failed (its error is swallowed into None with only a warning), nothing usable remains and login must abort.

Solutions

  1. Check the warn! log line immediately above the error ('Failed to create API key from OAuth token: ...') — it contains the real root cause (HTTP status/body or parse error).
  2. Re-run the login flow; transient network failures during key creation are common and a retry usually succeeds.
  3. Verify the authenticated account/organization actually permits API key creation (org settings, billing active, role permissions).
  4. Confirm the authorization URL requests the intended scopes; if you expect bearer auth, ensure the inference scope (CLAUDE_AI_INFERENCE_SCOPE) is granted.
  5. If the server changed its CreateApiKeyResponse shape, update the parser in this crate to match the current wire format.

Example fix

// before: key-creation failure is silently discarded, making the root cause opaque
let api_key = if !uses_bearer {
    match create_api_key(&token_resp.access_token).await {
        Ok(key) => Some(key),
        Err(e) => {
            warn!("Failed to create API key from OAuth token: {}", e);
            None
        }
    }
} else { None };
// after: surface the upstream reason in the final error
let api_key = if !uses_bearer {
    create_api_key(&token_resp.access_token).await.ok()
} else { None };
if !uses_bearer && api_key.is_none() {
    bail!("Login succeeded but could not obtain a usable credential: API key creation failed — see prior warning for the server response");
}
Defensive patterns

Strategy: fallback

Validate before calling

// Before starting login, verify the flow can produce a credential
let scopes: Vec<&str> = auth_url_scopes.split_whitespace().collect();
let will_use_bearer = scopes.contains(oauth::CLAUDE_AI_INFERENCE_SCOPE);
if !will_use_bearer {
    ensure_console_key_creation_allowed()?; // org/policy check up front
}

Type guard

fn usable_credential(uses_bearer: bool, api_key: &Option<String>) -> Option<(String, bool)> {
    if uses_bearer {
        Some(("bearer".to_string(), true))
    } else {
        api_key.clone().map(|k| (k, false))
    }
}

Try / catch

match run_oauth_login_flow_with_label(label).await {
    Ok(result) => info!("logged in"),
    Err(e) if e.to_string().contains("usable credential") => {
        eprintln!("Login succeeded but key creation failed: {}", e);
        eprintln!("Check org API-key permissions and billing, then retry.");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: finalize_login receives a TokenExchangeResponse whose scope set does not contain CLAUDE_AI_INFERENCE_SCOPE (so uses_bearer is false), and the subsequent create_api_key call fails (network error, non-success HTTP status, missing raw_key), so api_key is None and the final if/else falls through to bail!.

Common situations: Developers hit this when the OAuth client is configured for the Console flow but the server rejects key creation (expired access token seconds after exchange, org policy forbidding API key creation, missing api-key creation scope), or when the upstream API returns an unexpected payload (e.g. raw_key absent) during a provider-side schema change.

Related errors


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

Appendix: source

Thrown at src-rust/crates/cli/src/oauth_flow.rs:248

        expires_at_ms: Some(expires_at_ms),
        scopes: scopes.clone(),
        account_uuid,
        email,
        organization_uuid,
        subscription_type: None,
        api_key: api_key.clone(),
    };
    tokens
        .save_and_register(label)
        .await
        .context("Failed to save OAuth tokens")?;

    let (credential, use_bearer_auth) = if uses_bearer {
        (token_resp.access_token.clone(), true)
    } else if let Some(key) = api_key {
        (key, false)
    } else {
        bail!("Login succeeded but could not obtain a usable credential")
    };

    Ok(LoginResult { credential, use_bearer_auth, tokens })
}

// ---- Helpers ----------------------------------------------------------------

/// Attempt to open the URL in the system default browser (best-effort).
fn try_open_browser(url: &str) {
    #[cfg(target_os = "windows")]
    {
        // Use PowerShell to safely open URLs containing special characters (& etc.)
        let ps_cmd = format!("Start-Process '{}'", url.replace('\'', "''"));
        let _ = std::process::Command::new("powershell")
            .args(["-NoProfile", "-NonInteractive", "-Command", &ps_cmd])
            .stdin(std::process::Stdio::null())
            .stdout(std::process::Stdio::null())
            .stderr(std::process::Stdio::null())

View on GitHub (pinned to b0637c97ec)