code_mesh_core/auth/
anthropic.rs

1//! Anthropic authentication implementation
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use sha2::{Sha256, Digest};
6use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
7use super::{Auth, AuthCredentials, AuthStorage};
8
9const ANTHROPIC_OAUTH_URL: &str = "https://auth.anthropic.com/authorize";
10const ANTHROPIC_TOKEN_URL: &str = "https://auth.anthropic.com/oauth/token";
11const CLIENT_ID: &str = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
12const REDIRECT_URI: &str = "http://localhost:60023/callback";
13
14/// Anthropic authentication provider
15pub struct AnthropicAuth {
16    storage: Box<dyn AuthStorage>,
17}
18
19impl AnthropicAuth {
20    pub fn new(storage: Box<dyn AuthStorage>) -> Self {
21        Self { storage }
22    }
23    
24    /// Start OAuth flow with PKCE
25    pub async fn start_oauth_flow(&self) -> crate::Result<OAuthFlow> {
26        // Generate PKCE challenge
27        let verifier = generate_code_verifier();
28        let challenge = generate_code_challenge(&verifier);
29        
30        // Build authorization URL
31        let auth_url = format!(
32            "{}?client_id={}&redirect_uri={}&response_type=code&scope=read:models&code_challenge={}&code_challenge_method=S256",
33            ANTHROPIC_OAUTH_URL,
34            CLIENT_ID,
35            urlencoding::encode(REDIRECT_URI),
36            challenge
37        );
38        
39        Ok(OAuthFlow {
40            auth_url,
41            verifier,
42            state: generate_state(),
43        })
44    }
45    
46    /// Exchange authorization code for tokens
47    pub async fn exchange_code(&self, code: &str, verifier: &str) -> crate::Result<TokenResponse> {
48        let client = reqwest::Client::new();
49        
50        let params = [
51            ("grant_type", "authorization_code"),
52            ("code", code),
53            ("redirect_uri", REDIRECT_URI),
54            ("client_id", CLIENT_ID),
55            ("code_verifier", verifier),
56        ];
57        
58        let response = client
59            .post(ANTHROPIC_TOKEN_URL)
60            .form(&params)
61            .send()
62            .await?;
63        
64        if !response.status().is_success() {
65            let error = response.text().await?;
66            return Err(crate::Error::AuthenticationFailed(format!(
67                "Token exchange failed: {}",
68                error
69            )));
70        }
71        
72        let tokens: TokenResponse = response.json().await?;
73        Ok(tokens)
74    }
75    
76    /// Refresh access token using refresh token
77    pub async fn refresh_token(&self, refresh_token: &str) -> crate::Result<TokenResponse> {
78        let client = reqwest::Client::new();
79        
80        let params = [
81            ("grant_type", "refresh_token"),
82            ("refresh_token", refresh_token),
83            ("client_id", CLIENT_ID),
84        ];
85        
86        let response = client
87            .post(ANTHROPIC_TOKEN_URL)
88            .form(&params)
89            .send()
90            .await?;
91        
92        if !response.status().is_success() {
93            let error = response.text().await?;
94            return Err(crate::Error::AuthenticationFailed(format!(
95                "Token refresh failed: {}",
96                error
97            )));
98        }
99        
100        let tokens: TokenResponse = response.json().await?;
101        Ok(tokens)
102    }
103}
104
105#[async_trait]
106impl Auth for AnthropicAuth {
107    fn provider_id(&self) -> &str {
108        "anthropic"
109    }
110    
111    async fn get_credentials(&self) -> crate::Result<AuthCredentials> {
112        if let Some(creds) = self.storage.get(self.provider_id()).await? {
113            // Check if OAuth token needs refresh
114            if creds.is_expired() {
115                if let AuthCredentials::OAuth { refresh_token: Some(refresh), .. } = &creds {
116                    let tokens = self.refresh_token(refresh).await?;
117                    let expires_at = tokens.expires_at();
118                    let new_creds = AuthCredentials::OAuth {
119                        access_token: tokens.access_token,
120                        refresh_token: tokens.refresh_token,
121                        expires_at,
122                    };
123                    self.storage.set(self.provider_id(), new_creds.clone()).await?;
124                    return Ok(new_creds);
125                }
126            }
127            Ok(creds)
128        } else {
129            Err(crate::Error::AuthenticationFailed(
130                "No credentials found. Please run 'code-mesh auth login'".to_string()
131            ))
132        }
133    }
134    
135    async fn set_credentials(&self, credentials: AuthCredentials) -> crate::Result<()> {
136        self.storage.set(self.provider_id(), credentials).await
137    }
138    
139    async fn remove_credentials(&self) -> crate::Result<()> {
140        self.storage.remove(self.provider_id()).await
141    }
142    
143    async fn has_credentials(&self) -> bool {
144        self.storage.get(self.provider_id()).await.ok().flatten().is_some()
145    }
146}
147
148/// OAuth flow information
149#[derive(Debug)]
150pub struct OAuthFlow {
151    pub auth_url: String,
152    pub verifier: String,
153    pub state: String,
154}
155
156/// Token response from OAuth
157#[derive(Debug, Deserialize)]
158pub struct TokenResponse {
159    pub access_token: String,
160    pub token_type: String,
161    pub expires_in: Option<u64>,
162    pub refresh_token: Option<String>,
163}
164
165impl TokenResponse {
166    pub fn expires_at(&self) -> Option<u64> {
167        self.expires_in.map(|expires_in| {
168            std::time::SystemTime::now()
169                .duration_since(std::time::UNIX_EPOCH)
170                .unwrap()
171                .as_secs() + expires_in
172        })
173    }
174}
175
176/// Generate code verifier for PKCE
177fn generate_code_verifier() -> String {
178    use rand::Rng;
179    let bytes: Vec<u8> = (0..32).map(|_| rand::thread_rng().gen()).collect();
180    URL_SAFE_NO_PAD.encode(&bytes)
181}
182
183/// Generate code challenge from verifier
184fn generate_code_challenge(verifier: &str) -> String {
185    let mut hasher = Sha256::new();
186    hasher.update(verifier.as_bytes());
187    let result = hasher.finalize();
188    URL_SAFE_NO_PAD.encode(&result)
189}
190
191/// Generate random state for OAuth
192fn generate_state() -> String {
193    use rand::Rng;
194    let bytes: Vec<u8> = (0..16).map(|_| rand::thread_rng().gen()).collect();
195    URL_SAFE_NO_PAD.encode(&bytes)
196}