claude_agent/auth/
config.rs

1//! OAuth configuration and request building for Claude Code CLI authentication.
2
3use std::collections::HashMap;
4
5use crate::client::BetaConfig;
6
7pub const DEFAULT_USER_AGENT: &str = "claude-cli/2.0.76 (external, cli)";
8pub const DEFAULT_APP_IDENTIFIER: &str = "cli";
9pub const CLAUDE_CODE_BETA: &str = "claude-code-20250219";
10
11#[derive(Debug, Clone)]
12pub struct OAuthConfig {
13    pub user_agent: String,
14    pub app_identifier: String,
15    pub url_params: HashMap<String, String>,
16    pub extra_headers: HashMap<String, String>,
17}
18
19impl Default for OAuthConfig {
20    fn default() -> Self {
21        Self {
22            user_agent: DEFAULT_USER_AGENT.to_string(),
23            app_identifier: DEFAULT_APP_IDENTIFIER.to_string(),
24            url_params: [("beta".to_string(), "true".to_string())]
25                .into_iter()
26                .collect(),
27            extra_headers: [(
28                "anthropic-dangerous-direct-browser-access".to_string(),
29                "true".to_string(),
30            )]
31            .into_iter()
32            .collect(),
33        }
34    }
35}
36
37impl OAuthConfig {
38    pub fn from_env() -> Self {
39        let mut config = Self::default();
40
41        if let Ok(ua) = std::env::var("CLAUDE_AGENT_USER_AGENT") {
42            config.user_agent = ua;
43        }
44        if let Ok(app) = std::env::var("CLAUDE_AGENT_APP_IDENTIFIER") {
45            config.app_identifier = app;
46        }
47
48        config
49    }
50
51    pub fn builder() -> OAuthConfigBuilder {
52        OAuthConfigBuilder::default()
53    }
54
55    pub fn build_beta_header(&self, base: &BetaConfig) -> String {
56        let mut beta = base.clone();
57        beta.add(crate::client::BetaFeature::OAuth);
58        beta.add_custom(CLAUDE_CODE_BETA);
59        beta.header_value().unwrap_or_default()
60    }
61
62    pub fn build_url(&self, base_url: &str, endpoint: &str) -> String {
63        let url = format!("{}{}", base_url, endpoint);
64        if self.url_params.is_empty() {
65            url
66        } else {
67            let params: Vec<String> = self
68                .url_params
69                .iter()
70                .map(|(k, v)| format!("{}={}", k, v))
71                .collect();
72            format!("{}?{}", url, params.join("&"))
73        }
74    }
75
76    pub fn apply_headers(
77        &self,
78        req: reqwest::RequestBuilder,
79        token: &str,
80        api_version: &str,
81        beta: &BetaConfig,
82    ) -> reqwest::RequestBuilder {
83        let mut r = req
84            .header("Authorization", format!("Bearer {}", token))
85            .header("anthropic-version", api_version)
86            .header("content-type", "application/json")
87            .header("user-agent", &self.user_agent)
88            .header("x-app", &self.app_identifier);
89
90        for (k, v) in &self.extra_headers {
91            r = r.header(k.as_str(), v.as_str());
92        }
93
94        let beta_header = self.build_beta_header(beta);
95        if !beta_header.is_empty() {
96            r = r.header("anthropic-beta", beta_header);
97        }
98
99        r
100    }
101}
102
103pub struct OAuthConfigBuilder {
104    config: OAuthConfig,
105}
106
107impl Default for OAuthConfigBuilder {
108    fn default() -> Self {
109        Self {
110            config: OAuthConfig::from_env(),
111        }
112    }
113}
114
115impl OAuthConfigBuilder {
116    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
117        self.config.user_agent = ua.into();
118        self
119    }
120
121    pub fn app_identifier(mut self, app: impl Into<String>) -> Self {
122        self.config.app_identifier = app.into();
123        self
124    }
125
126    pub fn url_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
127        self.config.url_params.insert(key.into(), value.into());
128        self
129    }
130
131    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
132        self.config.extra_headers.insert(key.into(), value.into());
133        self
134    }
135
136    pub fn build(self) -> OAuthConfig {
137        self.config
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_default_config() {
147        let config = OAuthConfig::default();
148        assert_eq!(config.user_agent, DEFAULT_USER_AGENT);
149        assert_eq!(config.app_identifier, DEFAULT_APP_IDENTIFIER);
150    }
151
152    #[test]
153    fn test_builder() {
154        let config = OAuthConfig::builder().user_agent("my-app/1.0").build();
155
156        assert_eq!(config.user_agent, "my-app/1.0");
157    }
158
159    #[test]
160    fn test_url_params() {
161        let config = OAuthConfig::default();
162        assert_eq!(config.url_params.get("beta"), Some(&"true".to_string()));
163    }
164}