Skip to content

fix(auth): disable Anthropic OAuth impersonation#29

Open
BunsDev wants to merge 1 commit into
mainfrom
codex/investigate-oauth-client-impersonation-flaw-3z2ubt
Open

fix(auth): disable Anthropic OAuth impersonation#29
BunsDev wants to merge 1 commit into
mainfrom
codex/investigate-oauth-client-impersonation-flaw-3z2ubt

Conversation

@BunsDev
Copy link
Copy Markdown
Member

@BunsDev BunsDev commented Jun 3, 2026

Motivation

  • Remove active request-time impersonation of Anthropic's Claude Code OAuth client to eliminate an OAuth client-identity impersonation vulnerability introduced by prior changes.
  • Prevent this third-party CLI from obtaining or presenting tokens that identify it as the official Claude Code client.

Description

  • Disable the interactive Anthropic OAuth PKCE flow by making run_oauth_login_flow* return an error and adding a test asserting the failure, so users must provide an API key instead (src-rust/crates/cli/src/oauth_flow.rs).
  • Stop injecting the Claude Code identity and headers on OAuth requests: removed system-prompt prepending and removed claude-cli UA / x-app / Claude-specific anthropic-beta flags; Bearer tokens are still accepted as credentials but no impersonation headers are emitted (src-rust/crates/api/src/lib.rs).
  • Clear embedded Claude Code OAuth client IDs and remove impersonation constants so the OSS build cannot reuse another application's client identity (src-rust/crates/core/src/oauth_config.rs and src-rust/crates/core/src/lib.rs).
  • Ignore stored Bearer-style Anthropic tokens for automatic resolution when they identify as subscription tokens (code now rejects stored bearer tokens until a first-party client ID is available) (src-rust/crates/core/src/lib.rs).

Testing

  • Ran cargo check -p claurst-api which completed successfully for the API crate used in this change (passed).
  • Ran cargo check -p claurst --no-default-features (passed).
  • Added and ran unit test oauth_flow::tests::anthropic_oauth_login_is_disabled via cargo test -p claurst --no-default-features --bin coven-code which passed.
  • Ran cargo test -p claurst-core oauth (core OAuth-related tests) which passed.
  • Note: cargo check --workspace was blocked in this environment by a missing system dependency (alsa.pc for alsa-sys), so a full workspace check could not be completed here.
  • Existing unrelated test failure observed earlier in claurst-api (codex_adapter::tests::test_anthropic_to_openai_request_basic) is pre-existing and unrelated to the impersonation fix (float precision assertion); it remains outside this patch.

Codex Task

Copilot AI review requested due to automatic review settings June 3, 2026 10:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR disables Anthropic OAuth “Claude Code” client impersonation across the Rust CLI/core/api crates by removing embedded Claude Code client identity constants and preventing the interactive PKCE login flow from running, so the OSS CLI cannot mint/present tokens as another application.

Changes:

  • Disabled the interactive Anthropic OAuth PKCE login flow by making run_oauth_login_flow* return an error and adding a unit test asserting the failure.
  • Removed request-time “Claude Code” impersonation behaviors (system-prompt prefixing and Claude-specific headers/UA/beta flags) from the Anthropic API client.
  • Cleared embedded Anthropic OAuth client IDs and updated auth resolution to ignore stored Bearer-style tokens while first-party OAuth is unavailable.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src-rust/crates/core/src/oauth_config.rs Removes Claude Code stealth-impersonation constants and blanks OAuth client IDs.
src-rust/crates/core/src/lib.rs Stops resolving stored Bearer-style OAuth tokens; clears Anthropic OAuth CLIENT_ID.
src-rust/crates/cli/src/oauth_flow.rs Disables Anthropic OAuth login flow and adds a test asserting it is disabled.
src-rust/crates/api/src/lib.rs Removes Claude Code impersonation headers and system-prompt modifications from OAuth/Bearer request paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1408 to 1411
let tokens = crate::oauth::OAuthTokens::load().await?;

