From 66bbb34d7f640a6b6ad9759af05f07c02a5c0e6a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Mon, 20 Jul 2026 12:55:30 -0600 Subject: [PATCH] feat: OpenAICompatibleOAuthProvider (config-driven OAuthProvider impl) --- src/client/claude_oauth.rs | 4 +- src/client/gemini_oauth.rs | 4 +- src/client/mod.rs | 1 + src/client/oauth.rs | 7 +- src/client/openai_compatible_oauth.rs | 92 +++++++++++++++++++++++++++ src/client/openai_oauth.rs | 4 +- src/mcp/oauth.rs | 4 +- 7 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 src/client/openai_compatible_oauth.rs diff --git a/src/client/claude_oauth.rs b/src/client/claude_oauth.rs index e552ba8..f7aa97e 100644 --- a/src/client/claude_oauth.rs +++ b/src/client/claude_oauth.rs @@ -25,8 +25,8 @@ impl OAuthProvider for ClaudeOAuthProvider { "https://console.anthropic.com/oauth/code/callback" } - fn scopes(&self) -> &str { - "org:create_api_key user:profile user:inference" + fn scopes(&self) -> String { + "org:create_api_key user:profile user:inference".to_string() } fn extra_authorize_params(&self) -> Vec<(&str, &str)> { diff --git a/src/client/gemini_oauth.rs b/src/client/gemini_oauth.rs index 6a95b06..250750b 100644 --- a/src/client/gemini_oauth.rs +++ b/src/client/gemini_oauth.rs @@ -27,8 +27,8 @@ impl OAuthProvider for GeminiOAuthProvider { "" } - fn scopes(&self) -> &str { - "https://www.googleapis.com/auth/generative-language.peruserquota https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/userinfo.email" + fn scopes(&self) -> String { + "https://www.googleapis.com/auth/generative-language.peruserquota https://www.googleapis.com/auth/generative-language.retriever https://www.googleapis.com/auth/userinfo.email".to_string() } fn client_secret(&self) -> Option<&str> { diff --git a/src/client/mod.rs b/src/client/mod.rs index 1b3361c..48bfac4 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -4,6 +4,7 @@ mod common; mod gemini_oauth; mod message; pub mod oauth; +mod openai_compatible_oauth; mod openai_oauth; #[macro_use] mod macros; diff --git a/src/client/oauth.rs b/src/client/oauth.rs index 86e0edf..fb3f316 100644 --- a/src/client/oauth.rs +++ b/src/client/oauth.rs @@ -118,7 +118,7 @@ pub trait OAuthProvider: Send + Sync { fn authorize_url(&self) -> &str; fn token_url(&self) -> &str; fn redirect_uri(&self) -> &str; - fn scopes(&self) -> &str; + fn scopes(&self) -> String; fn client_secret(&self) -> Option<&str> { None @@ -204,7 +204,8 @@ async fn run_pkce_flow(provider: &dyn OAuthProvider, client_name: &str) -> Resul (provider.redirect_uri().to_string(), false) }; - let encoded_scopes = urlencoding::encode(provider.scopes()); + let scopes = provider.scopes(); + let encoded_scopes = urlencoding::encode(&scopes); let encoded_redirect = urlencoding::encode(&redirect_uri); let mut authorize_url = format!( @@ -313,7 +314,7 @@ async fn run_client_credentials_flow( ("client_id", provider.client_id()), ]; if !scopes.is_empty() { - params.push(("scope", scopes)); + params.push(("scope", scopes.as_str())); } let request = build_token_request(&client, provider, ¶ms); diff --git a/src/client/openai_compatible_oauth.rs b/src/client/openai_compatible_oauth.rs new file mode 100644 index 0000000..2e5974e --- /dev/null +++ b/src/client/openai_compatible_oauth.rs @@ -0,0 +1,92 @@ +use super::oauth::{OAuthConfig, OAuthFlow, OAuthProvider, TokenRequestFormat}; + +pub struct OpenAICompatibleOAuthProvider { + pub config: OAuthConfig, + pub client_name: String, +} + +impl OAuthProvider for OpenAICompatibleOAuthProvider { + fn provider_name(&self) -> &str { + &self.client_name + } + + fn client_id(&self) -> &str { + &self.config.client_id + } + + fn authorize_url(&self) -> &str { + self.config.authorize_url.as_deref().unwrap_or("") + } + + fn token_url(&self) -> &str { + &self.config.token_url + } + + fn redirect_uri(&self) -> &str { + self.config.redirect_uri.as_deref().unwrap_or("") + } + + fn scopes(&self) -> String { + self.config.scopes.join(" ") + } + + fn client_secret(&self) -> Option<&str> { + self.config.client_secret.as_deref() + } + + fn extra_authorize_params(&self) -> Vec<(&str, &str)> { + self.config + .extra_authorize_params + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect() + } + + fn token_request_format(&self) -> TokenRequestFormat { + self.config + .token_request_format + .unwrap_or(TokenRequestFormat::FormUrlEncoded) + } + + fn uses_localhost_redirect(&self) -> bool { + self.config.redirect_uri.is_none() && self.config.redirect_port.is_none() + } + + fn fixed_redirect_uri(&self) -> Option { + if let Some(uri) = &self.config.redirect_uri { + return Some(uri.clone()); + } + if let Some(port) = self.config.redirect_port { + return Some(format!("http://127.0.0.1:{port}/callback")); + } + None + } + + fn extra_token_headers(&self) -> Vec<(&str, &str)> { + self.config + .extra_token_headers + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect() + } + + fn extra_request_headers(&self) -> Vec<(&str, &str)> { + self.config + .extra_request_headers + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect() + } + + fn flow(&self) -> OAuthFlow { + self.config.flow + } + + fn echo_pkce_in_token_exchange(&self) -> bool { + self.config.echo_pkce_in_token_exchange + } + + fn include_state_in_token_exchange(&self) -> bool { + self.config.include_state_in_token_exchange + } +} diff --git a/src/client/openai_oauth.rs b/src/client/openai_oauth.rs index 45126ce..190e825 100644 --- a/src/client/openai_oauth.rs +++ b/src/client/openai_oauth.rs @@ -26,8 +26,8 @@ impl OAuthProvider for OpenAIOAuthProvider { "http://localhost:1455/auth/callback" } - fn scopes(&self) -> &str { - "openid profile email offline_access" + fn scopes(&self) -> String { + "openid profile email offline_access".to_string() } fn token_request_format(&self) -> TokenRequestFormat { diff --git a/src/mcp/oauth.rs b/src/mcp/oauth.rs index dbf3e40..4bccecb 100644 --- a/src/mcp/oauth.rs +++ b/src/mcp/oauth.rs @@ -61,8 +61,8 @@ impl OAuthProvider for McpOAuthProvider { "" } - fn scopes(&self) -> &str { - &self.scopes + fn scopes(&self) -> String { + self.scopes.clone() } fn token_request_format(&self) -> TokenRequestFormat {