Skip to main content

crates_docs/server/
auth.rs

1//! OAuth authentication module
2//!
3//! Provides OAuth 2.0 authentication support.
4
5use crate::error::{Error, Result};
6use url::Url;
7
8/// OAuth configuration
9#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
10pub struct OAuthConfig {
11    /// Whether OAuth is enabled
12    pub enabled: bool,
13    /// Client ID
14    pub client_id: Option<String>,
15    /// Client secret
16    pub client_secret: Option<String>,
17    /// Redirect URI
18    pub redirect_uri: Option<String>,
19    /// Authorization endpoint
20    pub authorization_endpoint: Option<String>,
21    /// Token endpoint
22    pub token_endpoint: Option<String>,
23    /// Scopes
24    pub scopes: Vec<String>,
25    /// Authentication provider type
26    pub provider: OAuthProvider,
27}
28
29/// OAuth provider type
30#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
31pub enum OAuthProvider {
32    /// Custom OAuth provider
33    Custom,
34    /// GitHub OAuth
35    GitHub,
36    /// Google OAuth
37    Google,
38    /// Keycloak
39    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    /// Create GitHub OAuth configuration
63    #[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    /// Create Google OAuth configuration
78    #[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    /// Create Keycloak OAuth configuration
99    #[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    /// Validate configuration
129    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"));
136        }
137
138        if self.client_secret.is_none() {
139            return Err(Error::config("client_secret", "is required"));
140        }
141
142        if self.redirect_uri.is_none() {
143            return Err(Error::config("redirect_uri", "is required"));
144        }
145
146        if self.authorization_endpoint.is_none() {
147            return Err(Error::config("authorization_endpoint", "is required"));
148        }
149
150        if self.token_endpoint.is_none() {
151            return Err(Error::config("token_endpoint", "is required"));
152        }
153
154        // Validate URLs
155        if let Some(uri) = &self.redirect_uri {
156            Url::parse(uri)
157                .map_err(|e| Error::config("redirect_uri", format!("Invalid URL: {e}")))?;
158        }
159
160        if let Some(endpoint) = &self.authorization_endpoint {
161            Url::parse(endpoint).map_err(|e| {
162                Error::config("authorization_endpoint", format!("Invalid URL: {e}"))
163            })?;
164        }
165
166        if let Some(endpoint) = &self.token_endpoint {
167            Url::parse(endpoint)
168                .map_err(|e| Error::config("token_endpoint", format!("Invalid URL: {e}")))?;
169        }
170
171        Ok(())
172    }
173
174    /// Convert to rust-mcp-sdk `OAuthConfig`
175    #[cfg(feature = "auth")]
176    pub fn to_mcp_config(&self) -> Result<()> {
177        if !self.enabled {
178            return Err(Error::config("oauth", "is not enabled"));
179        }
180
181        // Temporarily return empty result, to be implemented when OAuth feature is complete
182        Ok(())
183    }
184
185    /// Convert to rust-mcp-sdk `OAuthConfig`
186    #[cfg(not(feature = "auth"))]
187    pub fn to_mcp_config(&self) -> Result<()> {
188        Err(Error::config("oauth", "feature is not enabled"))
189    }
190}
191
192/// Authentication manager
193#[derive(Default)]
194pub struct AuthManager {
195    config: OAuthConfig,
196}
197
198impl AuthManager {
199    /// Create a new authentication manager
200    pub fn new(config: OAuthConfig) -> Result<Self> {
201        config.validate()?;
202        Ok(Self { config })
203    }
204
205    /// Check if authentication is enabled
206    #[must_use]
207    pub fn is_enabled(&self) -> bool {
208        self.config.enabled
209    }
210
211    /// Get configuration
212    #[must_use]
213    pub fn config(&self) -> &OAuthConfig {
214        &self.config
215    }
216}
217
218/// Simple in-memory token store (production should use Redis or database)
219#[derive(Default)]
220pub struct TokenStore {
221    tokens: std::sync::RwLock<std::collections::HashMap<String, TokenInfo>>,
222}
223
224/// OAuth token information
225#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
226pub struct TokenInfo {
227    /// Access token
228    pub access_token: String,
229    /// Refresh token (optional)
230    pub refresh_token: Option<String>,
231    /// Token expiration time
232    pub expires_at: chrono::DateTime<chrono::Utc>,
233    /// Authorization scopes
234    pub scopes: Vec<String>,
235    /// User ID (optional)
236    pub user_id: Option<String>,
237    /// User email (optional)
238    pub user_email: Option<String>,
239}
240
241impl TokenStore {
242    /// Create a new token store
243    #[must_use]
244    pub fn new() -> Self {
245        Self::default()
246    }
247
248    /// Store token
249    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    /// Get token
255    pub fn get_token(&self, key: &str) -> Option<TokenInfo> {
256        let tokens = self.tokens.read().unwrap();
257        tokens.get(key).cloned()
258    }
259
260    /// Remove token
261    pub fn remove_token(&self, key: &str) {
262        let mut tokens = self.tokens.write().unwrap();
263        tokens.remove(key);
264    }
265
266    /// Cleanup expired tokens
267    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}