Skip to main content

ri/auth/
oauth.rs

1//! Copyright © 2025 Wenze Wei. All Rights Reserved.
2//! 
3//! This file is part of Ri.
4//! The Ri 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 Ri.
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 Ri 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::RiMemoryCache::new());
35//! let oauth_manager = RiOAuthManager::new(cache);
36//! 
37//! // Register a Google OAuth provider
38//! let google_provider = RiOAuthProvider {
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 as FxHashMap;
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 RiOAuthProvider {
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    /// Whitelist of allowed redirect URI patterns (for security validation)
110    pub allowed_redirect_uris: Vec<String>,
111}
112
113#[cfg(feature = "pyo3")]
114#[pyo3::prelude::pymethods]
115impl RiOAuthProvider {
116    #[new]
117    fn py_new(
118        id: String,
119        name: String,
120        client_id: String,
121        client_secret: String,
122        auth_url: String,
123        token_url: String,
124        user_info_url: String,
125        scopes: Vec<String>,
126        enabled: bool,
127        redirect_uri: Option<String>,
128        allowed_redirect_uris: Option<Vec<String>>,
129    ) -> Self {
130        Self {
131            id,
132            name,
133            client_id,
134            client_secret,
135            auth_url,
136            token_url,
137            user_info_url,
138            scopes,
139            enabled,
140            redirect_uri,
141            allowed_redirect_uris: allowed_redirect_uris.unwrap_or_default(),
142        }
143    }
144}
145
146/// OAuth token response.
147///
148/// This struct represents the token response from an OAuth provider,
149/// including access token, refresh token, and expiration information.
150#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RiOAuthToken {
153    /// Access token for making authenticated API requests
154    pub access_token: String,
155    /// Refresh token for obtaining new access tokens when expired
156    pub refresh_token: Option<String>,
157    /// Token expiration time in seconds from issuance
158    pub expires_in: Option<i64>,
159    /// Token type (typically "Bearer")
160    pub token_type: String,
161    /// Granted scopes (may differ from requested scopes)
162    pub scope: Option<String>,
163}
164
165#[cfg(feature = "pyo3")]
166#[pyo3::prelude::pymethods]
167impl RiOAuthToken {
168    #[new]
169    fn py_new(
170        access_token: String,
171        token_type: String,
172        refresh_token: Option<String>,
173        scope: Option<String>,
174        expires_in: Option<i64>,
175    ) -> Self {
176        Self {
177            access_token,
178            token_type,
179            refresh_token,
180            scope,
181            expires_in,
182        }
183    }
184}
185
186/// OAuth user information.
187///
188/// This struct represents the user information retrieved from an OAuth provider,
189/// including user ID, email, name, and profile information.
190#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RiOAuthUserInfo {
193    /// Unique user identifier from the OAuth provider
194    pub id: String,
195    /// User's email address from the provider
196    pub email: String,
197    /// User's full name from the provider
198    pub name: Option<String>,
199    /// URL to user's avatar profile image
200    pub avatar_url: Option<String>,
201    /// Name of the OAuth provider that authenticated the user
202    pub provider: String,
203}
204
205#[cfg(feature = "pyo3")]
206#[pyo3::prelude::pymethods]
207impl RiOAuthUserInfo {
208    #[new]
209    fn py_new(
210        id: String,
211        email: String,
212        name: Option<String>,
213        avatar_url: Option<String>,
214        provider: String,
215    ) -> Self {
216        Self {
217            id,
218            email,
219            name,
220            avatar_url,
221            provider,
222        }
223    }
224}
225
226/// OAuth manager for handling multiple identity providers.
227///
228/// This struct manages OAuth providers, handles token exchange, and retrieves user information.
229/// It supports concurrent access through RwLock and integrates with the Ri cache system
230/// for token storage.
231#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
232pub struct RiOAuthManager {
233    /// Hash map of registered OAuth providers indexed by provider ID
234    providers: RwLock<FxHashMap<String, RiOAuthProvider>>,
235    /// Cache implementation for storing OAuth tokens
236    _token_cache: Arc<dyn crate::cache::RiCache>,
237}
238
239impl RiOAuthManager {
240    /// Creates a new OAuth manager with the specified cache.
241    /// 
242    /// # Parameters
243    /// - `cache`: Cache implementation for storing tokens
244    /// 
245    /// # Returns
246    /// A new instance of `RiOAuthManager`
247    pub fn new(cache: Arc<dyn crate::cache::RiCache>) -> Self {
248        Self {
249            providers: RwLock::new(FxHashMap::default()),
250            _token_cache: cache,
251        }
252    }
253
254    /// Validates a redirect URI against the allowed list.
255    ///
256    /// # Security
257    ///
258    /// This method prevents open redirect attacks by validating that
259    /// the redirect URI matches one of the allowed patterns.
260    ///
261    /// # Parameters
262    /// - `provider`: The OAuth provider configuration
263    /// - `redirect_uri`: The redirect URI to validate
264    ///
265    /// # Returns
266    /// `true` if the redirect URI is allowed, `false` otherwise
267    #[allow(dead_code)]
268    fn is_redirect_uri_allowed(provider: &RiOAuthProvider, redirect_uri: &str) -> bool {
269        // If no whitelist is configured, only allow the default redirect URI
270        if provider.allowed_redirect_uris.is_empty() {
271            let default_uri = provider.redirect_uri.as_deref()
272                .unwrap_or("http://localhost:8080/auth/callback");
273            return redirect_uri == default_uri;
274        }
275
276        // Check against whitelist
277        for allowed in &provider.allowed_redirect_uris {
278            // Exact match
279            if redirect_uri == allowed {
280                return true;
281            }
282            
283            // Prefix match for paths (e.g., "https://example.com/auth/*")
284            if allowed.ends_with('*') {
285                let prefix = &allowed[..allowed.len()-1];
286                if redirect_uri.starts_with(prefix) {
287                    return true;
288                }
289            }
290        }
291        
292        false
293    }
294
295    /// Registers a new OAuth provider.
296    /// 
297    /// # Parameters
298    /// - `provider`: OAuth provider configuration
299    /// 
300    /// # Returns
301    /// `Ok(())` if the provider was successfully registered
302    pub async fn register_provider(&self, provider: RiOAuthProvider) -> crate::core::RiResult<()> {
303        let mut providers = self.providers.write().await;
304        providers.insert(provider.id.clone(), provider);
305        Ok(())
306    }
307
308    /// Gets an OAuth provider by ID.
309    /// 
310    /// # Parameters
311    /// - `provider_id`: Unique identifier of the provider
312    /// 
313    /// # Returns
314    /// `Some(RiOAuthProvider)` if the provider exists, otherwise `None`
315    pub async fn get_provider(&self, provider_id: &str) -> crate::core::RiResult<Option<RiOAuthProvider>> {
316        let providers = self.providers.read().await;
317        Ok(providers.get(provider_id).cloned())
318    }
319
320    /// Gets the authentication URL for a provider.
321    /// 
322    /// # Parameters
323    /// - `provider_id`: Unique identifier of the provider
324    /// - `state`: State parameter for CSRF protection
325    /// 
326    /// # Returns
327    /// `Some(String)` containing the authentication URL if the provider is enabled, otherwise `None`
328    #[cfg(feature = "auth")]
329    pub async fn get_auth_url(&self, provider_id: &str, state: &str) -> crate::core::RiResult<Option<String>> {
330        // Security: Validate state parameter to prevent open redirect attacks
331        // State must be alphanumeric with optional dashes and underscores, max 128 chars
332        if state.is_empty() || state.len() > 128 {
333            return Err(crate::core::RiError::Other("Invalid state parameter: must be 1-128 characters".to_string()));
334        }
335        
336        for c in state.chars() {
337            if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
338                return Err(crate::core::RiError::Other("Invalid state parameter: only alphanumeric, dash, and underscore allowed".to_string()));
339            }
340        }
341        
342        let providers = self.providers.read().await;
343        
344        if let Some(provider) = providers.get(provider_id) {
345            if !provider.enabled {
346                return Ok(None);
347            }
348
349            let scope = provider.scopes.join(" ");
350            let encoded_scope = urlencoding::encode(&scope);
351            let redirect_uri = provider.redirect_uri.as_deref()
352                .unwrap_or("http://localhost:8080/auth/callback");
353            let auth_url = format!(
354                "{}?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}",
355                provider.auth_url,
356                urlencoding::encode(&provider.client_id),
357                urlencoding::encode(redirect_uri),
358                encoded_scope,
359                urlencoding::encode(state)
360            );
361            
362            Ok(Some(auth_url))
363        } else {
364            Ok(None)
365        }
366    }
367
368    #[cfg(not(feature = "auth"))]
369    /// Gets the authentication URL for a provider.
370    ///
371    /// This method requires the `auth` feature to be enabled.
372    /// Without this feature, calling this method returns an error.
373    pub async fn get_auth_url(&self, _provider_id: &str, _state: &str) -> crate::core::RiResult<Option<String>> {
374        Err(crate::core::RiError::Other("Auth feature is not enabled. Enable the 'auth' feature to use OAuth functionality.".to_string()))
375    }
376
377    /// Exchanges an authorization code for an access token.
378    /// 
379    /// # Parameters
380    /// - `provider_id`: Unique identifier of the provider
381    /// - `code`: Authorization code from the provider
382    /// - `redirect_uri`: Redirect URI used in the authentication request
383    /// 
384    /// # Returns
385    /// `Some(RiOAuthToken)` if the code exchange was successful, otherwise `None`
386    #[cfg(feature = "http_client")]
387    pub async fn exchange_code_for_token(
388        &self,
389        provider_id: &str,
390        code: &str,
391        redirect_uri: &str,
392    ) -> crate::core::RiResult<Option<RiOAuthToken>> {
393        let providers = self.providers.read().await;
394        
395        if let Some(provider) = providers.get(provider_id) {
396            if !provider.enabled {
397                return Ok(None);
398            }
399
400            // Security: Validate redirect URI to prevent open redirect attacks
401            if !Self::is_redirect_uri_allowed(provider, redirect_uri) {
402                log::warn!(
403                    "[Ri.OAuth] Redirect URI not allowed: {} for provider {}",
404                    redirect_uri, provider_id
405                );
406                return Err(crate::core::RiError::Other(
407                    "Invalid redirect URI: not in allowed list".to_string()
408                ));
409            }
410
411            // Security: Validate authorization code format
412            if code.is_empty() || code.len() > 1024 {
413                return Err(crate::core::RiError::Other(
414                    "Invalid authorization code: must be 1-1024 characters".to_string()
415                ));
416            }
417
418            let client = reqwest::Client::new();
419            let params = [
420                ("grant_type", "authorization_code"),
421                ("code", code),
422                ("redirect_uri", redirect_uri),
423                ("client_id", &provider.client_id),
424                ("client_secret", &provider.client_secret),
425            ];
426
427            let response = client
428                .post(&provider.token_url)
429                .form(&params)
430                .send()
431                .await
432                .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
433
434            if response.status().is_success() {
435                let token_data: serde_json::Value = response.json().await
436                    .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
437
438                let token = RiOAuthToken {
439                    access_token: token_data["access_token"]
440                        .as_str()
441                        .ok_or_else(|| crate::core::RiError::ExternalError("Missing access_token".to_string()))?
442                        .to_string(),
443                    refresh_token: token_data["refresh_token"].as_str().map(String::from),
444                    expires_in: token_data["expires_in"].as_i64(),
445                    token_type: token_data["token_type"]
446                        .as_str()
447                        .unwrap_or("Bearer")
448                        .to_string(),
449                    scope: token_data["scope"].as_str().map(String::from),
450                };
451
452                Ok(Some(token))
453            } else {
454                Ok(None)
455            }
456        } else {
457            Ok(None)
458        }
459    }
460    
461    #[cfg(not(feature = "http_client"))]
462    /// Exchanges an authorization code for an access token.
463    ///
464    /// This method requires the `http_client` feature to be enabled.
465    /// Without this feature, calling this method returns an error.
466    ///
467    /// # Parameters
468    ///
469    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
470    /// - `_code`: Authorization code from the provider (not used when feature is disabled)
471    /// - `_redirect_uri`: Redirect URI used in the authentication request (not used when feature is disabled)
472    ///
473    /// # Returns
474    ///
475    /// A Result containing an error indicating the http_client feature is not enabled
476    pub async fn exchange_code_for_token(
477        &self,
478        _provider_id: &str,
479        _code: &str,
480        _redirect_uri: &str,
481    ) -> crate::core::RiResult<Option<RiOAuthToken>> {
482        Err(crate::core::RiError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
483    }
484
485    /// Retrieves user information from an OAuth provider.
486    /// 
487    /// # Parameters
488    /// - `provider_id`: Unique identifier of the provider
489    /// - `access_token`: Access token for authentication
490    /// 
491    /// # Returns
492    /// `Some(RiOAuthUserInfo)` if the user information was successfully retrieved, otherwise `None`
493    #[cfg(feature = "http_client")]
494    pub async fn get_user_info(
495        &self,
496        provider_id: &str,
497        access_token: &str,
498    ) -> crate::core::RiResult<Option<RiOAuthUserInfo>> {
499        let providers = self.providers.read().await;
500        
501        if let Some(provider) = providers.get(provider_id) {
502            if !provider.enabled {
503                return Ok(None);
504            }
505
506            let client = reqwest::Client::new();
507            let response = client
508                .get(&provider.user_info_url)
509                .bearer_auth(access_token)
510                .send()
511                .await
512                .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
513
514            if response.status().is_success() {
515                let user_data: serde_json::Value = response.json().await
516                    .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
517
518                let user_info = RiOAuthUserInfo {
519                    id: user_data["id"]
520                        .as_str()
521                        .ok_or_else(|| crate::core::RiError::ExternalError("Missing user id".to_string()))?
522                        .to_string(),
523                    email: user_data["email"]
524                        .as_str()
525                        .ok_or_else(|| crate::core::RiError::ExternalError("Missing email".to_string()))?
526                        .to_string(),
527                    name: user_data["name"].as_str().map(String::from),
528                    avatar_url: user_data["avatar_url"].as_str().map(String::from),
529                    provider: provider_id.to_string(),
530                };
531
532                Ok(Some(user_info))
533            } else {
534                Ok(None)
535            }
536        } else {
537            Ok(None)
538        }
539    }
540    
541    #[cfg(not(feature = "http_client"))]
542    /// Retrieves user information from an OAuth provider.
543    ///
544    /// This method requires the `http_client` feature to be enabled.
545    /// Without this feature, calling this method returns an error.
546    ///
547    /// # Parameters
548    ///
549    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
550    /// - `_access_token`: Access token for authentication (not used when feature is disabled)
551    ///
552    /// # Returns
553    ///
554    /// A Result containing an error indicating the http_client feature is not enabled
555    pub async fn get_user_info(
556        &self,
557        _provider_id: &str,
558        _access_token: &str,
559    ) -> crate::core::RiResult<Option<RiOAuthUserInfo>> {
560        Err(crate::core::RiError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
561    }
562
563    /// Refreshes an access token using a refresh token.
564    /// 
565    /// # Parameters
566    /// - `provider_id`: Unique identifier of the provider
567    /// - `refresh_token`: Refresh token for obtaining a new access token
568    /// 
569    /// # Returns
570    /// `Some(RiOAuthToken)` if the token refresh was successful, otherwise `None`
571    #[cfg(feature = "http_client")]
572    pub async fn refresh_token(
573        &self,
574        provider_id: &str,
575        refresh_token: &str,
576    ) -> crate::core::RiResult<Option<RiOAuthToken>> {
577        let providers = self.providers.read().await;
578        
579        if let Some(provider) = providers.get(provider_id) {
580            if !provider.enabled {
581                return Ok(None);
582            }
583
584            let client = reqwest::Client::new();
585            let params = [
586                ("grant_type", "refresh_token"),
587                ("refresh_token", refresh_token),
588                ("client_id", &provider.client_id),
589                ("client_secret", &provider.client_secret),
590            ];
591
592            let response = client
593                .post(&provider.token_url)
594                .form(&params)
595                .send()
596                .await
597                .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
598
599            if response.status().is_success() {
600                let token_data: serde_json::Value = response.json().await
601                    .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
602
603                let token = RiOAuthToken {
604                    access_token: token_data["access_token"]
605                        .as_str()
606                        .ok_or_else(|| crate::core::RiError::ExternalError("Missing access_token".to_string()))?
607                        .to_string(),
608                    refresh_token: token_data["refresh_token"].as_str().map(String::from),
609                    expires_in: token_data["expires_in"].as_i64(),
610                    token_type: token_data["token_type"]
611                        .as_str()
612                        .unwrap_or("Bearer")
613                        .to_string(),
614                    scope: token_data["scope"].as_str().map(String::from),
615                };
616
617                Ok(Some(token))
618            } else {
619                Ok(None)
620            }
621        } else {
622            Ok(None)
623        }
624    }
625    
626    #[cfg(not(feature = "http_client"))]
627    /// Refreshes an access token using a refresh token.
628    ///
629    /// This method requires the `http_client` feature to be enabled.
630    /// Without this feature, calling this method returns an error.
631    ///
632    /// # Parameters
633    ///
634    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
635    /// - `_refresh_token`: Refresh token for obtaining a new access token (not used when feature is disabled)
636    ///
637    /// # Returns
638    ///
639    /// A Result containing an error indicating the http_client feature is not enabled
640    pub async fn refresh_token(
641        &self,
642        _provider_id: &str,
643        _refresh_token: &str,
644    ) -> crate::core::RiResult<Option<RiOAuthToken>> {
645        Err(crate::core::RiError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
646    }
647
648    /// Revokes an access token.
649    /// 
650    /// # Parameters
651    /// - `provider_id`: Unique identifier of the provider
652    /// - `access_token`: Access token to revoke
653    /// 
654    /// # Returns
655    /// `true` if the token was successfully revoked, otherwise `false`
656    #[cfg(feature = "http_client")]
657    pub async fn revoke_token(
658        &self,
659        provider_id: &str,
660        access_token: &str,
661    ) -> crate::core::RiResult<bool> {
662        let providers = self.providers.read().await;
663        
664        if let Some(provider) = providers.get(provider_id) {
665            if !provider.enabled {
666                return Ok(false);
667            }
668
669            let client = reqwest::Client::new();
670            let params = [
671                ("token", access_token),
672                ("client_id", &provider.client_id),
673                ("client_secret", &provider.client_secret),
674            ];
675
676            let response = client
677                .post(format!("{}/revoke", provider.token_url))
678                .form(&params)
679                .send()
680                .await
681                .map_err(|e| crate::core::RiError::ExternalError(e.to_string()))?;
682
683            Ok(response.status().is_success())
684        } else {
685            Ok(false)
686        }
687    }
688    
689    #[cfg(not(feature = "http_client"))]
690    /// Revokes an access token.
691    ///
692    /// This method requires the `http_client` feature to be enabled.
693    /// Without this feature, calling this method returns an error.
694    ///
695    /// # Parameters
696    ///
697    /// - `_provider_id`: Unique identifier of the provider (not used when feature is disabled)
698    /// - `_access_token`: Access token to revoke (not used when feature is disabled)
699    ///
700    /// # Returns
701    ///
702    /// A Result containing an error indicating the http_client feature is not enabled
703    pub async fn revoke_token(
704        &self,
705        _provider_id: &str,
706        _access_token: &str,
707    ) -> crate::core::RiResult<bool> {
708        Err(crate::core::RiError::Other("HTTP client is not enabled. Enable the 'http_client' feature to use OAuth functionality.".to_string()))
709    }
710
711    /// Lists all registered OAuth providers.
712    /// 
713    /// # Returns
714    /// A vector of all registered OAuth providers
715    pub async fn list_providers(&self) -> crate::core::RiResult<Vec<RiOAuthProvider>> {
716        let providers = self.providers.read().await;
717        Ok(providers.values().cloned().collect())
718    }
719
720    /// Disables an OAuth provider.
721    /// 
722    /// # Parameters
723    /// - `provider_id`: Unique identifier of the provider
724    /// 
725    /// # Returns
726    /// `true` if the provider was successfully disabled, otherwise `false`
727    pub async fn disable_provider(&self, provider_id: &str) -> crate::core::RiResult<bool> {
728        let mut providers = self.providers.write().await;
729        
730        if let Some(provider) = providers.get_mut(provider_id) {
731            provider.enabled = false;
732            Ok(true)
733        } else {
734            Ok(false)
735        }
736    }
737
738    /// Enables an OAuth provider.
739    /// 
740    /// # Parameters
741    /// - `provider_id`: Unique identifier of the provider
742    /// 
743    /// # Returns
744    /// `true` if the provider was successfully enabled, otherwise `false`
745    pub async fn enable_provider(&self, provider_id: &str) -> crate::core::RiResult<bool> {
746        let mut providers = self.providers.write().await;
747        
748        if let Some(provider) = providers.get_mut(provider_id) {
749            provider.enabled = true;
750            Ok(true)
751        } else {
752            Ok(false)
753        }
754    }
755}
756
757#[cfg(feature = "pyo3")]
758/// Python bindings for the OAuth Manager.
759///
760/// This module provides Python interface to Ri OAuth functionality,
761/// enabling Python applications to integrate with OAuth identity providers.
762///
763/// ## Supported Operations
764///
765/// - Provider registration and management
766/// - Authentication URL generation for OAuth flows
767/// - Token exchange with authorization codes
768/// - User information retrieval from OAuth providers
769/// - Token refresh and revocation
770///
771/// ## Python Usage Example
772///
773/// ```python
774/// from ri import RiOAuthProvider, RiOAuthManager
775///
776/// # Create OAuth manager
777/// oauth_manager = RiOAuthManager()
778///
779/// # Register a provider
780/// provider = RiOAuthProvider(
781///     id="google",
782///     name="Google",
783///     client_id="your_client_id",
784///     client_secret="your_client_secret",
785///     auth_url="https://accounts.google.com/o/oauth2/auth",
786///     token_url="https://oauth2.googleapis.com/token",
787///     user_info_url="https://www.googleapis.com/oauth2/v3/userinfo",
788///     scopes=["openid", "email", "profile"],
789///     enabled=True,
790/// )
791/// # Note: Async operations require Python 3.7+ with asyncio
792/// ```
793#[pyo3::prelude::pymethods]
794impl RiOAuthManager {
795    #[new]
796    fn py_new() -> PyResult<Self> {
797        let cache = Arc::new(crate::cache::RiMemoryCache::new());
798        Ok(Self::new(cache))
799    }
800    
801    #[pyo3(name = "register_provider")]
802    fn register_provider_impl(&self, provider: RiOAuthProvider) -> PyResult<bool> {
803        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
804        rt.block_on(async {
805            self.register_provider(provider).await?;
806            Ok(true)
807        })
808    }
809    
810    #[pyo3(name = "get_provider")]
811    fn get_provider_impl(&self, provider_id: String) -> PyResult<Option<RiOAuthProvider>> {
812        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
813        rt.block_on(async {
814            self.get_provider(&provider_id).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
815        })
816    }
817    
818    #[pyo3(name = "get_auth_url")]
819    fn get_auth_url_impl(&self, provider_id: String, state: String) -> PyResult<Option<String>> {
820        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
821        rt.block_on(async {
822            self.get_auth_url(&provider_id, &state).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
823        })
824    }
825    
826    #[pyo3(name = "exchange_code_for_token")]
827    fn exchange_code_for_token_impl(&self, provider_id: String, code: String, redirect_uri: String) -> PyResult<Option<RiOAuthToken>> {
828        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
829        rt.block_on(async {
830            self.exchange_code_for_token(&provider_id, &code, &redirect_uri).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
831        })
832    }
833    
834    #[pyo3(name = "get_user_info")]
835    fn get_user_info_impl(&self, provider_id: String, access_token: String) -> PyResult<Option<RiOAuthUserInfo>> {
836        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
837        rt.block_on(async {
838            self.get_user_info(&provider_id, &access_token).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
839        })
840    }
841    
842    #[pyo3(name = "refresh_token")]
843    fn refresh_token_impl(&self, provider_id: String, refresh_token: String) -> PyResult<Option<RiOAuthToken>> {
844        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
845        rt.block_on(async {
846            self.refresh_token(&provider_id, &refresh_token).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
847        })
848    }
849    
850    #[pyo3(name = "revoke_token")]
851    fn revoke_token_impl(&self, provider_id: String, access_token: String) -> PyResult<bool> {
852        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
853        rt.block_on(async {
854            self.revoke_token(&provider_id, &access_token).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
855        })
856    }
857    
858    #[pyo3(name = "list_providers")]
859    fn list_providers_impl(&self) -> PyResult<Vec<RiOAuthProvider>> {
860        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
861        rt.block_on(async {
862            self.list_providers().await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
863        })
864    }
865    
866    #[pyo3(name = "disable_provider")]
867    fn disable_provider_impl(&self, provider_id: String) -> PyResult<bool> {
868        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
869        rt.block_on(async {
870            self.disable_provider(&provider_id).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
871        })
872    }
873    
874    #[pyo3(name = "enable_provider")]
875    fn enable_provider_impl(&self, provider_id: String) -> PyResult<bool> {
876        let rt = Runtime::new().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
877        rt.block_on(async {
878            self.enable_provider(&provider_id).await.map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
879        })
880    }
881}