fix(auth): disable Anthropic OAuth impersonation#29
Open
BunsDev wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Description
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).claude-cliUA /x-app/ Claude-specificanthropic-betaflags; Bearer tokens are still accepted as credentials but no impersonation headers are emitted (src-rust/crates/api/src/lib.rs).src-rust/crates/core/src/oauth_config.rsandsrc-rust/crates/core/src/lib.rs).src-rust/crates/core/src/lib.rs).Testing
cargo check -p claurst-apiwhich completed successfully for the API crate used in this change (passed).cargo check -p claurst --no-default-features(passed).oauth_flow::tests::anthropic_oauth_login_is_disabledviacargo test -p claurst --no-default-features --bin coven-codewhich passed.cargo test -p claurst-core oauth(core OAuth-related tests) which passed.cargo check --workspacewas blocked in this environment by a missing system dependency (alsa.pcforalsa-sys), so a full workspace check could not be completed here.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