dmsc/auth/
oauth.rs

1//! Copyright © 2025 Wenze Wei. All Rights Reserved.
2//! 
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//! 
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//! 
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//! 
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! OAuth 2.0 authentication implementation for DMSC.
19//! 
20//! This module provides OAuth 2.0 authentication functionality, including support for
21//! multiple identity providers, token management, and user information retrieval.
22//! It implements the OAuth 2.0 authorization code flow and supports token refresh and revocation.
23//! 
24//! # Design Principles
25//! - **Multi-Provider Support**: Allows registration of multiple OAuth providers
26//! - **Thread Safety**: Uses RwLock for concurrent access to provider configuration
27//! - **Caching**: Integrates with DMSC cache for token storage
28//! - **Async Operations**: All network operations are asynchronous
29//! - **Extensibility**: Designed to support additional OAuth flows and providers
30//! 
31//! # Usage Examples
32//! ```rust
33//! // Create an OAuth manager with a cache
34//! let cache = Arc::new(crate::cache::backends::memory_backend::DMSCMemoryCache::new());
35//! let oauth_manager = DMSCOAuthManager::new(cache);
36//! 
37//! // Register a Google OAuth provider
38//! let google_provider = DMSCOAuthProvider {
39//!     id: "google".to_string(),
40//!     name: "Google".to_string(),
41//!     client_id: "client_id".to_string(),
42//!     client_secret: "client_secret".to_string(),
43//!     auth_url: "https://accounts.google.com/o/oauth2/auth".to_string(),
44//!     token_url: "https://oauth2.googleapis.com/token".to_string(),
45//!     user_info_url: "https://www.googleapis.com/oauth2/v3/userinfo".to_string(),
46//!     scopes: vec!["openid", "email", "profile"].iter().map(|s| s.to_string()).collect(),
47//!     enabled: true,
48//! };
49//! oauth_manager.register_provider(google_provider).await?;
50//! 
51//! // Get authentication URL for a provider
52//! let auth_url = oauth_manager.get_auth_url("google", "state123").await?;
53//! 
54//! // Exchange authorization code for token
55//! let token = oauth_manager.exchange_code_for_token(
56//!     "google",
57//!     "auth_code",
58//!     "http://localhost:8080/auth/callback"
59//! ).await?;
60//! 
61//! // Get user information
62//! if let Some(token) = token {
63//!     let user_info = oauth_manager.get_user_info("google", &token.access_token).await?;
64//! }
65//! ```
66
67#![allow(non_snake_case)]
68
69use serde::{Deserialize, Serialize};
70use std::collections::HashMap;
71use std::sync::Arc;
72use tokio::sync::RwLock;
73#[cfg(feature = "pyo3")]
74use tokio::runtime::Runtime;
75
76#[cfg(feature = "pyo3")]
77use pyo3::PyResult;
78
79#[cfg(feature = "auth")]
80extern crate urlencoding;
81
82/// OAuth provider configuration.
83///
84/// This struct defines the configuration for an OAuth identity provider,
85/// including client credentials, endpoints, scopes, and redirect URI.
86#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct DMSCOAuthProvider {
89    /// Unique identifier for the OAuth provider
90    pub id: String,
91    /// Human-readable name of the provider (e.g., "Google", "GitHub")
92    pub name: String,
93    /// OAuth client ID issued by the provider
94    pub client_id: String,
95    /// OAuth client secret issued by the provider
96    pub client_secret: String,
97    /// Authorization endpoint URL for initiating OAuth flow
98    pub auth_url: String,
99    /// Token endpoint URL for exchanging authorization codes
100    pub token_url: String,
101    /// User information endpoint URL for retrieving user details
102    pub user_info_url: String,
103    /// Requested OAuth scopes (e.g., "openid", "email", "profile")
104    pub scopes: Vec<String>,
105    /// Whether the provider is enabled for authentication
106    pub enabled: bool,
107    /// Redirect URI for OAuth callback (defaults to "http://localhost:8080/auth/callback" if not set)
108    pub redirect_uri: Option<String>,
109}
110
111#[cfg(feature = "pyo3")]
112#[pyo3::prelude::pymethods]
113impl DMSCOAuthProvider {
114    #[new]
115    fn py_new(
116        id: String,
117        name: String,
118        client_id: String,
119        client_secret: String,
120        auth_url: String,
121        token_url: String,
122        user_info_url: String,
123        scopes: Vec<String>,
124        enabled: bool,
125        redirect_uri: Option<String>,
126    ) -> Self {
127        Self {
128            id,
129            name,
130            client_id,
131            client_secret,
132            auth_url,
133            token_url,
134            user_info_url,
135            scopes,
136            enabled,
137            redirect_uri,
138        }
139    }
140}
141
142/// OAuth token response.
143///
144/// This struct represents the token response from an OAuth provider,
145/// including access token, refresh token, and expiration information.
146#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct DMSCOAuthToken {
149    /// Access token for making authenticated API requests
150    pub access_token: String,
151    /// Refresh token for obtaining new access tokens when expired
152    pub refresh_token: Option<String>,
153    /// Token expiration time in seconds from issuance
154    pub expires_in: Option<i64>,
155    /// Token type (typically "Bearer")
156    pub token_type: String,
157    /// Granted scopes (may differ from requested scopes)
158    pub scope: Option<String>,
159}
160
161#[cfg(feature = "pyo3")]
162#[pyo3::prelude::pymethods]
163impl DMSCOAuthToken {
164    #[new]
165    fn py_new(
166        access_token: String,
167        token_type: String,
168        refresh_token: Option<String>,
169        scope: Option<String>,
170        expires_in: Option<i64>,
171    ) -> Self {
172        Self {
173            access_token,
174            token_type,
175            refresh_token,
176            scope,
177            expires_in,
178        }
179    }
180}
181
182/// OAuth user information.
183///
184/// This struct represents the user information retrieved from an OAuth provider,
185/// including user ID, email, name, and profile information.
186#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct DMSCOAuthUserInfo {
189    /// Unique user identifier from the OAuth provider
190    pub id: String,
191    /// User's email address from the provider
192    pub email: String,
193    /// User's full name from the provider
194    pub name: Option<String>,
195    /// URL to user's avatar profile image
196    pub avatar_url: Option<String>,
197    /// Name of the OAuth provider that authenticated the user
198    pub provider: String,
199}
200
201#[cfg(feature = "pyo3")]
202#[pyo3::prelude::pymethods]
203impl DMSCOAuthUserInfo {
204    #[new]
205    fn py_new(
206        id: String,
207        email: String,
208        name: Option<String>,
209        avatar_url: Option<String>,
210        provider: String,
211    ) -> Self {
212        Self {
213            id,
214            email,
215            name,
216            avatar_url,
217            provider,
218        }
219    }
220}
221
222/// OAuth manager for handling multiple identity providers.
223///
224/// This struct manages OAuth providers, handles token exchange, and retrieves user information.
225/// It supports concurrent access through RwLock and integrates with the DMSC cache system
226/// for token storage.
227#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
228pub struct DMSCOAuthManager {
229    /// Hash map of registered OAuth providers indexed by provider ID
230    providers: RwLock<HashMap<String, DMSCOAuthProvider>>,
231    /// Cache implementation for storing OAuth tokens
232    _token_cache: Arc<dyn crate::cache::DMSCCache>,
233}
234
235impl DMSCOAuthManager {
236    /// Creates a new OAuth manager with the specified cache.
237    /// 
238    /// # Parameters
239    /// - `cache`: Cache implementation for storing tokens
240    /// 
241    /// # Returns
242    /// A new instance of `DMSCOAuthManager`
243    pub fn new(cache: Arc<dyn crate::cache::DMSCCache>) -> Self {
244        Self {
245            providers: RwLock::new(HashMap::new()),
246            _token_cache: cache,
247        }
248    }
249
250    /// Registers a new OAuth provider.
251    /// 
252    /// # Parameters
253    /// - `provider`: OAuth provider configuration
254    /// 
255    /// # Returns
256    /// `Ok(())` if the provider was successfully registered
257    pub async fn register_provider(&self, provider: DMSCOAuthProvider) -> crate::core::DMSCResult<()> {
258        let mut providers = self.providers.write().await;
259        providers.insert(provider.id.clone(), provider);
260        Ok(())
261    }
262
263    /// Gets an OAuth provider by ID.
264    /// 
265    /// # Parameters
266    /// - `provider_id`: Unique identifier of the provider
267    /// 
268    /// # Returns
269    /// `Some(DMSCOAuthProvider)` if the provider exists, otherwise `None`
270    pub async fn get_provider(&self, provider_id: &str) -> crate::core::DMSCResult<Option<DMSCOAuthProvider>> {
271        let providers = self.providers.read().await;
272        Ok(providers.get(provider_id).cloned())
273    }
274
275    /// Gets the authentication URL for a provider.
276    /// 
277    /// # Parameters
278    /// - `provider_id`: Unique identifier of the provider
279    /// - `state`: State parameter for CSRF protection
280    /// 
281    /// # Returns
282    /// `Some(String)` containing the authentication URL if the provider is enabled, otherwise `None`
283    #[cfg(feature = "auth")]
284    pub async fn get_auth_url(&self, provider_id: &str, state: &str) -> crate::core::DMSCResult<Option<String>> {
285        let providers = self.providers.read().await;
286        
287        if let Some(provider) = providers.get(provider_id) {
288            if !provider.enabled {
289                return Ok(None);
290            }
291
292            let scope = provider.scopes.join(" ");
293            let encoded_scope = scope.clone();
294            let redirect_uri = provider.redirect_uri.as_deref()
295                .unwrap_or("http://localhost:8080/auth/callback");
296            let auth_url = format!(
297                "{}?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}",
298                provider.auth_url,
299                provider.client_id,
300                urlencoding::encode(redirect_uri),
301                encoded_scope,
302                state
303            );
304            
305            Ok(Some(auth_url))
306        } else {
307            Ok(None)
308        }
309    }
310
311    #[cfg(not(feature = "auth"))]
312    /// Gets the authentication URL for a provider.
313    ///
314    /// This method requires the `auth` feature to be enabled.
315    /// Without this feature, calling this method returns an error.
316    pub async fn get_auth_url(&self, _provider_id: &str, _state: &str) -> crate::core::DMSCResult<Option<String>> {
317        Err(crate::core::DMSCError::Other("Auth feature is not enabled. Enable the 'auth' feature to use OAuth functionality.".to_string()))
318    }
319
320    /// Exchanges an authorization code for an access token.
321    /// 
322    /// # Parameters
323    /// - `provider_id`: Unique identifier of the provider
324    /// - `code`: Authorization code from the provider
325    /// - `redirect_uri`: Redirect URI used in the authentication request
326    /// 
327    /// # Returns
328    /// `Some(DMSCOAuthToken)` if the code exchange was successful, otherwise `None`
329    #[cfg(feature = "http_client")]
330    pub async fn exchange_code_for_token(
331        &self,
332        provider_id: &str,
333        code: &str,
334        redirect_uri: &str,
335    ) -> crate::core::DMSCResult<Option<DMSCOAuthToken>> {
336        let providers = self.providers.read().await;
337        
338        if let Some(provider) = providers.get(provider_id) {
339            if !provider.enabled {
340                return Ok(None);
341            }
342
343            let client = reqwest::Client::new();
344            let params = [
345                ("grant_type", "authorization_code"),
346                ("code", code),
347                ("redirect_uri", redirect_uri),
348                ("client_id", &provider.client_id),
349                ("client_secret", &provider.client_secret),
350            ];
351
352            let response = client
353                .post(&provider.token_url)
354                .form(&params)
355                .send()
356                .await
357                .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
358
359            if response.status().is_success() {
360                let token_data: serde_json::Value = response.json().await
361                    .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
362
363                let token = DMSCOAuthToken {
364                    access_token: token_data["access_token"]
365                        .as_str()
366                        .ok_or_else(|| crate::core::DMSCError::ExternalError("Missing access_token".to_string()))?
367                        .to_string(),
368                    refresh_token: token_data["refresh_token"].as_str().map(String::from),
369                    expires_in: token_data["expires_in"].as_i64(),
370                    token_type: token_data["token_type"]
371                        .as_str()
372                        .unwrap_or("Bearer")
373                        .to_string(),
374                    scope: token_data["scope"].as_str().map(String::from),
375                };
376
377                Ok(Some(token))
378            } else {
379                Ok(None)
380            }
381        } else {
382            Ok(None)
383        }
384    }
385    
386    #[cfg(not(feature = "http_client"))]
387    /// Exchanges an authorization code for an access token.
388    ///
389    /// This method requires the `http_client` feature to be enabled.
390    /// Without this feature, calling this method returns an error.
391    ///
392    /// # Parameters
393    ///
394    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
395    /// - `_code`: Authorization code from the provider (not used when feature is disabled)
396    /// - `_redirect_uri`: Redirect URI used in the authentication request (not used when feature is disabled)
397    ///
398    /// # Returns
399    ///
400    /// A Result containing an error indicating the http_client feature is not enabled
401    pub async fn exchange_code_for_token(
402        &self,
403        _provider_id: &str,
404        _code: &str,
405        _redirect_uri: &str,
406    ) -> crate::core::DMSCResult<Option<DMSCOAuthToken>> {
407        Err(crate::core::DMSCError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
408    }
409
410    /// Retrieves user information from an OAuth provider.
411    /// 
412    /// # Parameters
413    /// - `provider_id`: Unique identifier of the provider
414    /// - `access_token`: Access token for authentication
415    /// 
416    /// # Returns
417    /// `Some(DMSCOAuthUserInfo)` if the user information was successfully retrieved, otherwise `None`
418    #[cfg(feature = "http_client")]
419    pub async fn get_user_info(
420        &self,
421        provider_id: &str,
422        access_token: &str,
423    ) -> crate::core::DMSCResult<Option<DMSCOAuthUserInfo>> {
424        let providers = self.providers.read().await;
425        
426        if let Some(provider) = providers.get(provider_id) {
427            if !provider.enabled {
428                return Ok(None);
429            }
430
431            let client = reqwest::Client::new();
432            let response = client
433                .get(&provider.user_info_url)
434                .bearer_auth(access_token)
435                .send()
436                .await
437                .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
438
439            if response.status().is_success() {
440                let user_data: serde_json::Value = response.json().await
441                    .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
442
443                let user_info = DMSCOAuthUserInfo {
444                    id: user_data["id"]
445                        .as_str()
446                        .ok_or_else(|| crate::core::DMSCError::ExternalError("Missing user id".to_string()))?
447                        .to_string(),
448                    email: user_data["email"]
449                        .as_str()
450                        .ok_or_else(|| crate::core::DMSCError::ExternalError("Missing email".to_string()))?
451                        .to_string(),
452                    name: user_data["name"].as_str().map(String::from),
453                    avatar_url: user_data["avatar_url"].as_str().map(String::from),
454                    provider: provider_id.to_string(),
455                };
456
457                Ok(Some(user_info))
458            } else {
459                Ok(None)
460            }
461        } else {
462            Ok(None)
463        }
464    }
465    
466    #[cfg(not(feature = "http_client"))]
467    /// Retrieves user information from an OAuth provider.
468    ///
469    /// This method requires the `http_client` feature to be enabled.
470    /// Without this feature, calling this method returns an error.
471    ///
472    /// # Parameters
473    ///
474    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
475    /// - `_access_token`: Access token for authentication (not used when feature is disabled)
476    ///
477    /// # Returns
478    ///
479    /// A Result containing an error indicating the http_client feature is not enabled
480    pub async fn get_user_info(
481        &self,
482        _provider_id: &str,
483        _access_token: &str,
484    ) -> crate::core::DMSCResult<Option<DMSCOAuthUserInfo>> {
485        Err(crate::core::DMSCError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
486    }
487
488    /// Refreshes an access token using a refresh token.
489    /// 
490    /// # Parameters
491    /// - `provider_id`: Unique identifier of the provider
492    /// - `refresh_token`: Refresh token for obtaining a new access token
493    /// 
494    /// # Returns
495    /// `Some(DMSCOAuthToken)` if the token refresh was successful, otherwise `None`
496    #[cfg(feature = "http_client")]
497    pub async fn refresh_token(
498        &self,
499        provider_id: &str,
500        refresh_token: &str,
501    ) -> crate::core::DMSCResult<Option<DMSCOAuthToken>> {
502        let providers = self.providers.read().await;
503        
504        if let Some(provider) = providers.get(provider_id) {
505            if !provider.enabled {
506                return Ok(None);
507            }
508
509            let client = reqwest::Client::new();
510            let params = [
511                ("grant_type", "refresh_token"),
512                ("refresh_token", refresh_token),
513                ("client_id", &provider.client_id),
514                ("client_secret", &provider.client_secret),
515            ];
516
517            let response = client
518                .post(&provider.token_url)
519                .form(&params)
520                .send()
521                .await
522                .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
523
524            if response.status().is_success() {
525                let token_data: serde_json::Value = response.json().await
526                    .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
527
528                let token = DMSCOAuthToken {
529                    access_token: token_data["access_token"]
530                        .as_str()
531                        .ok_or_else(|| crate::core::DMSCError::ExternalError("Missing access_token".to_string()))?
532                        .to_string(),
533                    refresh_token: token_data["refresh_token"].as_str().map(String::from),
534                    expires_in: token_data["expires_in"].as_i64(),
535                    token_type: token_data["token_type"]
536                        .as_str()
537                        .unwrap_or("Bearer")
538                        .to_string(),
539                    scope: token_data["scope"].as_str().map(String::from),
540                };
541
542                Ok(Some(token))
543            } else {
544                Ok(None)
545            }
546        } else {
547            Ok(None)
548        }
549    }
550    
551    #[cfg(not(feature = "http_client"))]
552    /// Refreshes an access token using a refresh token.
553    ///
554    /// This method requires the `http_client` feature to be enabled.
555    /// Without this feature, calling this method returns an error.
556    ///
557    /// # Parameters
558    ///
559    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
560    /// - `_refresh_token`: Refresh token for obtaining a new access token (not used when feature is disabled)
561    ///
562    /// # Returns
563    ///
564    /// A Result containing an error indicating the http_client feature is not enabled
565    pub async fn refresh_token(
566        &self,
567        _provider_id: &str,
568        _refresh_token: &str,
569    ) -> crate::core::DMSCResult<Option<DMSCOAuthToken>> {
570        Err(crate::core::DMSCError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
571    }
572
573    /// Revokes an access token.
574    /// 
575    /// # Parameters
576    /// - `provider_id`: Unique identifier of the provider
577    /// - `access_token`: Access token to revoke
578    /// 
579    /// # Returns
580    /// `true` if the token was successfully revoked, otherwise `false`
581    #[cfg(feature = "http_client")]
582    pub async fn revoke_token(
583        &self,
584        provider_id: &str,
585        access_token: &str,
586    ) -> crate::core::DMSCResult<bool> {
587        let providers = self.providers.read().await;
588        
589        if let Some(provider) = providers.get(provider_id) {
590            if !provider.enabled {
591                return Ok(false);
592            }
593
594            let client = reqwest::Client::new();
595            let params = [
596                ("token", access_token),
597                ("client_id", &provider.client_id),
598                ("client_secret", &provider.client_secret),
599            ];
600
601            let response = client
602                .post(format!("{}/revoke", provider.token_url))
603                .form(&params)
604                .send()
605                .await
606                .map_err(|e| crate::core::DMSCError::ExternalError(e.to_string()))?;
607
608            Ok(response.status().is_success())
609        } else {
610            Ok(false)
611        }
612    }
613    
614    #[cfg(not(feature = "http_client"))]
615    /// Revokes an access token.
616    ///
617    /// This method requires the `http_client` feature to be enabled.
618    /// Without this feature, calling this method returns an error.
619    ///
620    /// # Parameters
621    ///
622    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
623    /// - `_access_token`: Access token to revoke (not used when feature is disabled)
624    ///
625    /// # Returns
626    ///
627    /// A Result containing an error indicating the http_client feature is not enabled
628    pub async fn revoke_token(
629        &self,
630        _provider_id: &str,
631        _access_token: &str,
632    ) -> crate::core::DMSCResult<bool> {
633        Err(crate::core::DMSCError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
634    }
635
636    /// Lists all registered OAuth providers.
637    /// 
638    /// # Returns
639    /// A vector of all registered OAuth providers
640    pub async fn list_providers(&self) -> crate::core::DMSCResult<Vec<DMSCOAuthProvider>> {
641        let providers = self.providers.read().await;
642        Ok(providers.values().cloned().collect())
643    }
644
645    /// Disables an OAuth provider.
646    /// 
647    /// # Parameters
648    /// - `provider_id`: Unique identifier of the provider
649    /// 
650    /// # Returns
651    /// `true` if the provider was successfully disabled, otherwise `false`
652    pub async fn disable_provider(&self, provider_id: &str) -> crate::core::DMSCResult<bool> {
653        let mut providers = self.providers.write().await;
654        
655        if let Some(provider) = providers.get_mut(provider_id) {
656            provider.enabled = false;
657            Ok(true)
658        } else {
659            Ok(false)
660        }
661    }
662
663    /// Enables an OAuth provider.
664    /// 
665    /// # Parameters
666    /// - `provider_id`: Unique identifier of the provider
667    /// 
668    /// # Returns
669    /// `true` if the provider was successfully enabled, otherwise `false`
670    pub async fn enable_provider(&self, provider_id: &str) -> crate::core::DMSCResult<bool> {
671        let mut providers = self.providers.write().await;
672        
673        if let Some(provider) = providers.get_mut(provider_id) {
674            provider.enabled = true;
675            Ok(true)
676        } else {
677            Ok(false)
678        }
679    }
680}
681
682#[cfg(feature = "pyo3")]
683/// Python bindings for the OAuth Manager.
684///
685/// This module provides Python interface to DMSC OAuth functionality,
686/// enabling Python applications to integrate with OAuth identity providers.
687///
688/// ## Supported Operations
689///
690/// - Provider registration and management
691/// - Authentication URL generation for OAuth flows
692/// - Token exchange with authorization codes
693/// - User information retrieval from OAuth providers
694/// - Token refresh and revocation
695///
696/// ## Python Usage Example
697///
698/// ```python
699/// from dmsc import DMSCOAuthProvider, DMSCOAuthManager
700///
701/// # Create OAuth manager
702/// oauth_manager = DMSCOAuthManager()
703///
704/// # Register a provider
705/// provider = DMSCOAuthProvider(
706///     id="google",
707///     name="Google",
708///     client_id="your_client_id",
709///     client_secret="your_client_secret",
710///     auth_url="https://accounts.google.com/o/oauth2/auth",
711///     token_url="https://oauth2.googleapis.com/token",
712///     user_info_url="https://www.googleapis.com/oauth2/v3/userinfo",
713///     scopes=["openid", "email", "profile"],
714///     enabled=True,
715/// )
716/// # Note: Async operations require Python 3.7+ with asyncio
717/// ```
718#[pyo3::prelude::pymethods]
719impl DMSCOAuthManager {
720    #[new]
721    fn py_new() -> PyResult<Self> {
722        let cache = Arc::new(crate::cache::DMSCMemoryCache::new());
723        Ok(Self::new(cache))
724    }
725    
726    #[pyo3(name = "register_provider")]
727    fn register_provider_impl(&self, provider: DMSCOAuthProvider) -> PyResult<bool> {
728        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
729        rt.block_on(async {
730            self.register_provider(provider).await?;
731            Ok(true)
732        })
733    }
734    
735    #[pyo3(name = "get_provider")]
736    fn get_provider_impl(&self, provider_id: String) -> PyResult<Option<DMSCOAuthProvider>> {
737        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
738        rt.block_on(async {
739            self.get_provider(&provider_id).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
740        })
741    }
742    
743    #[pyo3(name = "get_auth_url")]
744    fn get_auth_url_impl(&self, provider_id: String, state: String) -> PyResult<Option<String>> {
745        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
746        rt.block_on(async {
747            self.get_auth_url(&provider_id, &state).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
748        })
749    }
750    
751    #[pyo3(name = "exchange_code_for_token")]
752    fn exchange_code_for_token_impl(&self, provider_id: String, code: String, redirect_uri: String) -> PyResult<Option<DMSCOAuthToken>> {
753        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
754        rt.block_on(async {
755            self.exchange_code_for_token(&provider_id, &code, &redirect_uri).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
756        })
757    }
758    
759    #[pyo3(name = "get_user_info")]
760    fn get_user_info_impl(&self, provider_id: String, access_token: String) -> PyResult<Option<DMSCOAuthUserInfo>> {
761        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
762        rt.block_on(async {
763            self.get_user_info(&provider_id, &access_token).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
764        })
765    }
766    
767    #[pyo3(name = "refresh_token")]
768    fn refresh_token_impl(&self, provider_id: String, refresh_token: String) -> PyResult<Option<DMSCOAuthToken>> {
769        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
770        rt.block_on(async {
771            self.refresh_token(&provider_id, &refresh_token).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
772        })
773    }
774    
775    #[pyo3(name = "revoke_token")]
776    fn revoke_token_impl(&self, provider_id: String, access_token: String) -> PyResult<bool> {
777        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
778        rt.block_on(async {
779            self.revoke_token(&provider_id, &access_token).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
780        })
781    }
782    
783    #[pyo3(name = "list_providers")]
784    fn list_providers_impl(&self) -> PyResult<Vec<DMSCOAuthProvider>> {
785        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
786        rt.block_on(async {
787            self.list_providers().await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
788        })
789    }
790    
791    #[pyo3(name = "disable_provider")]
792    fn disable_provider_impl(&self, provider_id: String) -> PyResult<bool> {
793        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
794        rt.block_on(async {
795            self.disable_provider(&provider_id).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
796        })
797    }
798    
799    #[pyo3(name = "enable_provider")]
800    fn enable_provider_impl(&self, provider_id: String) -> PyResult<bool> {
801        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
802        rt.block_on(async {
803            self.enable_provider(&provider_id).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
804        })
805    }
806}