// If expired and we have a refresh token, attempt silent refresh.
// Clone the refresh token up-front so we don't borrow `tokens` during the async call.
let refresh_token_owned = tokens.refresh_token.clone();
let tokens = if tokens.is_expired() {
if let Some(rt) = refresh_token_owned {
// Inline the refresh HTTP call (cc_core can't depend on cc_cli::oauth_flow).
let body = serde_json::json!({
"grant_type": "refresh_token",
"refresh_token": rt,
"client_id": crate::oauth::CLIENT_ID,
"scope": crate::oauth::ALL_SCOPES.join(" "),
});
let refreshed = 'refresh: {
let Ok(client) = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build() else { break 'refresh None; };
let Ok(resp) = client
.post(crate::oauth::TOKEN_URL)
.header("content-type", "application/json")
.json(&body)
.send()
.await else { break 'refresh None; };
if !resp.status().is_success() { break 'refresh None; }
let Ok(data) = resp.json::<serde_json::Value>().await else { break 'refresh None; };
let new_at = data["access_token"].as_str().unwrap_or("").to_string();
if new_at.is_empty() { break 'refresh None; }
let new_rt = data["refresh_token"].as_str().map(String::from);
let exp_in = data["expires_in"].as_u64().unwrap_or(3600);
let exp_ms = chrono::Utc::now().timestamp_millis() + (exp_in as i64 * 1000);
let scopes: Vec<String> = data["scope"]
.as_str().unwrap_or("").split_whitespace().map(String::from).collect();
let mut r = tokens.clone();
r.access_token = new_at;
if let Some(nrt) = new_rt { r.refresh_token = Some(nrt); }
r.expires_at_ms = Some(exp_ms);
r.scopes = scopes;
let _ = r.save().await;
Some(r)
};
refreshed.unwrap_or(tokens)
} else {
tokens // expired, no refresh token → can't fix
}
} else {
tokens
};

if let Some(cred) = tokens.effective_credential() {
Some((cred.to_string(), tokens.uses_bearer_auth()))
} else {
None
if tokens.uses_bearer_auth() {
return None;
}
Comment on lines 1408 to 1411
let tokens = crate::oauth::OAuthTokens::load().await?;

// If expired and we have a refresh token, attempt silent refresh.
// Clone the refresh token up-front so we don't borrow `tokens` during the async call.
let refresh_token_owned = tokens.refresh_token.clone();
let tokens = if tokens.is_expired() {
if let Some(rt) = refresh_token_owned {
// Inline the refresh HTTP call (cc_core can't depend on cc_cli::oauth_flow).
let body = serde_json::json!({
"grant_type": "refresh_token",
"refresh_token": rt,
"client_id": crate::oauth::CLIENT_ID,
"scope": crate::oauth::ALL_SCOPES.join(" "),
});
let refreshed = 'refresh: {
let Ok(client) = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build() else { break 'refresh None; };
let Ok(resp) = client
.post(crate::oauth::TOKEN_URL)
.header("content-type", "application/json")
.json(&body)
.send()
.await else { break 'refresh None; };
if !resp.status().is_success() { break 'refresh None; }
let Ok(data) = resp.json::<serde_json::Value>().await else { break 'refresh None; };
let new_at = data["access_token"].as_str().unwrap_or("").to_string();
if new_at.is_empty() { break 'refresh None; }
let new_rt = data["refresh_token"].as_str().map(String::from);
let exp_in = data["expires_in"].as_u64().unwrap_or(3600);
let exp_ms = chrono::Utc::now().timestamp_millis() + (exp_in as i64 * 1000);
let scopes: Vec<String> = data["scope"]
.as_str().unwrap_or("").split_whitespace().map(String::from).collect();
let mut r = tokens.clone();
r.access_token = new_at;
if let Some(nrt) = new_rt { r.refresh_token = Some(nrt); }
r.expires_at_ms = Some(exp_ms);
r.scopes = scopes;
let _ = r.save().await;
Some(r)
};
refreshed.unwrap_or(tokens)
} else {
tokens // expired, no refresh token → can't fix
}
} else {
tokens
};

if let Some(cred) = tokens.effective_credential() {
Some((cred.to_string(), tokens.uses_bearer_auth()))
} else {
None
if tokens.uses_bearer_auth() {
return None;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants