code_mesh_core/auth/
anthropic.rs1use 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
14pub 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 pub async fn start_oauth_flow(&self) -> crate::Result<OAuthFlow> {
26 let verifier = generate_code_verifier();
28 let challenge = generate_code_challenge(&verifier);
29
30 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 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(¶ms)
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 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(¶ms)
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 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#[derive(Debug)]
150pub struct OAuthFlow {
151 pub auth_url: String,
152 pub verifier: String,
153 pub state: String,
154}
155
156#[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
176fn 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
183fn 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
191fn 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}