Skip to content

Commit b263473

Browse files
authored
fix(auth): disable Anthropic OAuth impersonation (#29)
Verified locally before merge: cargo check -p claurst-api; cargo check -p claurst --no-default-features (known claurst-tui::rustle warning only); cargo test -p claurst --no-default-features --bin coven-code oauth_flow::tests::anthropic_oauth_login_is_disabled -- --nocapture; cargo test -p claurst-core oauth; runtime auth-status smoke for stored user:inference token reported loggedIn=false/authMethod=none/disabledTokenSource and exit 1; git diff --check. Co-authored-by: Nova <nova@openclaw.ai>
1 parent d426f19 commit b263473

7 files changed

Lines changed: 112 additions & 662 deletions

File tree

src-rust/crates/api/src/lib.rs

Lines changed: 12 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -468,55 +468,12 @@ pub mod client {
468468
self.config.api_key.is_empty()
469469
}
470470

471-
/// Returns `true` when this client is configured to use a Claude Code
472-
/// OAuth Bearer token (Claude.ai Pro/Max). The query path and the
473-
/// request builders check this to enable stealth-impersonation.
471+
/// Returns `true` when this client is configured to use Bearer-token
472+
/// authorization instead of an Anthropic API key.
474473
pub fn is_oauth(&self) -> bool {
475474
self.config.use_bearer_auth
476475
}
477476

478-
/// Mutate the outgoing request so it looks like Claude Code when the
479-
/// client is authenticated with an OAuth Bearer token:
480-
///
481-
/// 1. Prepend the required `"You are Claude Code, …"` system block.
482-
/// Existing system content is preserved as a second block so the
483-
/// rest of Coven Code's prompt assembly still reaches the model.
484-
///
485-
/// No-op when `use_bearer_auth` is false (API-key flow).
486-
fn apply_oauth_stealth(&self, request: &mut CreateMessageRequest) {
487-
if !self.config.use_bearer_auth {
488-
return;
489-
}
490-
491-
let identity_block = SystemBlock {
492-
block_type: "text".to_string(),
493-
text: claurst_core::oauth_config::CLAUDE_CODE_SYSTEM_PROMPT_PREFIX.to_string(),
494-
cache_control: None,
495-
};
496-
497-
request.system = match request.system.take() {
498-
None => Some(SystemPrompt::Blocks(vec![identity_block])),
499-
Some(SystemPrompt::Text(existing)) => {
500-
let existing_block = SystemBlock {
501-
block_type: "text".to_string(),
502-
text: existing,
503-
cache_control: None,
504-
};
505-
Some(SystemPrompt::Blocks(vec![identity_block, existing_block]))
506-
}
507-
Some(SystemPrompt::Blocks(mut blocks)) => {
508-
// Avoid duplicating the identity block on retries / re-sends.
509-
let already_first = blocks.first().is_some_and(|b| {
510-
b.text == claurst_core::oauth_config::CLAUDE_CODE_SYSTEM_PROMPT_PREFIX
511-
});
512-
if !already_first {
513-
blocks.insert(0, identity_block);
514-
}
515-
Some(SystemPrompt::Blocks(blocks))
516-
}
517-
};
518-
}
519-
520477
/// Build a new client. Panics if `config.api_key` is empty.
521478
pub fn new(config: ClientConfig) -> anyhow::Result<Self> {
522479
// Allow empty key at construction — validation is deferred to
@@ -592,8 +549,9 @@ pub mod client {
592549
model
593550
)
594551
} else {
595-
"Set ANTHROPIC_API_KEY, run `coven-code auth login`, \
596-
or use --provider to select a different provider (e.g. --provider openai).".to_string()
552+
"Set ANTHROPIC_API_KEY, or use --provider to select a different provider \
553+
(e.g. --provider openai). Anthropic OAuth login is disabled until Coven Code \
554+
has its own OAuth client.".to_string()
597555
};
598556
return Err(ClaudeError::Auth(
599557
format!("No API key for the selected model. {}", hint)
@@ -605,7 +563,6 @@ pub mod client {
605563
}
606564

607565
request.stream = false;
608-
self.apply_oauth_stealth(&mut request);
609566
let body = serde_json::to_value(&request).map_err(ClaudeError::Json)?;
610567

611568
let resp = self.send_with_retry(&body).await?;
@@ -696,8 +653,9 @@ pub mod client {
696653
} else if model.starts_with("llama") {
697654
format!("Model '{}' looks like a Llama model. Use `--provider groq` or `--provider ollama` for local.", model)
698655
} else {
699-
"Set ANTHROPIC_API_KEY, run `coven-code auth login`, \
700-
or use --provider to select a different provider (e.g. --provider openai).".to_string()
656+
"Set ANTHROPIC_API_KEY, or use --provider to select a different provider \
657+
(e.g. --provider openai). Anthropic OAuth login is disabled until Coven Code \
658+
has its own OAuth client.".to_string()
701659
};
702660
return Err(ClaudeError::Auth(
703661
format!("No API key for the selected model. {}", hint)
@@ -711,7 +669,6 @@ pub mod client {
711669
}
712670

713671
request.stream = true;
714-
self.apply_oauth_stealth(&mut request);
715672
let body = serde_json::to_value(&request).map_err(ClaudeError::Json)?;
716673

717674
let resp = self.send_with_retry(&body).await?;
@@ -756,15 +713,7 @@ pub mod client {
756713
.header("anthropic-version", &self.config.api_version)
757714
.header("content-type", "application/json");
758715
if self.config.use_bearer_auth {
759-
let ua = format!(
760-
"claude-cli/{}",
761-
claurst_core::oauth_config::CLAUDE_CODE_VERSION_FOR_OAUTH
762-
);
763-
req = req
764-
.header("anthropic-beta", claurst_core::oauth_config::OAUTH_BETA_FLAGS.join(","))
765-
.header("user-agent", ua)
766-
.header("x-app", "cli")
767-
.header("Authorization", format!("Bearer {}", &self.config.api_key));
716+
req = req.header("Authorization", format!("Bearer {}", &self.config.api_key));
768717
} else {
769718
req = req.header("x-api-key", &self.config.api_key);
770719
}
@@ -803,25 +752,10 @@ pub mod client {
803752
loop {
804753
attempts += 1;
805754

806-
// Use Bearer auth for Claude.ai OAuth tokens; x-api-key for regular keys.
755+
// Use Bearer auth when explicitly configured; otherwise use x-api-key.
807756
let use_oauth = self.config.use_bearer_auth;
808757

809-
// On the OAuth path we impersonate Claude Code: prepend the
810-
// required beta flags, advertise `claude-cli/<ver>` as the
811-
// user-agent, and drop the CCH billing header (the real
812-
// Claude Code client sends it but the API does not require
813-
// it for OAuth tokens, and emitting it would fingerprint
814-
// mismatch against the impersonated UA).
815-
let anthropic_beta = if use_oauth {
816-
let mut flags: Vec<&str> =
817-
claurst_core::oauth_config::OAUTH_BETA_FLAGS.to_vec();
818-
if !self.config.beta_features.is_empty() {
819-
flags.push(&self.config.beta_features);
820-
}
821-
flags.join(",")
822-
} else {
823-
self.config.beta_features.clone()
824-
};
758+
let anthropic_beta = self.config.beta_features.clone();
825759

826760
let mut req = self
827761
.http
@@ -832,14 +766,7 @@ pub mod client {
832766
.header("accept", "text/event-stream");
833767

834768
if use_oauth {
835-
let ua = format!(
836-
"claude-cli/{}",
837-
claurst_core::oauth_config::CLAUDE_CODE_VERSION_FOR_OAUTH
838-
);
839-
req = req
840-
.header("user-agent", ua)
841-
.header("x-app", "cli")
842-
.header("Authorization", format!("Bearer {}", &self.config.api_key));
769+
req = req.header("Authorization", format!("Bearer {}", &self.config.api_key));
843770
} else {
844771
// Compute CCH billing hash and attach on the API-key path
845772
// only — this is the codepath the official client uses

src-rust/crates/cli/src/main.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ async fn main() -> anyhow::Result<()> {
584584
- Set GOOGLE_API_KEY for Google Gemini\n\
585585
- Set GROQ_API_KEY for Groq (fast, free tier available)\n\
586586
- Run `coven-code --provider ollama` for local models (no key needed)\n\
587-
- Run `coven-code auth login` for Anthropic OAuth"
587+
- Anthropic OAuth login is disabled until Coven Code has its own OAuth client"
588588
);
589589
} else {
590590
(String::new(), false)
@@ -3901,7 +3901,7 @@ async fn run_interactive(
39013901
// Called before Cli::parse() so it doesn't conflict with positional `prompt`.
39023902
//
39033903
// Usage:
3904-
// claude auth login [--console] — OAuth PKCE login (claude.ai by default)
3904+
// claude auth login [--console] — Anthropic OAuth is disabled until Coven Code has its own client
39053905
// claude auth logout — Clear stored credentials
39063906
// claude auth status [--json] — Show authentication status
39073907

@@ -4314,18 +4314,23 @@ async fn auth_status(json_output: bool) {
43144314
.filter(|tokens| !tokens.uses_bearer_auth() && tokens.api_key.is_some())
43154315
.map(|_| "/login managed key".to_string())
43164316
});
4317-
let token_source = oauth_tokens.as_ref().map(|tokens| {
4317+
let usable_oauth_tokens = oauth_tokens
4318+
.as_ref()
4319+
.filter(|tokens| !tokens.uses_bearer_auth());
4320+
let disabled_bearer_token = oauth_tokens
4321+
.as_ref()
4322+
.is_some_and(|tokens| tokens.uses_bearer_auth());
4323+
let token_source = usable_oauth_tokens.map(|tokens| {
43184324
if tokens.uses_bearer_auth() {
43194325
"claude.ai".to_string()
43204326
} else {
43214327
"console_oauth".to_string()
43224328
}
43234329
});
4324-
let login_method = oauth_tokens
4325-
.as_ref()
4330+
let login_method = usable_oauth_tokens
43264331
.and_then(|tokens| subscription_label(tokens.subscription_type.as_deref()))
43274332
.or_else(|| {
4328-
oauth_tokens.as_ref().map(|tokens| {
4333+
usable_oauth_tokens.map(|tokens| {
43294334
if tokens.uses_bearer_auth() {
43304335
"Coven Code Account".to_string()
43314336
} else {
@@ -4334,7 +4339,7 @@ async fn auth_status(json_output: bool) {
43344339
})
43354340
})
43364341
.or_else(|| api_key_source.as_ref().map(|_| "API Key".to_string()));
4337-
let billing_mode = oauth_tokens.as_ref().map_or_else(
4342+
let billing_mode = usable_oauth_tokens.map_or_else(
43384343
|| {
43394344
if api_key_source.is_some() {
43404345
"API".to_string()
@@ -4351,7 +4356,7 @@ async fn auth_status(json_output: bool) {
43514356
},
43524357
);
43534358

4354-
let (auth_method, logged_in) = if let Some(ref tokens) = oauth_tokens {
4359+
let (auth_method, logged_in) = if let Some(tokens) = usable_oauth_tokens {
43554360
let method = if tokens.uses_bearer_auth() {
43564361
"claude.ai"
43574362
} else {
@@ -4381,6 +4386,10 @@ async fn auth_status(json_output: bool) {
43814386
if let Some(ref method) = login_method {
43824387
obj["loginMethod"] = serde_json::Value::String(method.clone());
43834388
}
4389+
if disabled_bearer_token {
4390+
obj["disabledTokenSource"] =
4391+
serde_json::Value::String("claude.ai OAuth token disabled".to_string());
4392+
}
43844393

43854394
if let Some(ref tokens) = oauth_tokens {
43864395
obj["email"] = json_null_or_string(&tokens.email);
@@ -4392,7 +4401,11 @@ async fn auth_status(json_output: bool) {
43924401
} else {
43934402
if !logged_in {
43944403
let hint = if active_provider == "anthropic" {
4395-
"Run `coven-code auth login` or set ANTHROPIC_API_KEY.".to_string()
4404+
if disabled_bearer_token {
4405+
"Stored claude.ai OAuth tokens are disabled; set ANTHROPIC_API_KEY.".to_string()
4406+
} else {
4407+
"Set ANTHROPIC_API_KEY.".to_string()
4408+
}
43964409
} else if let Some(env_var) =
43974410
claurst_core::config::primary_api_key_env_var_for_provider(active_provider)
43984411
{

0 commit comments

Comments
 (0)