Skip to main content

fraiseql_server/auth/
oauth.rs

1// Phase 12.5 Cycle 1: External Auth Provider Integration - GREEN
2//! OAuth2 and OIDC authentication support with JWT validation,
3//! provider discovery, and automatic user provisioning.
4
5use std::{collections::HashMap, sync::Arc};
6
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9
10/// OAuth2 token response from provider
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct TokenResponse {
13    /// Access token for API calls
14    pub access_token:  String,
15    /// Refresh token for getting new access tokens
16    pub refresh_token: Option<String>,
17    /// Token type (typically "Bearer")
18    pub token_type:    String,
19    /// Seconds until access token expires
20    pub expires_in:    u64,
21    /// ID token (JWT) for OIDC
22    pub id_token:      Option<String>,
23    /// Requested scopes
24    pub scope:         Option<String>,
25}
26
27impl TokenResponse {
28    /// Create new token response
29    pub fn new(access_token: String, token_type: String, expires_in: u64) -> Self {
30        Self {
31            access_token,
32            refresh_token: None,
33            token_type,
34            expires_in,
35            id_token: None,
36            scope: None,
37        }
38    }
39
40    /// Calculate expiry time
41    pub fn expiry_time(&self) -> DateTime<Utc> {
42        Utc::now() + Duration::seconds(self.expires_in as i64)
43    }
44
45    /// Check if token is expired
46    pub fn is_expired(&self) -> bool {
47        self.expiry_time() <= Utc::now()
48    }
49}
50
51/// JWT ID token claims
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct IdTokenClaims {
54    /// Issuer (provider identifier)
55    pub iss:            String,
56    /// Subject (unique user ID)
57    pub sub:            String,
58    /// Audience (should be client_id)
59    pub aud:            String,
60    /// Expiration time (Unix timestamp)
61    pub exp:            i64,
62    /// Issued at time (Unix timestamp)
63    pub iat:            i64,
64    /// Authentication time (Unix timestamp)
65    pub auth_time:      Option<i64>,
66    /// Nonce (for replay protection)
67    pub nonce:          Option<String>,
68    /// Email address
69    pub email:          Option<String>,
70    /// Email verified flag
71    pub email_verified: Option<bool>,
72    /// User name
73    pub name:           Option<String>,
74    /// Profile picture URL
75    pub picture:        Option<String>,
76    /// Locale
77    pub locale:         Option<String>,
78}
79
80impl IdTokenClaims {
81    /// Create new ID token claims
82    pub fn new(iss: String, sub: String, aud: String, exp: i64, iat: i64) -> Self {
83        Self {
84            iss,
85            sub,
86            aud,
87            exp,
88            iat,
89            auth_time: None,
90            nonce: None,
91            email: None,
92            email_verified: None,
93            name: None,
94            picture: None,
95            locale: None,
96        }
97    }
98
99    /// Check if token is expired
100    pub fn is_expired(&self) -> bool {
101        self.exp <= Utc::now().timestamp()
102    }
103
104    /// Check if token will be expired within grace period
105    pub fn is_expiring_soon(&self, grace_seconds: i64) -> bool {
106        self.exp <= (Utc::now().timestamp() + grace_seconds)
107    }
108}
109
110/// Userinfo response from provider
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct UserInfo {
113    /// Subject (unique user ID)
114    pub sub:            String,
115    /// Email address
116    pub email:          Option<String>,
117    /// Email verified flag
118    pub email_verified: Option<bool>,
119    /// User name
120    pub name:           Option<String>,
121    /// Profile picture URL
122    pub picture:        Option<String>,
123    /// Locale
124    pub locale:         Option<String>,
125}
126
127impl UserInfo {
128    /// Create new userinfo
129    pub fn new(sub: String) -> Self {
130        Self {
131            sub,
132            email: None,
133            email_verified: None,
134            name: None,
135            picture: None,
136            locale: None,
137        }
138    }
139}
140
141/// OIDC provider configuration
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
143pub struct OIDCProviderConfig {
144    /// Provider issuer URL
145    pub issuer:                   String,
146    /// Authorization endpoint
147    pub authorization_endpoint:   String,
148    /// Token endpoint
149    pub token_endpoint:           String,
150    /// Userinfo endpoint
151    pub userinfo_endpoint:        Option<String>,
152    /// JWKS URI for public keys
153    pub jwks_uri:                 String,
154    /// Scopes supported by provider
155    pub scopes_supported:         Vec<String>,
156    /// Response types supported
157    pub response_types_supported: Vec<String>,
158}
159
160impl OIDCProviderConfig {
161    /// Create new provider configuration
162    pub fn new(
163        issuer: String,
164        authorization_endpoint: String,
165        token_endpoint: String,
166        jwks_uri: String,
167    ) -> Self {
168        Self {
169            issuer,
170            authorization_endpoint,
171            token_endpoint,
172            userinfo_endpoint: None,
173            jwks_uri,
174            scopes_supported: vec![
175                "openid".to_string(),
176                "profile".to_string(),
177                "email".to_string(),
178            ],
179            response_types_supported: vec!["code".to_string()],
180        }
181    }
182}
183
184/// OAuth2 client for authorization code flow
185#[derive(Debug, Clone)]
186pub struct OAuth2Client {
187    /// Client ID from provider
188    pub client_id:              String,
189    /// Client secret from provider
190    #[allow(dead_code)]
191    client_secret:              String,
192    /// Authorization endpoint
193    pub authorization_endpoint: String,
194    /// Token endpoint
195    #[allow(dead_code)]
196    token_endpoint:             String,
197    /// Scopes to request
198    pub scopes:                 Vec<String>,
199    /// Use PKCE for additional security
200    pub use_pkce:               bool,
201}
202
203impl OAuth2Client {
204    /// Create new OAuth2 client
205    pub fn new(
206        client_id: impl Into<String>,
207        client_secret: impl Into<String>,
208        authorization_endpoint: impl Into<String>,
209        token_endpoint: impl Into<String>,
210    ) -> Self {
211        Self {
212            client_id:              client_id.into(),
213            client_secret:          client_secret.into(),
214            authorization_endpoint: authorization_endpoint.into(),
215            token_endpoint:         token_endpoint.into(),
216            scopes:                 vec![
217                "openid".to_string(),
218                "profile".to_string(),
219                "email".to_string(),
220            ],
221            use_pkce:               false,
222        }
223    }
224
225    /// Set scopes for request
226    pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
227        self.scopes = scopes;
228        self
229    }
230
231    /// Enable PKCE protection
232    pub fn with_pkce(mut self, enabled: bool) -> Self {
233        self.use_pkce = enabled;
234        self
235    }
236
237    /// Generate authorization URL
238    pub fn authorization_url(&self, redirect_uri: &str) -> Result<String, String> {
239        let state = uuid::Uuid::new_v4().to_string();
240        let scope = self.scopes.join(" ");
241
242        let url = format!(
243            "{}?client_id={}&redirect_uri={}&response_type=code&scope={}&state={}",
244            self.authorization_endpoint,
245            urlencoding::encode(&self.client_id),
246            urlencoding::encode(redirect_uri),
247            urlencoding::encode(&scope),
248            urlencoding::encode(&state),
249        );
250
251        Ok(url)
252    }
253
254    /// Exchange authorization code for tokens
255    pub async fn exchange_code(
256        &self,
257        _code: &str,
258        _redirect_uri: &str,
259    ) -> Result<TokenResponse, String> {
260        // Return mock token for GREEN phase
261        Ok(TokenResponse {
262            access_token:  format!("access_token_{}", uuid::Uuid::new_v4()),
263            refresh_token: Some(format!("refresh_token_{}", uuid::Uuid::new_v4())),
264            token_type:    "Bearer".to_string(),
265            expires_in:    3600,
266            id_token:      Some("mock_id_token".to_string()),
267            scope:         Some(self.scopes.join(" ")),
268        })
269    }
270
271    /// Refresh access token
272    pub async fn refresh_token(&self, refresh_token: &str) -> Result<TokenResponse, String> {
273        // Return mock token for GREEN phase
274        Ok(TokenResponse {
275            access_token:  format!("access_token_{}", uuid::Uuid::new_v4()),
276            refresh_token: Some(refresh_token.to_string()),
277            token_type:    "Bearer".to_string(),
278            expires_in:    3600,
279            id_token:      None,
280            scope:         Some(self.scopes.join(" ")),
281        })
282    }
283}
284
285/// OIDC client for OpenID Connect flow
286#[derive(Debug, Clone)]
287pub struct OIDCClient {
288    /// Provider configuration
289    pub config:    OIDCProviderConfig,
290    /// Client ID
291    pub client_id: String,
292    /// Client secret
293    #[allow(dead_code)]
294    client_secret: String,
295}
296
297impl OIDCClient {
298    /// Create new OIDC client
299    pub fn new(
300        config: OIDCProviderConfig,
301        client_id: impl Into<String>,
302        client_secret: impl Into<String>,
303    ) -> Self {
304        Self {
305            config,
306            client_id: client_id.into(),
307            client_secret: client_secret.into(),
308        }
309    }
310
311    /// Verify ID token claims
312    pub fn verify_id_token(
313        &self,
314        _id_token: &str,
315        expected_nonce: Option<&str>,
316    ) -> Result<IdTokenClaims, String> {
317        // Return mock claims for GREEN phase
318        let claims = IdTokenClaims {
319            iss:            self.config.issuer.clone(),
320            sub:            "user_123".to_string(),
321            aud:            self.client_id.clone(),
322            exp:            (Utc::now() + Duration::hours(1)).timestamp(),
323            iat:            Utc::now().timestamp(),
324            auth_time:      Some(Utc::now().timestamp()),
325            nonce:          expected_nonce.map(|s| s.to_string()),
326            email:          Some("user@example.com".to_string()),
327            email_verified: Some(true),
328            name:           Some("Test User".to_string()),
329            picture:        None,
330            locale:         Some("en-US".to_string()),
331        };
332
333        // Verify nonce if provided
334        if let Some(expected) = expected_nonce {
335            if claims.nonce.as_deref() != Some(expected) {
336                return Err("Nonce mismatch".to_string());
337            }
338        }
339
340        Ok(claims)
341    }
342
343    /// Get userinfo from provider
344    pub async fn get_userinfo(&self, _access_token: &str) -> Result<UserInfo, String> {
345        // Return mock userinfo for GREEN phase
346        Ok(UserInfo {
347            sub:            "user_123".to_string(),
348            email:          Some("user@example.com".to_string()),
349            email_verified: Some(true),
350            name:           Some("Test User".to_string()),
351            picture:        None,
352            locale:         Some("en-US".to_string()),
353        })
354    }
355}
356
357/// External authentication provider type
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
359pub enum ProviderType {
360    /// OAuth2 provider
361    OAuth2,
362    /// OIDC provider
363    OIDC,
364}
365
366impl std::fmt::Display for ProviderType {
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        match self {
369            Self::OAuth2 => write!(f, "oauth2"),
370            Self::OIDC => write!(f, "oidc"),
371        }
372    }
373}
374
375/// OAuth session stored in database
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct OAuthSession {
378    /// Session ID
379    pub id:               String,
380    /// User ID (local system)
381    pub user_id:          String,
382    /// Provider type (oauth2, oidc)
383    pub provider_type:    ProviderType,
384    /// Provider name (Auth0, Google, etc.)
385    pub provider_name:    String,
386    /// Provider's user ID (sub claim)
387    pub provider_user_id: String,
388    /// Access token (encrypted)
389    pub access_token:     String,
390    /// Refresh token (encrypted), if available
391    pub refresh_token:    Option<String>,
392    /// When access token expires
393    pub token_expiry:     DateTime<Utc>,
394    /// Session creation time
395    pub created_at:       DateTime<Utc>,
396    /// Last time token was refreshed
397    pub last_refreshed:   Option<DateTime<Utc>>,
398}
399
400impl OAuthSession {
401    /// Create new OAuth session
402    pub fn new(
403        user_id: String,
404        provider_type: ProviderType,
405        provider_name: String,
406        provider_user_id: String,
407        access_token: String,
408        token_expiry: DateTime<Utc>,
409    ) -> Self {
410        Self {
411            id: uuid::Uuid::new_v4().to_string(),
412            user_id,
413            provider_type,
414            provider_name,
415            provider_user_id,
416            access_token,
417            refresh_token: None,
418            token_expiry,
419            created_at: Utc::now(),
420            last_refreshed: None,
421        }
422    }
423
424    /// Check if session is expired
425    pub fn is_expired(&self) -> bool {
426        self.token_expiry <= Utc::now()
427    }
428
429    /// Check if session will be expired within grace period
430    pub fn is_expiring_soon(&self, grace_seconds: i64) -> bool {
431        self.token_expiry <= (Utc::now() + Duration::seconds(grace_seconds))
432    }
433
434    /// Update tokens after refresh
435    pub fn refresh_tokens(&mut self, access_token: String, token_expiry: DateTime<Utc>) {
436        self.access_token = access_token;
437        self.token_expiry = token_expiry;
438        self.last_refreshed = Some(Utc::now());
439    }
440}
441
442/// External auth provider configuration
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
444pub struct ExternalAuthProvider {
445    /// Provider ID
446    pub id: String,
447    /// Provider type (oauth2, oidc)
448    pub provider_type: ProviderType,
449    /// Provider name (Auth0, Google, Microsoft, Okta)
450    pub provider_name: String,
451    /// Client ID
452    pub client_id: String,
453    /// Client secret (should be fetched from vault)
454    pub client_secret_vault_path: String,
455    /// Provider configuration (OIDC)
456    pub oidc_config: Option<OIDCProviderConfig>,
457    /// OAuth2 configuration
458    pub oauth2_config: Option<OAuth2ClientConfig>,
459    /// Enabled flag
460    pub enabled: bool,
461    /// Requested scopes
462    pub scopes: Vec<String>,
463}
464
465/// OAuth2 client configuration
466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
467pub struct OAuth2ClientConfig {
468    /// Authorization endpoint
469    pub authorization_endpoint: String,
470    /// Token endpoint
471    pub token_endpoint:         String,
472    /// Use PKCE
473    pub use_pkce:               bool,
474}
475
476impl ExternalAuthProvider {
477    /// Create new external auth provider
478    pub fn new(
479        provider_type: ProviderType,
480        provider_name: impl Into<String>,
481        client_id: impl Into<String>,
482        client_secret_vault_path: impl Into<String>,
483    ) -> Self {
484        Self {
485            id: uuid::Uuid::new_v4().to_string(),
486            provider_type,
487            provider_name: provider_name.into(),
488            client_id: client_id.into(),
489            client_secret_vault_path: client_secret_vault_path.into(),
490            oidc_config: None,
491            oauth2_config: None,
492            enabled: true,
493            scopes: vec![
494                "openid".to_string(),
495                "profile".to_string(),
496                "email".to_string(),
497            ],
498        }
499    }
500
501    /// Enable or disable provider
502    pub fn set_enabled(&mut self, enabled: bool) {
503        self.enabled = enabled;
504    }
505
506    /// Set requested scopes
507    pub fn set_scopes(&mut self, scopes: Vec<String>) {
508        self.scopes = scopes;
509    }
510}
511
512/// Provider registry managing multiple OAuth providers
513#[derive(Debug, Clone)]
514pub struct ProviderRegistry {
515    /// Map of providers by name
516    providers: Arc<std::sync::Mutex<HashMap<String, ExternalAuthProvider>>>,
517}
518
519impl ProviderRegistry {
520    /// Create new provider registry
521    pub fn new() -> Self {
522        Self {
523            providers: Arc::new(std::sync::Mutex::new(HashMap::new())),
524        }
525    }
526
527    /// Register provider
528    pub fn register(&self, provider: ExternalAuthProvider) -> Result<(), String> {
529        let mut providers = self.providers.lock().map_err(|_| "Lock failed".to_string())?;
530        providers.insert(provider.provider_name.clone(), provider);
531        Ok(())
532    }
533
534    /// Get provider by name
535    pub fn get(&self, name: &str) -> Result<Option<ExternalAuthProvider>, String> {
536        let providers = self.providers.lock().map_err(|_| "Lock failed".to_string())?;
537        Ok(providers.get(name).cloned())
538    }
539
540    /// List all enabled providers
541    pub fn list_enabled(&self) -> Result<Vec<ExternalAuthProvider>, String> {
542        let providers = self.providers.lock().map_err(|_| "Lock failed".to_string())?;
543        Ok(providers.values().filter(|p| p.enabled).cloned().collect())
544    }
545
546    /// Disable provider
547    pub fn disable(&self, name: &str) -> Result<bool, String> {
548        let mut providers = self.providers.lock().map_err(|_| "Lock failed".to_string())?;
549        if let Some(provider) = providers.get_mut(name) {
550            provider.set_enabled(false);
551            Ok(true)
552        } else {
553            Ok(false)
554        }
555    }
556
557    /// Enable provider
558    pub fn enable(&self, name: &str) -> Result<bool, String> {
559        let mut providers = self.providers.lock().map_err(|_| "Lock failed".to_string())?;
560        if let Some(provider) = providers.get_mut(name) {
561            provider.set_enabled(true);
562            Ok(true)
563        } else {
564            Ok(false)
565        }
566    }
567}
568
569impl Default for ProviderRegistry {
570    fn default() -> Self {
571        Self::new()
572    }
573}
574
575/// PKCE code challenge for public clients
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct PKCEChallenge {
578    /// Random code verifier (43-128 characters)
579    pub code_verifier:         String,
580    /// BASE64URL(SHA256(code_verifier))
581    pub code_challenge:        String,
582    /// Challenge method: "S256" (SHA256)
583    pub code_challenge_method: String,
584}
585
586impl PKCEChallenge {
587    /// Generate new PKCE challenge
588    pub fn new() -> Self {
589        use sha2::{Digest, Sha256};
590
591        // Generate random verifier
592        let verifier = format!("{}", uuid::Uuid::new_v4());
593
594        // Compute challenge
595        let mut hasher = Sha256::new();
596        hasher.update(verifier.as_bytes());
597        let digest = hasher.finalize();
598        let challenge = urlencoding::encode_binary(&digest).to_string();
599
600        Self {
601            code_verifier:         verifier,
602            code_challenge:        challenge,
603            code_challenge_method: "S256".to_string(),
604        }
605    }
606
607    /// Verify code verifier matches challenge
608    pub fn verify(&self, verifier: &str) -> bool {
609        use sha2::{Digest, Sha256};
610
611        let mut hasher = Sha256::new();
612        hasher.update(verifier.as_bytes());
613        let digest = hasher.finalize();
614        let computed_challenge = urlencoding::encode_binary(&digest).to_string();
615
616        computed_challenge == self.code_challenge
617    }
618}
619
620impl Default for PKCEChallenge {
621    fn default() -> Self {
622        Self::new()
623    }
624}
625
626/// OAuth state parameter for CSRF protection
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct StateParameter {
629    /// Random state value
630    pub state:      String,
631    /// When state expires
632    pub expires_at: DateTime<Utc>,
633}
634
635impl StateParameter {
636    /// Generate new state parameter
637    pub fn new() -> Self {
638        Self {
639            state:      uuid::Uuid::new_v4().to_string(),
640            expires_at: Utc::now() + Duration::minutes(10),
641        }
642    }
643
644    /// Check if state is expired
645    pub fn is_expired(&self) -> bool {
646        self.expires_at <= Utc::now()
647    }
648
649    /// Verify state matches and is not expired
650    pub fn verify(&self, provided_state: &str) -> bool {
651        self.state == provided_state && !self.is_expired()
652    }
653}
654
655impl Default for StateParameter {
656    fn default() -> Self {
657        Self::new()
658    }
659}
660
661/// Nonce parameter for replay protection
662#[derive(Debug, Clone, Serialize, Deserialize)]
663pub struct NonceParameter {
664    /// Random nonce value
665    pub nonce:      String,
666    /// When nonce expires
667    pub expires_at: DateTime<Utc>,
668}
669
670impl NonceParameter {
671    /// Generate new nonce
672    pub fn new() -> Self {
673        Self {
674            nonce:      uuid::Uuid::new_v4().to_string(),
675            expires_at: Utc::now() + Duration::minutes(10),
676        }
677    }
678
679    /// Check if nonce is expired
680    pub fn is_expired(&self) -> bool {
681        self.expires_at <= Utc::now()
682    }
683
684    /// Verify nonce matches and is not expired
685    pub fn verify(&self, provided_nonce: &str) -> bool {
686        self.nonce == provided_nonce && !self.is_expired()
687    }
688}
689
690impl Default for NonceParameter {
691    fn default() -> Self {
692        Self::new()
693    }
694}
695
696/// Token refresh scheduler
697#[derive(Debug, Clone)]
698pub struct TokenRefreshScheduler {
699    /// Sessions needing refresh
700    refresh_queue: Arc<std::sync::Mutex<Vec<(String, DateTime<Utc>)>>>,
701}
702
703impl TokenRefreshScheduler {
704    /// Create new refresh scheduler
705    pub fn new() -> Self {
706        Self {
707            refresh_queue: Arc::new(std::sync::Mutex::new(Vec::new())),
708        }
709    }
710
711    /// Schedule token refresh for session
712    pub fn schedule_refresh(
713        &self,
714        session_id: String,
715        refresh_time: DateTime<Utc>,
716    ) -> Result<(), String> {
717        let mut queue = self.refresh_queue.lock().map_err(|_| "Lock failed".to_string())?;
718        queue.push((session_id, refresh_time));
719        queue.sort_by_key(|(_, time)| *time);
720        Ok(())
721    }
722
723    /// Get next session to refresh
724    pub fn get_next_refresh(&self) -> Result<Option<String>, String> {
725        let mut queue = self.refresh_queue.lock().map_err(|_| "Lock failed".to_string())?;
726        if let Some((_, refresh_time)) = queue.first() {
727            if *refresh_time <= Utc::now() {
728                let (id, _) = queue.remove(0);
729                return Ok(Some(id));
730            }
731        }
732        Ok(None)
733    }
734
735    /// Cancel scheduled refresh
736    pub fn cancel_refresh(&self, session_id: &str) -> Result<bool, String> {
737        let mut queue = self.refresh_queue.lock().map_err(|_| "Lock failed".to_string())?;
738        let len_before = queue.len();
739        queue.retain(|(id, _)| id != session_id);
740        Ok(queue.len() < len_before)
741    }
742}
743
744impl Default for TokenRefreshScheduler {
745    fn default() -> Self {
746        Self::new()
747    }
748}
749
750/// Multi-provider failover manager
751#[derive(Debug, Clone)]
752pub struct ProviderFailoverManager {
753    /// Primary provider name
754    primary_provider:   String,
755    /// Fallback providers in priority order
756    fallback_providers: Vec<String>,
757    /// Providers currently unavailable
758    unavailable:        Arc<std::sync::Mutex<Vec<(String, DateTime<Utc>)>>>,
759}
760
761impl ProviderFailoverManager {
762    /// Create new failover manager
763    pub fn new(primary: String, fallbacks: Vec<String>) -> Self {
764        Self {
765            primary_provider:   primary,
766            fallback_providers: fallbacks,
767            unavailable:        Arc::new(std::sync::Mutex::new(Vec::new())),
768        }
769    }
770
771    /// Get next available provider
772    pub fn get_available_provider(&self) -> Result<String, String> {
773        let unavailable = self.unavailable.lock().map_err(|_| "Lock failed".to_string())?;
774        let now = Utc::now();
775
776        // Check if primary is available
777        if !unavailable
778            .iter()
779            .any(|(name, exp)| name == &self.primary_provider && *exp > now)
780        {
781            return Ok(self.primary_provider.clone());
782        }
783
784        // Find first available fallback
785        for fallback in &self.fallback_providers {
786            if !unavailable.iter().any(|(name, exp)| name == fallback && *exp > now) {
787                return Ok(fallback.clone());
788            }
789        }
790
791        Err("No providers available".to_string())
792    }
793
794    /// Mark provider as unavailable
795    pub fn mark_unavailable(&self, provider: String, duration_seconds: u64) -> Result<(), String> {
796        let mut unavailable = self.unavailable.lock().map_err(|_| "Lock failed".to_string())?;
797        unavailable.push((provider, Utc::now() + Duration::seconds(duration_seconds as i64)));
798        Ok(())
799    }
800
801    /// Mark provider as available
802    pub fn mark_available(&self, provider: &str) -> Result<(), String> {
803        let mut unavailable = self.unavailable.lock().map_err(|_| "Lock failed".to_string())?;
804        unavailable.retain(|(name, _)| name != provider);
805        Ok(())
806    }
807}
808
809/// OAuth audit event for logging
810#[derive(Debug, Clone, Serialize, Deserialize)]
811pub struct OAuthAuditEvent {
812    /// Event type: authorization, token_exchange, token_refresh, logout
813    pub event_type: String,
814    /// Provider name
815    pub provider:   String,
816    /// User ID (if known)
817    pub user_id:    Option<String>,
818    /// Status: success, failed
819    pub status:     String,
820    /// Error message (if failed)
821    pub error:      Option<String>,
822    /// Timestamp
823    pub timestamp:  DateTime<Utc>,
824    /// Additional metadata
825    pub metadata:   HashMap<String, String>,
826}
827
828impl OAuthAuditEvent {
829    /// Create new audit event
830    pub fn new(
831        event_type: impl Into<String>,
832        provider: impl Into<String>,
833        status: impl Into<String>,
834    ) -> Self {
835        Self {
836            event_type: event_type.into(),
837            provider:   provider.into(),
838            user_id:    None,
839            status:     status.into(),
840            error:      None,
841            timestamp:  Utc::now(),
842            metadata:   HashMap::new(),
843        }
844    }
845
846    /// Set user ID
847    pub fn with_user_id(mut self, user_id: String) -> Self {
848        self.user_id = Some(user_id);
849        self
850    }
851
852    /// Set error message
853    pub fn with_error(mut self, error: String) -> Self {
854        self.error = Some(error);
855        self
856    }
857
858    /// Add metadata
859    pub fn with_metadata(mut self, key: String, value: String) -> Self {
860        self.metadata.insert(key, value);
861        self
862    }
863}
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868
869    #[test]
870    fn test_token_response_creation() {
871        let token = TokenResponse::new("token123".to_string(), "Bearer".to_string(), 3600);
872        assert_eq!(token.access_token, "token123");
873        assert_eq!(token.token_type, "Bearer");
874        assert_eq!(token.expires_in, 3600);
875    }
876
877    #[test]
878    fn test_token_response_expiry_calculation() {
879        let token = TokenResponse::new("token123".to_string(), "Bearer".to_string(), 3600);
880        assert!(!token.is_expired());
881    }
882
883    #[test]
884    fn test_id_token_claims_creation() {
885        let exp = (Utc::now() + Duration::hours(1)).timestamp();
886        let claims = IdTokenClaims::new(
887            "https://provider.com".to_string(),
888            "user123".to_string(),
889            "client_id".to_string(),
890            exp,
891            Utc::now().timestamp(),
892        );
893        assert_eq!(claims.sub, "user123");
894        assert!(!claims.is_expired());
895    }
896
897    #[test]
898    fn test_id_token_claims_expiry() {
899        let exp = (Utc::now() - Duration::hours(1)).timestamp();
900        let claims = IdTokenClaims::new(
901            "https://provider.com".to_string(),
902            "user123".to_string(),
903            "client_id".to_string(),
904            exp,
905            (Utc::now() - Duration::hours(2)).timestamp(),
906        );
907        assert!(claims.is_expired());
908    }
909
910    #[test]
911    fn test_userinfo_creation() {
912        let userinfo = UserInfo::new("user123".to_string());
913        assert_eq!(userinfo.sub, "user123");
914        assert!(userinfo.email.is_none());
915    }
916
917    #[test]
918    fn test_oauth2_client_creation() {
919        let client = OAuth2Client::new(
920            "client_id",
921            "client_secret",
922            "https://provider.com/authorize",
923            "https://provider.com/token",
924        );
925        assert_eq!(client.client_id, "client_id");
926    }
927
928    #[test]
929    fn test_oauth2_client_with_scopes() {
930        let scopes = vec!["openid".to_string(), "profile".to_string()];
931        let client = OAuth2Client::new(
932            "client_id",
933            "client_secret",
934            "https://provider.com/authorize",
935            "https://provider.com/token",
936        )
937        .with_scopes(scopes.clone());
938        assert_eq!(client.scopes, scopes);
939    }
940
941    #[test]
942    fn test_oidc_provider_config_creation() {
943        let config = OIDCProviderConfig::new(
944            "https://provider.com".to_string(),
945            "https://provider.com/authorize".to_string(),
946            "https://provider.com/token".to_string(),
947            "https://provider.com/jwks".to_string(),
948        );
949        assert_eq!(config.issuer, "https://provider.com");
950    }
951
952    #[test]
953    fn test_oauth_session_creation() {
954        let session = OAuthSession::new(
955            "user_123".to_string(),
956            ProviderType::OIDC,
957            "auth0".to_string(),
958            "auth0|user_id".to_string(),
959            "access_token".to_string(),
960            Utc::now() + Duration::hours(1),
961        );
962        assert_eq!(session.user_id, "user_123");
963        assert!(!session.is_expired());
964    }
965
966    #[test]
967    fn test_oauth_session_token_refresh() {
968        let mut session = OAuthSession::new(
969            "user_123".to_string(),
970            ProviderType::OIDC,
971            "auth0".to_string(),
972            "auth0|user_id".to_string(),
973            "old_token".to_string(),
974            Utc::now() + Duration::hours(1),
975        );
976        let new_expiry = Utc::now() + Duration::hours(2);
977        session.refresh_tokens("new_token".to_string(), new_expiry);
978        assert_eq!(session.access_token, "new_token");
979        assert!(session.last_refreshed.is_some());
980    }
981
982    #[test]
983    fn test_external_auth_provider_creation() {
984        let provider = ExternalAuthProvider::new(
985            ProviderType::OIDC,
986            "auth0",
987            "client_id",
988            "vault/path/to/secret",
989        );
990        assert_eq!(provider.provider_name, "auth0");
991        assert!(provider.enabled);
992    }
993
994    #[test]
995    fn test_provider_registry_register_and_get() {
996        let registry = ProviderRegistry::new();
997        let provider =
998            ExternalAuthProvider::new(ProviderType::OIDC, "auth0", "client_id", "vault/path");
999        registry.register(provider.clone()).unwrap();
1000        let retrieved = registry.get("auth0").unwrap();
1001        assert_eq!(retrieved, Some(provider));
1002    }
1003
1004    #[test]
1005    fn test_provider_registry_list_enabled() {
1006        let registry = ProviderRegistry::new();
1007        let provider1 = ExternalAuthProvider::new(ProviderType::OIDC, "auth0", "id1", "path1");
1008        let provider2 = ExternalAuthProvider::new(ProviderType::OAuth2, "google", "id2", "path2");
1009        registry.register(provider1).unwrap();
1010        registry.register(provider2).unwrap();
1011        let enabled = registry.list_enabled().unwrap();
1012        assert_eq!(enabled.len(), 2);
1013    }
1014
1015    #[test]
1016    fn test_provider_registry_disable_enable() {
1017        let registry = ProviderRegistry::new();
1018        let provider = ExternalAuthProvider::new(ProviderType::OIDC, "auth0", "id", "path");
1019        registry.register(provider).unwrap();
1020
1021        registry.disable("auth0").unwrap();
1022        let retrieved = registry.get("auth0").unwrap();
1023        assert!(!retrieved.unwrap().enabled);
1024
1025        registry.enable("auth0").unwrap();
1026        let retrieved = registry.get("auth0").unwrap();
1027        assert!(retrieved.unwrap().enabled);
1028    }
1029
1030    #[test]
1031    fn test_pkce_challenge_generation() {
1032        let challenge = PKCEChallenge::new();
1033        assert!(!challenge.code_verifier.is_empty());
1034        assert!(!challenge.code_challenge.is_empty());
1035        assert_eq!(challenge.code_challenge_method, "S256");
1036    }
1037
1038    #[test]
1039    fn test_pkce_verification() {
1040        let challenge = PKCEChallenge::new();
1041        let verifier = challenge.code_verifier.clone();
1042        assert!(challenge.verify(&verifier));
1043    }
1044
1045    #[test]
1046    fn test_pkce_verification_fails_with_wrong_verifier() {
1047        let challenge = PKCEChallenge::new();
1048        assert!(!challenge.verify("wrong_verifier"));
1049    }
1050
1051    #[test]
1052    fn test_state_parameter_generation() {
1053        let state = StateParameter::new();
1054        assert!(!state.state.is_empty());
1055        assert!(!state.is_expired());
1056    }
1057
1058    #[test]
1059    fn test_state_parameter_verification() {
1060        let state = StateParameter::new();
1061        assert!(state.verify(&state.state));
1062    }
1063
1064    #[test]
1065    fn test_state_parameter_verification_fails_with_wrong_state() {
1066        let state = StateParameter::new();
1067        assert!(!state.verify("wrong_state"));
1068    }
1069
1070    #[test]
1071    fn test_nonce_parameter_generation() {
1072        let nonce = NonceParameter::new();
1073        assert!(!nonce.nonce.is_empty());
1074        assert!(!nonce.is_expired());
1075    }
1076
1077    #[test]
1078    fn test_nonce_parameter_verification() {
1079        let nonce = NonceParameter::new();
1080        assert!(nonce.verify(&nonce.nonce));
1081    }
1082
1083    #[test]
1084    fn test_token_refresh_scheduler_schedule_and_retrieve() {
1085        let scheduler = TokenRefreshScheduler::new();
1086        let refresh_time = Utc::now() - Duration::seconds(10);
1087        scheduler.schedule_refresh("session_1".to_string(), refresh_time).unwrap();
1088
1089        let next = scheduler.get_next_refresh().unwrap();
1090        assert_eq!(next, Some("session_1".to_string()));
1091    }
1092
1093    #[test]
1094    fn test_token_refresh_scheduler_cancel() {
1095        let scheduler = TokenRefreshScheduler::new();
1096        let refresh_time = Utc::now() + Duration::hours(1);
1097        scheduler.schedule_refresh("session_1".to_string(), refresh_time).unwrap();
1098
1099        let cancelled = scheduler.cancel_refresh("session_1").unwrap();
1100        assert!(cancelled);
1101    }
1102
1103    #[test]
1104    fn test_failover_manager_primary_available() {
1105        let manager = ProviderFailoverManager::new("auth0".to_string(), vec!["google".to_string()]);
1106        let available = manager.get_available_provider().unwrap();
1107        assert_eq!(available, "auth0");
1108    }
1109
1110    #[test]
1111    fn test_failover_manager_fallback() {
1112        let manager = ProviderFailoverManager::new("auth0".to_string(), vec!["google".to_string()]);
1113        manager.mark_unavailable("auth0".to_string(), 300).unwrap();
1114        let available = manager.get_available_provider().unwrap();
1115        assert_eq!(available, "google");
1116    }
1117
1118    #[test]
1119    fn test_failover_manager_mark_available() {
1120        let manager = ProviderFailoverManager::new("auth0".to_string(), vec!["google".to_string()]);
1121        manager.mark_unavailable("auth0".to_string(), 300).unwrap();
1122        manager.mark_available("auth0").unwrap();
1123        let available = manager.get_available_provider().unwrap();
1124        assert_eq!(available, "auth0");
1125    }
1126
1127    #[test]
1128    fn test_oauth_audit_event_creation() {
1129        let event = OAuthAuditEvent::new("authorization", "auth0", "success");
1130        assert_eq!(event.event_type, "authorization");
1131        assert_eq!(event.provider, "auth0");
1132        assert_eq!(event.status, "success");
1133    }
1134
1135    #[test]
1136    fn test_oauth_audit_event_with_user_id() {
1137        let event = OAuthAuditEvent::new("token_exchange", "auth0", "success")
1138            .with_user_id("user_123".to_string());
1139        assert_eq!(event.user_id, Some("user_123".to_string()));
1140    }
1141
1142    #[test]
1143    fn test_oauth_audit_event_with_error() {
1144        let event = OAuthAuditEvent::new("token_exchange", "auth0", "failed")
1145            .with_error("Provider unavailable".to_string());
1146        assert_eq!(event.error, Some("Provider unavailable".to_string()));
1147    }
1148
1149    #[test]
1150    fn test_oauth_audit_event_with_metadata() {
1151        let event = OAuthAuditEvent::new("authorization", "auth0", "success")
1152            .with_metadata("ip_address".to_string(), "192.168.1.1".to_string());
1153        assert_eq!(event.metadata.get("ip_address"), Some(&"192.168.1.1".to_string()));
1154    }
1155}