Skip to main content

ai_agent/constants/
oauth.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/constants/oauth.ts
2//! OAuth configuration constants
3
4use once_cell::sync::Lazy;
5use std::env;
6
7pub const CLAUDE_AI_INFERENCE_SCOPE: &str = "user:inference";
8pub const CLAUDE_AI_PROFILE_SCOPE: &str = "user:profile";
9const CONSOLE_SCOPE: &str = "org:create_api_key";
10pub const OAUTH_BETA_HEADER: &str = "oauth-2025-04-20";
11
12pub const CONSOLE_OAUTH_SCOPES: &[&str] = &[CONSOLE_SCOPE, CLAUDE_AI_PROFILE_SCOPE];
13
14pub const CLAUDE_AI_OAUTH_SCOPES: &[&str] = &[
15    CLAUDE_AI_PROFILE_SCOPE,
16    CLAUDE_AI_INFERENCE_SCOPE,
17    "user:sessions:claude_code",
18    "user:mcp_servers",
19    "user:file_upload",
20];
21
22pub fn get_all_oauth_scopes() -> Vec<&'static str> {
23    let mut scopes: Vec<&str> = CONSOLE_OAUTH_SCOPES.to_vec();
24    for scope in CLAUDE_AI_OAUTH_SCOPES {
25        if !scopes.contains(scope) {
26            scopes.push(scope);
27        }
28    }
29    scopes
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub enum OauthConfigType {
34    Prod,
35    Staging,
36    Local,
37}
38
39fn get_oauth_config_type() -> OauthConfigType {
40    let user_type = env::var("USER_TYPE").unwrap_or_default();
41    if user_type == "ant" {
42        let use_local = env::var("USE_LOCAL_OAUTH")
43            .map(|v| v != "0" && v.to_lowercase() != "false")
44            .unwrap_or(false);
45        if use_local {
46            return OauthConfigType::Local;
47        }
48        let use_staging = env::var("USE_STAGING_OAUTH")
49            .map(|v| v != "0" && v.to_lowercase() != "false")
50            .unwrap_or(false);
51        if use_staging {
52            return OauthConfigType::Staging;
53        }
54    }
55    OauthConfigType::Prod
56}
57
58pub fn file_suffix_for_oauth_config() -> String {
59    if env::var("AI_CODE_CUSTOM_OAUTH_URL").is_ok() {
60        return "-custom-oauth".to_string();
61    }
62    match get_oauth_config_type() {
63        OauthConfigType::Local => "-local-oauth".to_string(),
64        OauthConfigType::Staging => "-staging-oauth".to_string(),
65        OauthConfigType::Prod => "".to_string(),
66    }
67}
68
69#[derive(Debug, Clone)]
70pub struct OauthConfig {
71    pub base_api_url: String,
72    pub console_authorize_url: String,
73    pub claude_ai_authorize_url: String,
74    pub claude_ai_origin: String,
75    pub token_url: String,
76    pub api_key_url: String,
77    pub roles_url: String,
78    pub console_success_url: String,
79    pub claudeai_success_url: String,
80    pub manual_redirect_url: String,
81    pub client_id: String,
82    pub oauth_file_suffix: String,
83    pub mcp_proxy_url: String,
84    pub mcp_proxy_path: String,
85}
86
87pub const MCP_CLIENT_METADATA_URL: &str = "https://claude.ai/oauth/claude-code-client-metadata";
88
89const ALLOWED_OAUTH_BASE_URLS: &[&str] = &[
90    "https://beacon.claude-ai.staging.ant.dev",
91    "https://claude.fedstart.com",
92    "https://claude-staging.fedstart.com",
93];
94
95fn get_local_oauth_config() -> OauthConfig {
96    let api = env::var("CLAUDE_LOCAL_OAUTH_API_BASE")
97        .map(|s| s.trim_end_matches('/').to_string())
98        .unwrap_or_else(|_| "http://localhost:8000".to_string());
99    let apps = env::var("CLAUDE_LOCAL_OAUTH_APPS_BASE")
100        .map(|s| s.trim_end_matches('/').to_string())
101        .unwrap_or_else(|_| "http://localhost:4000".to_string());
102    let console_base = env::var("CLAUDE_LOCAL_OAUTH_CONSOLE_BASE")
103        .map(|s| s.trim_end_matches('/').to_string())
104        .unwrap_or_else(|_| "http://localhost:3000".to_string());
105
106    OauthConfig {
107        base_api_url: api.clone(),
108        console_authorize_url: format!("{}/oauth/authorize", console_base),
109        claude_ai_authorize_url: format!("{}/oauth/authorize", apps),
110        claude_ai_origin: apps,
111        token_url: format!("{}/v1/oauth/token", api),
112        api_key_url: format!("{}/api/oauth/claude_cli/create_api_key", api),
113        roles_url: format!("{}/api/oauth/claude_cli/roles", api),
114        console_success_url: format!(
115            "{}/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code",
116            console_base
117        ),
118        claudeai_success_url: format!("{}/oauth/code/success?app=claude-code", console_base),
119        manual_redirect_url: format!("{}/oauth/code/callback", console_base),
120        client_id: "22422756-60c9-4084-8eb7-27705fd5cf9a".to_string(),
121        oauth_file_suffix: "-local-oauth".to_string(),
122        mcp_proxy_url: "http://localhost:8205".to_string(),
123        mcp_proxy_path: "/v1/toolbox/shttp/mcp/{server_id}".to_string(),
124    }
125}
126
127static PROD_OAUTH_CONFIG: Lazy<OauthConfig> = Lazy::new(|| OauthConfig {
128    base_api_url: "https://api.anthropic.com".to_string(),
129    console_authorize_url: "https://platform.claude.com/oauth/authorize".to_string(),
130    claude_ai_authorize_url: "https://claude.com/cai/oauth/authorize".to_string(),
131    claude_ai_origin: "https://claude.ai".to_string(),
132    token_url: "https://platform.claude.com/v1/oauth/token".to_string(),
133    api_key_url: "https://api.anthropic.com/api/oauth/claude_cli/create_api_key".to_string(),
134    roles_url: "https://api.anthropic.com/api/oauth/claude_cli/roles".to_string(),
135    console_success_url:
136        "https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code"
137            .to_string(),
138    claudeai_success_url: "https://platform.claude.com/oauth/code/success?app=claude-code"
139        .to_string(),
140    manual_redirect_url: "https://platform.claude.com/oauth/code/callback".to_string(),
141    client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e".to_string(),
142    oauth_file_suffix: "".to_string(),
143    mcp_proxy_url: "https://mcp-proxy.anthropic.com".to_string(),
144    mcp_proxy_path: "/v1/mcp/{server_id}".to_string(),
145});
146
147pub fn get_oauth_config() -> OauthConfig {
148    let base_config = match get_oauth_config_type() {
149        OauthConfigType::Local => get_local_oauth_config(),
150        OauthConfigType::Staging => {
151            // For staging, check if we're an ant build
152            if env::var("USER_TYPE").map(|t| t == "ant").unwrap_or(false) {
153                OauthConfig {
154                    base_api_url: "https://api-staging.anthropic.com".to_string(),
155                    console_authorize_url:
156                        "https://platform.staging.ant.dev/oauth/authorize".to_string(),
157                    claude_ai_authorize_url:
158                        "https://claude-ai.staging.ant.dev/oauth/authorize".to_string(),
159                    claude_ai_origin: "https://claude-ai.staging.ant.dev".to_string(),
160                    token_url: "https://platform.staging.ant.dev/v1/oauth/token".to_string(),
161                    api_key_url:
162                        "https://api-staging.anthropic.com/api/oauth/claude_cli/create_api_key"
163                            .to_string(),
164                    roles_url:
165                        "https://api-staging.anthropic.com/api/oauth/claude_cli/roles".to_string(),
166                    console_success_url:
167                        "https://platform.staging.ant.dev/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code"
168                            .to_string(),
169                    claudeai_success_url:
170                        "https://platform.staging.ant.dev/oauth/code/success?app=claude-code"
171                            .to_string(),
172                    manual_redirect_url:
173                        "https://platform.staging.ant.dev/oauth/code/callback".to_string(),
174                    client_id: "22422756-60c9-4084-8eb7-27705fd5cf9a".to_string(),
175                    oauth_file_suffix: "-staging-oauth".to_string(),
176                    mcp_proxy_url: "https://mcp-proxy-staging.anthropic.com".to_string(),
177                    mcp_proxy_path: "/v1/mcp/{server_id}".to_string(),
178                }
179            } else {
180                PROD_OAUTH_CONFIG.clone()
181            }
182        }
183        OauthConfigType::Prod => PROD_OAUTH_CONFIG.clone(),
184    };
185
186    let mut config = base_config;
187
188    // Allow overriding all OAuth URLs to point to an approved FedStart deployment
189    if let Ok(oauth_base_url) = env::var("AI_CODE_CUSTOM_OAUTH_URL") {
190        let base = oauth_base_url.trim_end_matches('/').to_string();
191        if !ALLOWED_OAUTH_BASE_URLS.contains(&base.as_str()) {
192            panic!("AI_CODE_CUSTOM_OAUTH_URL is not an approved endpoint.");
193        }
194        config.base_api_url = base.clone();
195        config.console_authorize_url = format!("{}/oauth/authorize", base);
196        config.claude_ai_authorize_url = format!("{}/oauth/authorize", base);
197        config.claude_ai_origin = base.clone();
198        config.token_url = format!("{}/v1/oauth/token", base);
199        config.api_key_url = format!("{}/api/oauth/claude_cli/create_api_key", base);
200        config.roles_url = format!("{}/api/oauth/claude_cli/roles", base);
201        config.console_success_url = format!("{}/oauth/code/success?app=claude-code", base);
202        config.claudeai_success_url = format!("{}/oauth/code/success?app=claude-code", base);
203        config.manual_redirect_url = format!("{}/oauth/code/callback", base);
204        config.oauth_file_suffix = "-custom-oauth".to_string();
205    }
206
207    // Allow CLIENT_ID override via environment variable
208    if let Ok(client_id_override) = env::var("AI_CODE_OAUTH_CLIENT_ID") {
209        config.client_id = client_id_override;
210    }
211
212    config
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_get_all_oauth_scopes() {
221        let scopes = get_all_oauth_scopes();
222        assert!(scopes.contains(&CONSOLE_SCOPE));
223        assert!(scopes.contains(&CLAUDE_AI_INFERENCE_SCOPE));
224    }
225
226    #[test]
227    fn test_file_suffix_for_oauth_config() {
228        let _ = file_suffix_for_oauth_config();
229    }
230}