1use crate::error::{Error, Result};
6use url::Url;
7
8#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
10pub struct OAuthConfig {
11 pub enabled: bool,
13 pub client_id: Option<String>,
15 pub client_secret: Option<String>,
17 pub redirect_uri: Option<String>,
19 pub authorization_endpoint: Option<String>,
21 pub token_endpoint: Option<String>,
23 pub scopes: Vec<String>,
25 pub provider: OAuthProvider,
27}
28
29#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
31pub enum OAuthProvider {
32 Custom,
34 GitHub,
36 Google,
38 Keycloak,
40}
41
42impl Default for OAuthConfig {
43 fn default() -> Self {
44 Self {
45 enabled: false,
46 client_id: None,
47 client_secret: None,
48 redirect_uri: None,
49 authorization_endpoint: None,
50 token_endpoint: None,
51 scopes: vec![
52 "openid".to_string(),
53 "profile".to_string(),
54 "email".to_string(),
55 ],
56 provider: OAuthProvider::Custom,
57 }
58 }
59}
60
61impl OAuthConfig {
62 #[must_use]
64 pub fn github(client_id: String, client_secret: String, redirect_uri: String) -> Self {
65 Self {
66 enabled: true,
67 client_id: Some(client_id),
68 client_secret: Some(client_secret),
69 redirect_uri: Some(redirect_uri),
70 authorization_endpoint: Some("https://github.com/login/oauth/authorize".to_string()),
71 token_endpoint: Some("https://github.com/login/oauth/access_token".to_string()),
72 scopes: vec!["read:user".to_string(), "user:email".to_string()],
73 provider: OAuthProvider::GitHub,
74 }
75 }
76
77 #[must_use]
79 pub fn google(client_id: String, client_secret: String, redirect_uri: String) -> Self {
80 Self {
81 enabled: true,
82 client_id: Some(client_id),
83 client_secret: Some(client_secret),
84 redirect_uri: Some(redirect_uri),
85 authorization_endpoint: Some(
86 "https://accounts.google.com/o/oauth2/v2/auth".to_string(),
87 ),
88 token_endpoint: Some("https://oauth2.googleapis.com/token".to_string()),
89 scopes: vec![
90 "openid".to_string(),
91 "https://www.googleapis.com/auth/userinfo.profile".to_string(),
92 "https://www.googleapis.com/auth/userinfo.email".to_string(),
93 ],
94 provider: OAuthProvider::Google,
95 }
96 }
97
98 #[must_use]
100 pub fn keycloak(
101 client_id: String,
102 client_secret: String,
103 redirect_uri: String,
104 base_url: &str,
105 realm: &str,
106 ) -> Self {
107 let base = base_url.trim_end_matches('/');
108 Self {
109 enabled: true,
110 client_id: Some(client_id),
111 client_secret: Some(client_secret),
112 redirect_uri: Some(redirect_uri),
113 authorization_endpoint: Some(format!(
114 "{base}/realms/{realm}/protocol/openid-connect/auth"
115 )),
116 token_endpoint: Some(format!(
117 "{base}/realms/{realm}/protocol/openid-connect/token"
118 )),
119 scopes: vec![
120 "openid".to_string(),
121 "profile".to_string(),
122 "email".to_string(),
123 ],
124 provider: OAuthProvider::Keycloak,
125 }
126 }
127
128 pub fn validate(&self) -> Result<()> {
130 if !self.enabled {
131 return Ok(());
132 }
133
134 if self.client_id.is_none() {
135 return Err(Error::Config("client_id is required".to_string()));
136 }
137
138 if self.client_secret.is_none() {
139 return Err(Error::Config("client_secret is required".to_string()));
140 }
141
142 if self.redirect_uri.is_none() {
143 return Err(Error::Config("redirect_uri is required".to_string()));
144 }
145
146 if self.authorization_endpoint.is_none() {
147 return Err(Error::Config(
148 "authorization_endpoint is required".to_string(),
149 ));
150 }
151
152 if self.token_endpoint.is_none() {
153 return Err(Error::Config("token_endpoint is required".to_string()));
154 }
155
156 if let Some(uri) = &self.redirect_uri {
158 Url::parse(uri).map_err(|e| Error::Config(format!("Invalid redirect_uri: {e}")))?;
159 }
160
161 if let Some(endpoint) = &self.authorization_endpoint {
162 Url::parse(endpoint)
163 .map_err(|e| Error::Config(format!("Invalid authorization_endpoint: {e}")))?;
164 }
165
166 if let Some(endpoint) = &self.token_endpoint {
167 Url::parse(endpoint)
168 .map_err(|e| Error::Config(format!("Invalid token_endpoint: {e}")))?;
169 }
170
171 Ok(())
172 }
173
174 #[cfg(feature = "auth")]
176 pub fn to_mcp_config(&self) -> Result<()> {
177 if !self.enabled {
178 return Err(Error::Config("OAuth is not enabled".to_string()));
179 }
180
181 Ok(())
183 }
184
185 #[cfg(not(feature = "auth"))]
187 pub fn to_mcp_config(&self) -> Result<()> {
188 Err(Error::Config("OAuth feature is not enabled".to_string()))
189 }
190}
191
192#[derive(Default)]
194pub struct AuthManager {
195 config: OAuthConfig,
196}
197
198impl AuthManager {
199 pub fn new(config: OAuthConfig) -> Result<Self> {
201 config.validate()?;
202 Ok(Self { config })
203 }
204
205 #[must_use]
207 pub fn is_enabled(&self) -> bool {
208 self.config.enabled
209 }
210
211 #[must_use]
213 pub fn config(&self) -> &OAuthConfig {
214 &self.config
215 }
216}
217
218#[derive(Default)]
220pub struct TokenStore {
221 tokens: std::sync::RwLock<std::collections::HashMap<String, TokenInfo>>,
222}
223
224#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
226pub struct TokenInfo {
227 pub access_token: String,
229 pub refresh_token: Option<String>,
231 pub expires_at: chrono::DateTime<chrono::Utc>,
233 pub scopes: Vec<String>,
235 pub user_id: Option<String>,
237 pub user_email: Option<String>,
239}
240
241impl TokenStore {
242 #[must_use]
244 pub fn new() -> Self {
245 Self::default()
246 }
247
248 pub fn store_token(&self, key: String, token: TokenInfo) {
250 let mut tokens = self.tokens.write().unwrap();
251 tokens.insert(key, token);
252 }
253
254 pub fn get_token(&self, key: &str) -> Option<TokenInfo> {
256 let tokens = self.tokens.read().unwrap();
257 tokens.get(key).cloned()
258 }
259
260 pub fn remove_token(&self, key: &str) {
262 let mut tokens = self.tokens.write().unwrap();
263 tokens.remove(key);
264 }
265
266 pub fn cleanup_expired(&self) {
268 let now = chrono::Utc::now();
269 let mut tokens = self.tokens.write().unwrap();
270 tokens.retain(|_, token| token.expires_at > now);
271 }
272}