use oauth2::PkceCodeChallenge;
use rand::Rng;
use url::Url;
use super::shared::*;
use crate::types::{ApiKeyResponse, TokenResponse};
use crate::{OAuthConfig, OAuthFlow, OAuthMode, Result, TokenSet};
pub struct OAuthClient {
config: OAuthConfig,
}
impl OAuthClient {
pub fn new(config: OAuthConfig) -> Result<Self> {
Ok(Self { config })
}
pub fn start_flow(&self, mode: OAuthMode) -> Result<OAuthFlow> {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let verifier = pkce_verifier.secret().to_string();
let state = generate_random_state();
let base_domain = match mode {
OAuthMode::Max => "claude.ai",
OAuthMode::Console => "console.anthropic.com",
};
let auth_url = format!("https://{}/oauth/authorize", base_domain);
let mut url = Url::parse(&auth_url)?;
url.query_pairs_mut()
.append_pair("code", "true")
.append_pair("client_id", &self.config.client_id)
.append_pair("response_type", "code")
.append_pair("redirect_uri", REDIRECT_URI)
.append_pair("scope", SCOPE)
.append_pair("code_challenge", pkce_challenge.as_str())
.append_pair("code_challenge_method", "S256")
.append_pair("state", &state);
Ok(OAuthFlow {
authorization_url: url.to_string(),
verifier,
state,
mode,
})
}
pub fn exchange_code(
&self,
code_with_state: &str,
expected_state: &str,
verifier: &str,
) -> Result<TokenSet> {
let (code, state) = parse_code_and_state(code_with_state, expected_state)?;
validate_code(&code)?;
validate_state(&state)?;
validate_verifier(verifier)?;
let client = reqwest::blocking::Client::new();
let request_body = build_token_request(&code, &state, verifier, &self.config.client_id);
let response = client.post(TOKEN_URL).json(&request_body).send()?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().unwrap_or_default();
return Err(create_http_error(status, &body));
}
let token_response: TokenResponse = response.json()?;
let tokens = TokenSet::from(token_response);
tokens.validate().map_err(|e| {
crate::AnthropicAuthError::OAuth(format!("Invalid token response: {}", e))
})?;
Ok(tokens)
}
pub fn refresh_token(&self, refresh_token: &str) -> Result<TokenSet> {
if refresh_token.is_empty() {
return Err(crate::AnthropicAuthError::OAuth(
"Refresh token is empty".to_string(),
));
}
let client = reqwest::blocking::Client::new();
let request_body = build_refresh_request(refresh_token, &self.config.client_id);
let response = client.post(TOKEN_URL).json(&request_body).send()?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().unwrap_or_default();
return Err(create_http_error(status, &body));
}
let token_response: TokenResponse = response.json()?;
let tokens = TokenSet::from(token_response);
tokens.validate().map_err(|e| {
crate::AnthropicAuthError::OAuth(format!("Invalid token response: {}", e))
})?;
Ok(tokens)
}
pub fn create_api_key(&self, access_token: &str) -> Result<String> {
validate_access_token(access_token)?;
let client = reqwest::blocking::Client::new();
let request_body = build_api_key_request();
let response = client
.post(API_KEY_URL)
.header("authorization", format!("Bearer {}", access_token))
.json(&request_body)
.send()?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().unwrap_or_default();
return Err(create_http_error(status, &body));
}
let key_response: ApiKeyResponse = response.json()?;
if key_response.raw_key.is_empty() {
return Err(crate::AnthropicAuthError::OAuth(
"Received empty API key from server".to_string(),
));
}
Ok(key_response.raw_key)
}
}
fn generate_random_state() -> String {
let mut rng = rand::thread_rng();
let random_bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&random_bytes,
)
}
fn parse_code_and_state(code_with_state: &str, expected_state: &str) -> Result<(String, String)> {
if let Some(hash_pos) = code_with_state.find('#') {
let code = &code_with_state[..hash_pos];
let returned_state = &code_with_state[hash_pos + 1..];
if returned_state != expected_state {
return Err(crate::AnthropicAuthError::OAuth(format!(
"State mismatch - possible CSRF attack. Expected: {}, Got: {}",
expected_state, returned_state
)));
}
Ok((code.to_string(), returned_state.to_string()))
} else {
Ok((code_with_state.to_string(), expected_state.to_string()))
}
}