Skip to main content

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 mut serializer = url::form_urlencoded::Serializer::new(String::new());
68            for (k, v) in &self.url_params {
69                serializer.append_pair(k, v);
70            }
71            format!("{}?{}", url, serializer.finish())
72        }
73    }
74
75    pub fn apply_headers(
76        &self,
77        req: reqwest::RequestBuilder,
78        token: &str,
79        api_version: &str,
80        beta: &BetaConfig,
81    ) -> reqwest::RequestBuilder {
82        let mut r = req
83            .header("Authorization", format!("Bearer {}", token))
84            .header("anthropic-version", api_version)
85            .header("content-type", "application/json")
86            .header("user-agent", &self.user_agent)
87            .header("x-app", &self.app_identifier);
88
89        for (k, v) in &self.extra_headers {
90            r = r.header(k.as_str(), v.as_str());
91        }
92
93        let beta_header = self.build_beta_header(beta);
94        if !beta_header.is_empty() {
95            r = r.header("anthropic-beta", beta_header);
96        }
97
98        r
99    }
100}
101
102pub struct OAuthConfigBuilder {
103    config: OAuthConfig,
104}
105
106impl Default for OAuthConfigBuilder {
107    fn default() -> Self {
108        Self {
109            config: OAuthConfig::from_env(),
110        }
111    }
112}
113
114impl OAuthConfigBuilder {
115    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
116        self.config.user_agent = ua.into();
117        self
118    }
119
120    pub fn app_identifier(mut self, app: impl Into<String>) -> Self {
121        self.config.app_identifier = app.into();
122        self
123    }
124
125    pub fn url_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
126        self.config.url_params.insert(key.into(), value.into());
127        self
128    }
129
130    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
131        self.config.extra_headers.insert(key.into(), value.into());
132        self
133    }
134
135    pub fn build(self) -> OAuthConfig {
136        self.config
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_default_config() {
146        let config = OAuthConfig::default();
147        assert_eq!(config.user_agent, DEFAULT_USER_AGENT);
148        assert_eq!(config.app_identifier, DEFAULT_APP_IDENTIFIER);
149    }
150
151    #[test]
152    fn test_builder() {
153        let config = OAuthConfig::builder().user_agent("my-app/1.0").build();
154
155        assert_eq!(config.user_agent, "my-app/1.0");
156    }
157
158    #[test]
159    fn test_url_params() {
160        let config = OAuthConfig::default();
161        assert_eq!(config.url_params.get("beta"), Some(&"true".to_string()));
162    }
163}