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(¶ms)
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(¶ms)
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(¶ms)
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}