mockforge_http/auth/
oidc.rs

1//! OpenID Connect (OIDC) simulation support
2//!
3//! This module provides OIDC-compliant endpoints for simulating identity providers,
4//! including discovery documents and JSON Web Key Set (JWKS) endpoints.
5
6use axum::response::Json;
7use jsonwebtoken::{Algorithm, EncodingKey, Header};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14use mockforge_core::Error;
15
16/// OIDC configuration
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct OidcConfig {
19    /// Whether OIDC is enabled
20    pub enabled: bool,
21    /// Issuer identifier (e.g., "https://mockforge.example.com")
22    pub issuer: String,
23    /// JWKS configuration
24    pub jwks: JwksConfig,
25    /// Default claims to include in tokens
26    pub claims: ClaimsConfig,
27    /// Multi-tenant configuration
28    pub multi_tenant: Option<MultiTenantConfig>,
29}
30
31/// JWKS (JSON Web Key Set) configuration
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct JwksConfig {
34    /// List of signing keys
35    pub keys: Vec<JwkKey>,
36}
37
38/// JSON Web Key configuration
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct JwkKey {
41    /// Key ID
42    pub kid: String,
43    /// Algorithm (RS256, ES256, HS256, etc.)
44    pub alg: String,
45    /// Public key (PEM format for RSA/ECDSA, base64 for HMAC)
46    pub public_key: String,
47    /// Private key (PEM format for RSA/ECDSA, base64 for HMAC) - optional, used for signing
48    #[serde(skip_serializing)]
49    pub private_key: Option<String>,
50    /// Key type (RSA, EC, oct)
51    pub kty: String,
52    /// Key use (sig, enc)
53    #[serde(default = "default_key_use")]
54    pub use_: String,
55}
56
57fn default_key_use() -> String {
58    "sig".to_string()
59}
60
61/// Claims configuration
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ClaimsConfig {
64    /// Default claims to include in all tokens
65    pub default: Vec<String>,
66    /// Custom claim templates
67    #[serde(default)]
68    pub custom: HashMap<String, serde_json::Value>,
69}
70
71impl Default for ClaimsConfig {
72    fn default() -> Self {
73        Self {
74            default: vec!["sub".to_string(), "iss".to_string(), "exp".to_string()],
75            custom: HashMap::new(),
76        }
77    }
78}
79
80/// Multi-tenant configuration
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct MultiTenantConfig {
83    /// Whether multi-tenant mode is enabled
84    pub enabled: bool,
85    /// Claim name for organization ID
86    pub org_id_claim: String,
87    /// Claim name for tenant ID
88    pub tenant_id_claim: Option<String>,
89}
90
91impl Default for OidcConfig {
92    fn default() -> Self {
93        Self {
94            enabled: false,
95            issuer: "https://mockforge.example.com".to_string(),
96            jwks: JwksConfig { keys: vec![] },
97            claims: ClaimsConfig {
98                default: vec!["sub".to_string(), "iss".to_string(), "exp".to_string()],
99                custom: HashMap::new(),
100            },
101            multi_tenant: None,
102        }
103    }
104}
105
106/// OIDC discovery document response
107#[derive(Debug, Serialize)]
108pub struct OidcDiscoveryDocument {
109    /// Issuer identifier
110    pub issuer: String,
111    /// Authorization endpoint
112    pub authorization_endpoint: String,
113    /// Token endpoint
114    pub token_endpoint: String,
115    /// UserInfo endpoint
116    pub userinfo_endpoint: String,
117    /// JWKS URI
118    pub jwks_uri: String,
119    /// Supported response types
120    pub response_types_supported: Vec<String>,
121    /// Supported subject types
122    pub subject_types_supported: Vec<String>,
123    /// Supported ID token signing algorithms
124    pub id_token_signing_alg_values_supported: Vec<String>,
125    /// Supported scopes
126    pub scopes_supported: Vec<String>,
127    /// Supported claims
128    pub claims_supported: Vec<String>,
129    /// Supported grant types
130    pub grant_types_supported: Vec<String>,
131}
132
133/// JWKS response
134#[derive(Debug, Serialize)]
135pub struct JwksResponse {
136    /// Array of JSON Web Keys
137    pub keys: Vec<JwkPublicKey>,
138}
139
140/// Public JSON Web Key (for JWKS endpoint - no private key)
141#[derive(Debug, Serialize)]
142pub struct JwkPublicKey {
143    /// Key ID
144    pub kid: String,
145    /// Key type
146    pub kty: String,
147    /// Algorithm
148    pub alg: String,
149    /// Key use
150    #[serde(rename = "use")]
151    pub use_: String,
152    /// RSA modulus (for RSA keys)
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub n: Option<String>,
155    /// RSA exponent (for RSA keys)
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub e: Option<String>,
158    /// EC curve (for EC keys)
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub crv: Option<String>,
161    /// EC X coordinate (for EC keys)
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub x: Option<String>,
164    /// EC Y coordinate (for EC keys)
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub y: Option<String>,
167}
168
169/// OIDC state stored in AuthState
170#[derive(Clone)]
171pub struct OidcState {
172    /// OIDC configuration
173    pub config: OidcConfig,
174    /// Active signing keys (indexed by kid)
175    pub signing_keys: Arc<RwLock<HashMap<String, EncodingKey>>>,
176}
177
178impl OidcState {
179    /// Create new OIDC state from configuration
180    pub fn new(config: OidcConfig) -> Result<Self, Error> {
181        let mut signing_keys = HashMap::new();
182
183        // Load signing keys
184        for key in &config.jwks.keys {
185            if let Some(ref private_key) = key.private_key {
186                let encoding_key = match key.alg.as_str() {
187                    "RS256" | "RS384" | "RS512" => {
188                        EncodingKey::from_rsa_pem(private_key.as_bytes()).map_err(|e| {
189                            Error::generic(format!("Failed to load RSA key {}: {}", key.kid, e))
190                        })?
191                    }
192                    "ES256" | "ES384" | "ES512" => EncodingKey::from_ec_pem(private_key.as_bytes())
193                        .map_err(|e| {
194                            Error::generic(format!("Failed to load EC key {}: {}", key.kid, e))
195                        })?,
196                    "HS256" | "HS384" | "HS512" => EncodingKey::from_secret(private_key.as_bytes()),
197                    _ => {
198                        return Err(Error::generic(format!("Unsupported algorithm: {}", key.alg)));
199                    }
200                };
201                signing_keys.insert(key.kid.clone(), encoding_key);
202            }
203        }
204
205        Ok(Self {
206            config,
207            signing_keys: Arc::new(RwLock::new(signing_keys)),
208        })
209    }
210
211    /// Create OIDC state with default configuration for mock server
212    ///
213    /// This creates a basic OIDC configuration suitable for testing and development.
214    /// For production use, load configuration from config files or environment variables.
215    pub fn default_mock() -> Result<Self, Error> {
216        use std::env;
217
218        // Get issuer from environment or use default
219        let issuer = env::var("MOCKFORGE_OIDC_ISSUER").unwrap_or_else(|_| {
220            env::var("MOCKFORGE_BASE_URL")
221                .unwrap_or_else(|_| "https://mockforge.example.com".to_string())
222        });
223
224        // Create default HS256 key for signing (suitable for development/testing)
225        let default_secret = env::var("MOCKFORGE_OIDC_SECRET")
226            .unwrap_or_else(|_| "mockforge-default-secret-key-change-in-production".to_string());
227
228        let default_key = JwkKey {
229            kid: "default".to_string(),
230            alg: "HS256".to_string(),
231            public_key: default_secret.clone(),
232            private_key: Some(default_secret),
233            kty: "oct".to_string(),
234            use_: "sig".to_string(),
235        };
236
237        let config = OidcConfig {
238            enabled: true,
239            issuer,
240            jwks: JwksConfig {
241                keys: vec![default_key],
242            },
243            claims: ClaimsConfig {
244                default: vec!["sub".to_string(), "iss".to_string(), "exp".to_string()],
245                custom: HashMap::new(),
246            },
247            multi_tenant: None,
248        };
249
250        Self::new(config)
251    }
252}
253
254/// Helper function to load OIDC state from configuration
255///
256/// Attempts to load OIDC configuration from:
257/// 1. Environment variables (MOCKFORGE_OIDC_CONFIG, MOCKFORGE_OIDC_ISSUER, etc.)
258/// 2. Config file (if available)
259/// 3. Default mock configuration
260///
261/// Returns None if OIDC is not configured or disabled.
262pub fn load_oidc_state() -> Option<OidcState> {
263    use std::env;
264
265    // Check if OIDC is explicitly disabled
266    if let Ok(disabled) = env::var("MOCKFORGE_OIDC_ENABLED") {
267        if disabled == "false" || disabled == "0" {
268            return None;
269        }
270    }
271
272    // Try to load from environment variable (JSON config)
273    if let Ok(config_json) = env::var("MOCKFORGE_OIDC_CONFIG") {
274        if let Ok(config) = serde_json::from_str::<OidcConfig>(&config_json) {
275            if config.enabled {
276                return OidcState::new(config).ok();
277            }
278            return None;
279        }
280    }
281
282    // Try to load from config file (future enhancement)
283    // For now, use default mock configuration if OIDC is not explicitly disabled
284    OidcState::default_mock().ok()
285}
286
287/// Get OIDC discovery document
288pub async fn get_oidc_discovery() -> Json<OidcDiscoveryDocument> {
289    // Get base URL from environment variable or use default
290    // In production, this would be loaded from configuration
291    let base_url = std::env::var("MOCKFORGE_BASE_URL")
292        .unwrap_or_else(|_| "https://mockforge.example.com".to_string());
293
294    let discovery = OidcDiscoveryDocument {
295        issuer: base_url.clone(),
296        authorization_endpoint: format!("{}/oauth2/authorize", base_url),
297        token_endpoint: format!("{}/oauth2/token", base_url),
298        userinfo_endpoint: format!("{}/oauth2/userinfo", base_url),
299        jwks_uri: format!("{}/.well-known/jwks.json", base_url),
300        response_types_supported: vec![
301            "code".to_string(),
302            "id_token".to_string(),
303            "token id_token".to_string(),
304        ],
305        subject_types_supported: vec!["public".to_string()],
306        id_token_signing_alg_values_supported: vec![
307            "RS256".to_string(),
308            "ES256".to_string(),
309            "HS256".to_string(),
310        ],
311        scopes_supported: vec![
312            "openid".to_string(),
313            "profile".to_string(),
314            "email".to_string(),
315            "address".to_string(),
316            "phone".to_string(),
317        ],
318        claims_supported: vec![
319            "sub".to_string(),
320            "iss".to_string(),
321            "aud".to_string(),
322            "exp".to_string(),
323            "iat".to_string(),
324            "auth_time".to_string(),
325            "nonce".to_string(),
326            "email".to_string(),
327            "email_verified".to_string(),
328            "name".to_string(),
329            "given_name".to_string(),
330            "family_name".to_string(),
331        ],
332        grant_types_supported: vec![
333            "authorization_code".to_string(),
334            "implicit".to_string(),
335            "refresh_token".to_string(),
336            "client_credentials".to_string(),
337        ],
338    };
339
340    Json(discovery)
341}
342
343/// Get JWKS (JSON Web Key Set)
344pub async fn get_jwks() -> Json<JwksResponse> {
345    // Return empty JWKS by default
346    // Use get_jwks_from_state() when OIDC state is available from request context
347    let jwks = JwksResponse { keys: vec![] };
348
349    Json(jwks)
350}
351
352/// Get JWKS from OIDC state
353pub fn get_jwks_from_state(oidc_state: &OidcState) -> Result<JwksResponse, Error> {
354    use crate::auth::jwks_converter::convert_jwk_key_simple;
355
356    let mut public_keys = Vec::new();
357
358    for key in &oidc_state.config.jwks.keys {
359        match convert_jwk_key_simple(key) {
360            Ok(jwk) => public_keys.push(jwk),
361            Err(e) => {
362                tracing::warn!("Failed to convert key {} to JWK format: {}", key.kid, e);
363                // Continue with other keys
364            }
365        }
366    }
367
368    Ok(JwksResponse { keys: public_keys })
369}
370
371/// Generate a signed JWT token with configurable claims
372///
373/// # Arguments
374/// * `claims` - Map of claim names to values
375/// * `kid` - Optional key ID for the signing key
376/// * `algorithm` - Signing algorithm (RS256, ES256, HS256, etc.)
377/// * `encoding_key` - Encoding key for signing
378/// * `expires_in_seconds` - Optional expiration time in seconds from now
379/// * `issuer` - Optional issuer claim
380/// * `audience` - Optional audience claim
381pub fn generate_signed_jwt(
382    mut claims: HashMap<String, serde_json::Value>,
383    kid: Option<String>,
384    algorithm: Algorithm,
385    encoding_key: &EncodingKey,
386    expires_in_seconds: Option<i64>,
387    issuer: Option<String>,
388    audience: Option<String>,
389) -> Result<String, Error> {
390    use chrono::Utc;
391
392    let mut header = Header::new(algorithm);
393    if let Some(kid) = kid {
394        header.kid = Some(kid);
395    }
396
397    // Add standard claims
398    let now = Utc::now();
399    claims.insert("iat".to_string(), json!(now.timestamp()));
400
401    if let Some(exp_seconds) = expires_in_seconds {
402        let exp = now + chrono::Duration::seconds(exp_seconds);
403        claims.insert("exp".to_string(), json!(exp.timestamp()));
404    }
405
406    if let Some(iss) = issuer {
407        claims.insert("iss".to_string(), json!(iss));
408    }
409
410    if let Some(aud) = audience {
411        claims.insert("aud".to_string(), json!(aud));
412    }
413
414    let token = jsonwebtoken::encode(&header, &claims, encoding_key)
415        .map_err(|e| Error::generic(format!("Failed to sign JWT: {}", e)))?;
416
417    Ok(token)
418}
419
420/// Tenant context for multi-tenant token generation
421#[derive(Debug, Clone)]
422pub struct TenantContext {
423    /// Organization ID
424    pub org_id: Option<String>,
425    /// Tenant ID
426    pub tenant_id: Option<String>,
427}
428
429/// Generate a signed JWT token with default claims from OIDC config
430pub fn generate_oidc_token(
431    oidc_state: &OidcState,
432    subject: String,
433    additional_claims: Option<HashMap<String, serde_json::Value>>,
434    expires_in_seconds: Option<i64>,
435    tenant_context: Option<TenantContext>,
436) -> Result<String, Error> {
437    use chrono::Utc;
438    use jsonwebtoken::Algorithm;
439
440    // Start with default claims
441    let mut claims = HashMap::new();
442    claims.insert("sub".to_string(), json!(subject));
443    claims.insert("iss".to_string(), json!(oidc_state.config.issuer.clone()));
444
445    // Add default claims from config
446    for claim_name in &oidc_state.config.claims.default {
447        if !claims.contains_key(claim_name) {
448            // Add standard claim if not already present
449            match claim_name.as_str() {
450                "sub" | "iss" => {} // Already added
451                "exp" => {
452                    let exp_seconds = expires_in_seconds.unwrap_or(3600);
453                    let exp = Utc::now() + chrono::Duration::seconds(exp_seconds);
454                    claims.insert("exp".to_string(), json!(exp.timestamp()));
455                }
456                "iat" => {
457                    claims.insert("iat".to_string(), json!(Utc::now().timestamp()));
458                }
459                _ => {
460                    // Use custom claim value if available
461                    if let Some(value) = oidc_state.config.claims.custom.get(claim_name) {
462                        claims.insert(claim_name.clone(), value.clone());
463                    }
464                }
465            }
466        }
467    }
468
469    // Add custom claims from config
470    for (key, value) in &oidc_state.config.claims.custom {
471        if !claims.contains_key(key) {
472            claims.insert(key.clone(), value.clone());
473        }
474    }
475
476    // Add multi-tenant claims if enabled
477    if let Some(ref mt_config) = oidc_state.config.multi_tenant {
478        if mt_config.enabled {
479            // Get org_id and tenant_id from tenant context or use defaults
480            let org_id = tenant_context
481                .as_ref()
482                .and_then(|ctx| ctx.org_id.clone())
483                .unwrap_or_else(|| "org-default".to_string());
484            let tenant_id = tenant_context
485                .as_ref()
486                .and_then(|ctx| ctx.tenant_id.clone())
487                .or_else(|| Some("tenant-default".to_string()));
488
489            claims.insert(mt_config.org_id_claim.clone(), json!(org_id));
490            if let Some(ref tenant_claim) = mt_config.tenant_id_claim {
491                if let Some(tid) = tenant_id {
492                    claims.insert(tenant_claim.clone(), json!(tid));
493                }
494            }
495        }
496    }
497
498    // Merge additional claims (override defaults)
499    if let Some(additional) = additional_claims {
500        for (key, value) in additional {
501            claims.insert(key, value);
502        }
503    }
504
505    // Get signing key (use first available key for now)
506    let signing_keys = oidc_state.signing_keys.blocking_read();
507    let (kid, encoding_key) = signing_keys
508        .iter()
509        .next()
510        .ok_or_else(|| Error::generic("No signing keys available".to_string()))?;
511
512    // Determine algorithm from key configuration
513    // Default to HS256 if algorithm not specified in key config
514    let algorithm = oidc_state
515        .config
516        .jwks
517        .keys
518        .iter()
519        .find(|k| k.kid == *kid)
520        .and_then(|k| match k.alg.as_str() {
521            "RS256" => Some(Algorithm::RS256),
522            "RS384" => Some(Algorithm::RS384),
523            "RS512" => Some(Algorithm::RS512),
524            "ES256" => Some(Algorithm::ES256),
525            "ES384" => Some(Algorithm::ES384),
526            "HS256" => Some(Algorithm::HS256),
527            "HS384" => Some(Algorithm::HS384),
528            "HS512" => Some(Algorithm::HS512),
529            _ => None,
530        })
531        .unwrap_or(Algorithm::HS256);
532
533    generate_signed_jwt(
534        claims,
535        Some(kid.clone()),
536        algorithm,
537        encoding_key,
538        expires_in_seconds,
539        Some(oidc_state.config.issuer.clone()),
540        None,
541    )
542}
543
544/// Create OIDC router with well-known endpoints
545pub fn oidc_router() -> axum::Router {
546    use axum::{routing::get, Router};
547
548    Router::new()
549        .route("/.well-known/openid-configuration", get(get_oidc_discovery))
550        .route("/.well-known/jwks.json", get(get_jwks))
551}
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use base64::engine::general_purpose;
557    use base64::Engine;
558    use jsonwebtoken::Algorithm;
559    use serde_json::json;
560
561    #[test]
562    fn test_default_key_use() {
563        assert_eq!(default_key_use(), "sig");
564    }
565
566    #[test]
567    fn test_oidc_config_default() {
568        let config = OidcConfig::default();
569        assert!(!config.enabled);
570        assert_eq!(config.issuer, "https://mockforge.example.com");
571        assert!(config.jwks.keys.is_empty());
572        assert_eq!(config.claims.default, vec!["sub", "iss", "exp"]);
573        assert!(config.claims.custom.is_empty());
574        assert!(config.multi_tenant.is_none());
575    }
576
577    #[test]
578    fn test_jwk_key_serialization() {
579        let key = JwkKey {
580            kid: "test-key".to_string(),
581            alg: "RS256".to_string(),
582            public_key: "public-key-data".to_string(),
583            private_key: Some("private-key-data".to_string()),
584            kty: "RSA".to_string(),
585            use_: "sig".to_string(),
586        };
587
588        let serialized = serde_json::to_value(&key).unwrap();
589        assert_eq!(serialized["kid"], "test-key");
590        assert_eq!(serialized["alg"], "RS256");
591        assert_eq!(serialized["kty"], "RSA");
592        // Private key should be skipped
593        assert!(serialized.get("private_key").is_none());
594    }
595
596    #[test]
597    fn test_oidc_state_new_with_hs256_key() {
598        let config = OidcConfig {
599            enabled: true,
600            issuer: "https://test.example.com".to_string(),
601            jwks: JwksConfig {
602                keys: vec![JwkKey {
603                    kid: "test-hs256".to_string(),
604                    alg: "HS256".to_string(),
605                    public_key: "test-secret-key".to_string(),
606                    private_key: Some("test-secret-key".to_string()),
607                    kty: "oct".to_string(),
608                    use_: "sig".to_string(),
609                }],
610            },
611            claims: ClaimsConfig {
612                default: vec!["sub".to_string(), "iss".to_string()],
613                custom: HashMap::new(),
614            },
615            multi_tenant: None,
616        };
617
618        let state = OidcState::new(config.clone()).unwrap();
619        assert_eq!(state.config.issuer, "https://test.example.com");
620
621        let signing_keys = state.signing_keys.blocking_read();
622        assert_eq!(signing_keys.len(), 1);
623        assert!(signing_keys.contains_key("test-hs256"));
624    }
625
626    #[test]
627    fn test_oidc_state_new_with_unsupported_algorithm() {
628        let config = OidcConfig {
629            enabled: true,
630            issuer: "https://test.example.com".to_string(),
631            jwks: JwksConfig {
632                keys: vec![JwkKey {
633                    kid: "test-unsupported".to_string(),
634                    alg: "UNSUPPORTED".to_string(),
635                    public_key: "key-data".to_string(),
636                    private_key: Some("key-data".to_string()),
637                    kty: "oct".to_string(),
638                    use_: "sig".to_string(),
639                }],
640            },
641            claims: ClaimsConfig::default(),
642            multi_tenant: None,
643        };
644
645        let result = OidcState::new(config);
646        assert!(result.is_err());
647    }
648
649    #[test]
650    fn test_oidc_state_default_mock() {
651        std::env::remove_var("MOCKFORGE_OIDC_ISSUER");
652        std::env::remove_var("MOCKFORGE_BASE_URL");
653        std::env::remove_var("MOCKFORGE_OIDC_SECRET");
654
655        let state = OidcState::default_mock().unwrap();
656        assert!(state.config.enabled);
657        assert_eq!(state.config.issuer, "https://mockforge.example.com");
658
659        let signing_keys = state.signing_keys.blocking_read();
660        assert_eq!(signing_keys.len(), 1);
661        assert!(signing_keys.contains_key("default"));
662    }
663
664    #[test]
665    fn test_oidc_state_default_mock_with_env() {
666        std::env::set_var("MOCKFORGE_OIDC_ISSUER", "https://custom.example.com");
667        std::env::set_var("MOCKFORGE_OIDC_SECRET", "custom-secret");
668
669        let state = OidcState::default_mock().unwrap();
670        assert_eq!(state.config.issuer, "https://custom.example.com");
671
672        std::env::remove_var("MOCKFORGE_OIDC_ISSUER");
673        std::env::remove_var("MOCKFORGE_OIDC_SECRET");
674    }
675
676    #[test]
677    fn test_load_oidc_state_disabled() {
678        std::env::set_var("MOCKFORGE_OIDC_ENABLED", "false");
679        let result = load_oidc_state();
680        assert!(result.is_none());
681        std::env::remove_var("MOCKFORGE_OIDC_ENABLED");
682    }
683
684    #[test]
685    fn test_load_oidc_state_from_json_config() {
686        let config_json = json!({
687            "enabled": true,
688            "issuer": "https://json-config.example.com",
689            "jwks": {
690                "keys": [{
691                    "kid": "json-key",
692                    "alg": "HS256",
693                    "public_key": "json-secret",
694                    "private_key": "json-secret",
695                    "kty": "oct",
696                    "use": "sig"
697                }]
698            },
699            "claims": {
700                "default": ["sub", "iss"],
701                "custom": {}
702            }
703        });
704
705        std::env::set_var("MOCKFORGE_OIDC_CONFIG", config_json.to_string());
706        let state = load_oidc_state();
707        assert!(state.is_some());
708
709        if let Some(state) = state {
710            assert_eq!(state.config.issuer, "https://json-config.example.com");
711        }
712
713        std::env::remove_var("MOCKFORGE_OIDC_CONFIG");
714    }
715
716    #[tokio::test]
717    async fn test_get_oidc_discovery() {
718        std::env::set_var("MOCKFORGE_BASE_URL", "https://test.mockforge.com");
719        let response = get_oidc_discovery().await;
720        let discovery = response.0;
721
722        assert_eq!(discovery.issuer, "https://test.mockforge.com");
723        assert_eq!(discovery.authorization_endpoint, "https://test.mockforge.com/oauth2/authorize");
724        assert_eq!(discovery.token_endpoint, "https://test.mockforge.com/oauth2/token");
725        assert_eq!(discovery.userinfo_endpoint, "https://test.mockforge.com/oauth2/userinfo");
726        assert_eq!(discovery.jwks_uri, "https://test.mockforge.com/.well-known/jwks.json");
727        assert!(discovery.response_types_supported.contains(&"code".to_string()));
728        assert!(discovery.scopes_supported.contains(&"openid".to_string()));
729        assert!(discovery.grant_types_supported.contains(&"authorization_code".to_string()));
730
731        std::env::remove_var("MOCKFORGE_BASE_URL");
732    }
733
734    #[tokio::test]
735    async fn test_get_jwks_empty() {
736        let response = get_jwks().await;
737        let jwks = response.0;
738        assert!(jwks.keys.is_empty());
739    }
740
741    #[test]
742    fn test_get_jwks_from_state() {
743        let state = OidcState::default_mock().unwrap();
744        let result = get_jwks_from_state(&state);
745        assert!(result.is_ok());
746    }
747
748    #[test]
749    fn test_generate_signed_jwt_basic() {
750        let mut claims = HashMap::new();
751        claims.insert("sub".to_string(), json!("user123"));
752
753        let secret = "test-secret-key";
754        let encoding_key = EncodingKey::from_secret(secret.as_bytes());
755
756        let token = generate_signed_jwt(
757            claims,
758            Some("test-kid".to_string()),
759            Algorithm::HS256,
760            &encoding_key,
761            Some(3600),
762            Some("https://test.issuer.com".to_string()),
763            Some("test-audience".to_string()),
764        );
765
766        assert!(token.is_ok());
767        let token_str = token.unwrap();
768        assert!(!token_str.is_empty());
769
770        // Verify the token can be decoded
771        use jsonwebtoken::{decode, DecodingKey, Validation};
772        let decoding_key = DecodingKey::from_secret(secret.as_bytes());
773        let mut validation = Validation::new(Algorithm::HS256);
774        validation.set_issuer(&["https://test.issuer.com"]);
775        validation.set_audience(&["test-audience"]);
776
777        let decoded =
778            decode::<HashMap<String, serde_json::Value>>(&token_str, &decoding_key, &validation);
779        assert!(decoded.is_ok());
780
781        let claims = decoded.unwrap().claims;
782        assert_eq!(claims.get("sub").unwrap(), "user123");
783        assert_eq!(claims.get("iss").unwrap(), "https://test.issuer.com");
784        assert_eq!(claims.get("aud").unwrap(), "test-audience");
785        assert!(claims.contains_key("iat"));
786        assert!(claims.contains_key("exp"));
787    }
788
789    #[test]
790    fn test_generate_signed_jwt_without_expiration() {
791        let mut claims = HashMap::new();
792        claims.insert("sub".to_string(), json!("user123"));
793
794        let secret = "test-secret-key";
795        let encoding_key = EncodingKey::from_secret(secret.as_bytes());
796
797        let token =
798            generate_signed_jwt(claims, None, Algorithm::HS256, &encoding_key, None, None, None);
799
800        assert!(token.is_ok());
801        let token_str = token.unwrap();
802
803        // Verify the token has iat but no exp
804        let parts: Vec<&str> = token_str.split('.').collect();
805        assert_eq!(parts.len(), 3);
806
807        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
808        let payload_json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
809        assert!(payload_json.get("iat").is_some());
810    }
811
812    #[test]
813    fn test_generate_oidc_token_basic() {
814        let state = OidcState::default_mock().unwrap();
815
816        let token = generate_oidc_token(&state, "user123".to_string(), None, Some(3600), None);
817
818        assert!(token.is_ok());
819        let token_str = token.unwrap();
820        assert!(!token_str.is_empty());
821
822        // Decode and verify claims
823        let parts: Vec<&str> = token_str.split('.').collect();
824        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
825        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
826
827        assert_eq!(claims.get("sub").unwrap(), "user123");
828        assert_eq!(claims.get("iss").unwrap(), &state.config.issuer);
829        assert!(claims.get("exp").is_some());
830        assert!(claims.get("iat").is_some());
831    }
832
833    #[test]
834    fn test_generate_oidc_token_with_additional_claims() {
835        let state = OidcState::default_mock().unwrap();
836
837        let mut additional = HashMap::new();
838        additional.insert("email".to_string(), json!("user@example.com"));
839        additional.insert("role".to_string(), json!("admin"));
840
841        let token =
842            generate_oidc_token(&state, "user123".to_string(), Some(additional), Some(3600), None);
843
844        assert!(token.is_ok());
845        let token_str = token.unwrap();
846
847        let parts: Vec<&str> = token_str.split('.').collect();
848        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
849        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
850
851        assert_eq!(claims.get("email").unwrap(), "user@example.com");
852        assert_eq!(claims.get("role").unwrap(), "admin");
853    }
854
855    #[test]
856    fn test_generate_oidc_token_with_multi_tenant() {
857        let config = OidcConfig {
858            enabled: true,
859            issuer: "https://test.example.com".to_string(),
860            jwks: JwksConfig {
861                keys: vec![JwkKey {
862                    kid: "test-key".to_string(),
863                    alg: "HS256".to_string(),
864                    public_key: "secret".to_string(),
865                    private_key: Some("secret".to_string()),
866                    kty: "oct".to_string(),
867                    use_: "sig".to_string(),
868                }],
869            },
870            claims: ClaimsConfig {
871                default: vec!["sub".to_string()],
872                custom: HashMap::new(),
873            },
874            multi_tenant: Some(MultiTenantConfig {
875                enabled: true,
876                org_id_claim: "org_id".to_string(),
877                tenant_id_claim: Some("tenant_id".to_string()),
878            }),
879        };
880
881        let state = OidcState::new(config).unwrap();
882
883        let tenant_context = TenantContext {
884            org_id: Some("org-123".to_string()),
885            tenant_id: Some("tenant-456".to_string()),
886        };
887
888        let token = generate_oidc_token(
889            &state,
890            "user123".to_string(),
891            None,
892            Some(3600),
893            Some(tenant_context),
894        );
895
896        assert!(token.is_ok());
897        let token_str = token.unwrap();
898
899        let parts: Vec<&str> = token_str.split('.').collect();
900        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
901        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
902
903        assert_eq!(claims.get("org_id").unwrap(), "org-123");
904        assert_eq!(claims.get("tenant_id").unwrap(), "tenant-456");
905    }
906
907    #[test]
908    fn test_generate_oidc_token_multi_tenant_defaults() {
909        let config = OidcConfig {
910            enabled: true,
911            issuer: "https://test.example.com".to_string(),
912            jwks: JwksConfig {
913                keys: vec![JwkKey {
914                    kid: "test-key".to_string(),
915                    alg: "HS256".to_string(),
916                    public_key: "secret".to_string(),
917                    private_key: Some("secret".to_string()),
918                    kty: "oct".to_string(),
919                    use_: "sig".to_string(),
920                }],
921            },
922            claims: ClaimsConfig::default(),
923            multi_tenant: Some(MultiTenantConfig {
924                enabled: true,
925                org_id_claim: "org_id".to_string(),
926                tenant_id_claim: Some("tenant_id".to_string()),
927            }),
928        };
929
930        let state = OidcState::new(config).unwrap();
931
932        // No tenant context provided
933        let token = generate_oidc_token(&state, "user123".to_string(), None, Some(3600), None);
934
935        assert!(token.is_ok());
936        let token_str = token.unwrap();
937
938        let parts: Vec<&str> = token_str.split('.').collect();
939        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
940        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
941
942        // Should have default values
943        assert_eq!(claims.get("org_id").unwrap(), "org-default");
944        assert_eq!(claims.get("tenant_id").unwrap(), "tenant-default");
945    }
946
947    #[test]
948    fn test_generate_oidc_token_no_signing_keys() {
949        let config = OidcConfig {
950            enabled: true,
951            issuer: "https://test.example.com".to_string(),
952            jwks: JwksConfig { keys: vec![] },
953            claims: ClaimsConfig::default(),
954            multi_tenant: None,
955        };
956
957        let state = OidcState::new(config).unwrap();
958
959        let token = generate_oidc_token(&state, "user123".to_string(), None, Some(3600), None);
960
961        assert!(token.is_err());
962    }
963
964    #[test]
965    fn test_tenant_context_creation() {
966        let context = TenantContext {
967            org_id: Some("org-1".to_string()),
968            tenant_id: Some("tenant-1".to_string()),
969        };
970
971        assert_eq!(context.org_id.unwrap(), "org-1");
972        assert_eq!(context.tenant_id.unwrap(), "tenant-1");
973    }
974
975    #[test]
976    fn test_claims_config_serialization() {
977        let config = ClaimsConfig {
978            default: vec!["sub".to_string(), "iss".to_string()],
979            custom: {
980                let mut map = HashMap::new();
981                map.insert("custom_claim".to_string(), json!("custom_value"));
982                map
983            },
984        };
985
986        let serialized = serde_json::to_value(&config).unwrap();
987        assert_eq!(serialized["default"].as_array().unwrap().len(), 2);
988        assert_eq!(serialized["custom"]["custom_claim"], "custom_value");
989    }
990
991    #[test]
992    fn test_multi_tenant_config_serialization() {
993        let config = MultiTenantConfig {
994            enabled: true,
995            org_id_claim: "organization_id".to_string(),
996            tenant_id_claim: Some("tenant".to_string()),
997        };
998
999        let serialized = serde_json::to_value(&config).unwrap();
1000        assert_eq!(serialized["enabled"], true);
1001        assert_eq!(serialized["org_id_claim"], "organization_id");
1002        assert_eq!(serialized["tenant_id_claim"], "tenant");
1003    }
1004
1005    #[test]
1006    fn test_oidc_discovery_document_serialization() {
1007        let doc = OidcDiscoveryDocument {
1008            issuer: "https://example.com".to_string(),
1009            authorization_endpoint: "https://example.com/auth".to_string(),
1010            token_endpoint: "https://example.com/token".to_string(),
1011            userinfo_endpoint: "https://example.com/userinfo".to_string(),
1012            jwks_uri: "https://example.com/jwks".to_string(),
1013            response_types_supported: vec!["code".to_string()],
1014            subject_types_supported: vec!["public".to_string()],
1015            id_token_signing_alg_values_supported: vec!["RS256".to_string()],
1016            scopes_supported: vec!["openid".to_string()],
1017            claims_supported: vec!["sub".to_string()],
1018            grant_types_supported: vec!["authorization_code".to_string()],
1019        };
1020
1021        let serialized = serde_json::to_value(&doc).unwrap();
1022        assert_eq!(serialized["issuer"], "https://example.com");
1023        assert_eq!(serialized["jwks_uri"], "https://example.com/jwks");
1024    }
1025
1026    #[test]
1027    fn test_jwks_response_serialization() {
1028        let response = JwksResponse {
1029            keys: vec![JwkPublicKey {
1030                kid: "key1".to_string(),
1031                kty: "RSA".to_string(),
1032                alg: "RS256".to_string(),
1033                use_: "sig".to_string(),
1034                n: Some("modulus".to_string()),
1035                e: Some("exponent".to_string()),
1036                crv: None,
1037                x: None,
1038                y: None,
1039            }],
1040        };
1041
1042        let serialized = serde_json::to_value(&response).unwrap();
1043        assert_eq!(serialized["keys"][0]["kid"], "key1");
1044        assert_eq!(serialized["keys"][0]["kty"], "RSA");
1045        assert_eq!(serialized["keys"][0]["use"], "sig");
1046    }
1047
1048    #[test]
1049    fn test_jwk_public_key_rsa() {
1050        let key = JwkPublicKey {
1051            kid: "rsa-key".to_string(),
1052            kty: "RSA".to_string(),
1053            alg: "RS256".to_string(),
1054            use_: "sig".to_string(),
1055            n: Some("modulus-data".to_string()),
1056            e: Some("exponent-data".to_string()),
1057            crv: None,
1058            x: None,
1059            y: None,
1060        };
1061
1062        let serialized = serde_json::to_value(&key).unwrap();
1063        assert_eq!(serialized["kty"], "RSA");
1064        assert_eq!(serialized["n"], "modulus-data");
1065        assert_eq!(serialized["e"], "exponent-data");
1066        // EC fields should not be present
1067        assert!(serialized.get("crv").is_none());
1068        assert!(serialized.get("x").is_none());
1069        assert!(serialized.get("y").is_none());
1070    }
1071
1072    #[test]
1073    fn test_jwk_public_key_ec() {
1074        let key = JwkPublicKey {
1075            kid: "ec-key".to_string(),
1076            kty: "EC".to_string(),
1077            alg: "ES256".to_string(),
1078            use_: "sig".to_string(),
1079            n: None,
1080            e: None,
1081            crv: Some("P-256".to_string()),
1082            x: Some("x-coordinate".to_string()),
1083            y: Some("y-coordinate".to_string()),
1084        };
1085
1086        let serialized = serde_json::to_value(&key).unwrap();
1087        assert_eq!(serialized["kty"], "EC");
1088        assert_eq!(serialized["crv"], "P-256");
1089        assert_eq!(serialized["x"], "x-coordinate");
1090        assert_eq!(serialized["y"], "y-coordinate");
1091        // RSA fields should not be present
1092        assert!(serialized.get("n").is_none());
1093        assert!(serialized.get("e").is_none());
1094    }
1095}