Skip to main content

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::internal(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::internal(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::internal(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::internal(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::internal("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    // Run the environment-reading code in a separate process for each fixture.
562    // Parent tests never mutate global environment shared with parallel tests.
563    fn isolated_env_test(name: &str, variables: &[(&str, &str)]) -> bool {
564        let test_name = format!("auth::oidc::tests::{name}");
565        if std::env::var("MOCKFORGE_OIDC_TEST_CHILD").as_deref() == Ok(test_name.as_str()) {
566            return true;
567        }
568        let mut command = std::process::Command::new(std::env::current_exe().unwrap());
569        command
570            .arg("--exact")
571            .arg(&test_name)
572            .arg("--test-threads=1")
573            .arg("--color=never")
574            .env("MOCKFORGE_OIDC_TEST_CHILD", &test_name)
575            .stdin(std::process::Stdio::null())
576            .stdout(std::process::Stdio::piped())
577            .stderr(std::process::Stdio::piped());
578        for key in [
579            "MOCKFORGE_OIDC_ISSUER",
580            "MOCKFORGE_BASE_URL",
581            "MOCKFORGE_OIDC_SECRET",
582            "MOCKFORGE_OIDC_ENABLED",
583            "MOCKFORGE_OIDC_CONFIG",
584        ] {
585            command.env_remove(key);
586        }
587        for (key, value) in variables {
588            command.env(key, value);
589        }
590        let output = command.output().unwrap();
591        let expected = format!("test {test_name} ... ok");
592        // A typo in the exact filter must fail rather than pass with zero tests.
593        assert!(
594            output.status.success() && String::from_utf8_lossy(&output.stdout).contains(&expected),
595            "isolated OIDC fixture did not pass: {name}"
596        );
597        false
598    }
599
600    #[test]
601    #[should_panic(expected = "isolated OIDC fixture did not pass")]
602    fn test_isolated_env_rejects_unknown_filter() {
603        isolated_env_test("missing_fixture", &[]);
604    }
605
606    #[test]
607    fn test_default_key_use() {
608        assert_eq!(default_key_use(), "sig");
609    }
610
611    #[test]
612    fn test_oidc_config_default() {
613        let config = OidcConfig::default();
614        assert!(!config.enabled);
615        assert_eq!(config.issuer, "https://mockforge.example.com");
616        assert!(config.jwks.keys.is_empty());
617        assert_eq!(config.claims.default, vec!["sub", "iss", "exp"]);
618        assert!(config.claims.custom.is_empty());
619        assert!(config.multi_tenant.is_none());
620    }
621
622    #[test]
623    fn test_jwk_key_serialization() {
624        let key = JwkKey {
625            kid: "test-key".to_string(),
626            alg: "RS256".to_string(),
627            public_key: "public-key-data".to_string(),
628            private_key: Some("private-key-data".to_string()),
629            kty: "RSA".to_string(),
630            use_: "sig".to_string(),
631        };
632
633        let serialized = serde_json::to_value(&key).unwrap();
634        assert_eq!(serialized["kid"], "test-key");
635        assert_eq!(serialized["alg"], "RS256");
636        assert_eq!(serialized["kty"], "RSA");
637        // Private key should be skipped
638        assert!(serialized.get("private_key").is_none());
639    }
640
641    #[test]
642    fn test_oidc_state_new_with_hs256_key() {
643        let config = OidcConfig {
644            enabled: true,
645            issuer: "https://test.example.com".to_string(),
646            jwks: JwksConfig {
647                keys: vec![JwkKey {
648                    kid: "test-hs256".to_string(),
649                    alg: "HS256".to_string(),
650                    public_key: "test-secret-key".to_string(),
651                    private_key: Some("test-secret-key".to_string()),
652                    kty: "oct".to_string(),
653                    use_: "sig".to_string(),
654                }],
655            },
656            claims: ClaimsConfig {
657                default: vec!["sub".to_string(), "iss".to_string()],
658                custom: HashMap::new(),
659            },
660            multi_tenant: None,
661        };
662
663        let state = OidcState::new(config.clone()).unwrap();
664        assert_eq!(state.config.issuer, "https://test.example.com");
665
666        let signing_keys = state.signing_keys.blocking_read();
667        assert_eq!(signing_keys.len(), 1);
668        assert!(signing_keys.contains_key("test-hs256"));
669    }
670
671    #[test]
672    fn test_oidc_state_new_with_unsupported_algorithm() {
673        let config = OidcConfig {
674            enabled: true,
675            issuer: "https://test.example.com".to_string(),
676            jwks: JwksConfig {
677                keys: vec![JwkKey {
678                    kid: "test-unsupported".to_string(),
679                    alg: "UNSUPPORTED".to_string(),
680                    public_key: "key-data".to_string(),
681                    private_key: Some("key-data".to_string()),
682                    kty: "oct".to_string(),
683                    use_: "sig".to_string(),
684                }],
685            },
686            claims: ClaimsConfig::default(),
687            multi_tenant: None,
688        };
689
690        let result = OidcState::new(config);
691        assert!(result.is_err());
692    }
693
694    #[test]
695    fn test_oidc_state_default_mock() {
696        if !isolated_env_test("test_oidc_state_default_mock", &[]) {
697            return;
698        }
699
700        let state = OidcState::default_mock().unwrap();
701        assert!(state.config.enabled);
702        assert_eq!(state.config.issuer, "https://mockforge.example.com");
703
704        let signing_keys = state.signing_keys.blocking_read();
705        assert_eq!(signing_keys.len(), 1);
706        assert!(signing_keys.contains_key("default"));
707    }
708
709    #[test]
710    fn test_oidc_state_default_mock_with_env() {
711        if !isolated_env_test(
712            "test_oidc_state_default_mock_with_env",
713            &[
714                ("MOCKFORGE_OIDC_ISSUER", "https://custom.example.com"),
715                ("MOCKFORGE_OIDC_SECRET", "custom-secret"),
716            ],
717        ) {
718            return;
719        }
720
721        let state = OidcState::default_mock().unwrap();
722        assert_eq!(state.config.issuer, "https://custom.example.com");
723    }
724
725    #[test]
726    fn test_load_oidc_state_disabled() {
727        if !isolated_env_test(
728            "test_load_oidc_state_disabled",
729            &[("MOCKFORGE_OIDC_ENABLED", "false")],
730        ) {
731            return;
732        }
733        let result = load_oidc_state();
734        assert!(result.is_none());
735    }
736
737    #[test]
738    fn test_load_oidc_state_from_json_config() {
739        let config_json = json!({
740            "enabled": true,
741            "issuer": "https://json-config.example.com",
742            "jwks": {
743                "keys": [{
744                    "kid": "json-key",
745                    "alg": "HS256",
746                    "public_key": "json-secret",
747                    "private_key": "json-secret",
748                    "kty": "oct",
749                    "use": "sig"
750                }]
751            },
752            "claims": {
753                "default": ["sub", "iss"],
754                "custom": {}
755            }
756        });
757
758        let config_json = config_json.to_string();
759        if !isolated_env_test(
760            "test_load_oidc_state_from_json_config",
761            &[("MOCKFORGE_OIDC_CONFIG", &config_json)],
762        ) {
763            return;
764        }
765        let state = load_oidc_state();
766        assert!(state.is_some());
767
768        if let Some(state) = state {
769            assert_eq!(state.config.issuer, "https://json-config.example.com");
770        }
771    }
772
773    #[tokio::test]
774    async fn test_get_oidc_discovery() {
775        if !isolated_env_test(
776            "test_get_oidc_discovery",
777            &[("MOCKFORGE_BASE_URL", "https://test.mockforge.com")],
778        ) {
779            return;
780        }
781        let response = get_oidc_discovery().await;
782        let discovery = response.0;
783
784        assert_eq!(discovery.issuer, "https://test.mockforge.com");
785        assert_eq!(discovery.authorization_endpoint, "https://test.mockforge.com/oauth2/authorize");
786        assert_eq!(discovery.token_endpoint, "https://test.mockforge.com/oauth2/token");
787        assert_eq!(discovery.userinfo_endpoint, "https://test.mockforge.com/oauth2/userinfo");
788        assert_eq!(discovery.jwks_uri, "https://test.mockforge.com/.well-known/jwks.json");
789        assert!(discovery.response_types_supported.contains(&"code".to_string()));
790        assert!(discovery.scopes_supported.contains(&"openid".to_string()));
791        assert!(discovery.grant_types_supported.contains(&"authorization_code".to_string()));
792    }
793
794    #[tokio::test]
795    async fn test_get_jwks_empty() {
796        let response = get_jwks().await;
797        let jwks = response.0;
798        assert!(jwks.keys.is_empty());
799    }
800
801    #[test]
802    fn test_get_jwks_from_state() {
803        let state = OidcState::default_mock().unwrap();
804        let result = get_jwks_from_state(&state);
805        assert!(result.is_ok());
806    }
807
808    #[test]
809    fn test_generate_signed_jwt_basic() {
810        let mut claims = HashMap::new();
811        claims.insert("sub".to_string(), json!("user123"));
812
813        let secret = "test-secret-key";
814        let encoding_key = EncodingKey::from_secret(secret.as_bytes());
815
816        let token = generate_signed_jwt(
817            claims,
818            Some("test-kid".to_string()),
819            Algorithm::HS256,
820            &encoding_key,
821            Some(3600),
822            Some("https://test.issuer.com".to_string()),
823            Some("test-audience".to_string()),
824        );
825
826        assert!(token.is_ok());
827        let token_str = token.unwrap();
828        assert!(!token_str.is_empty());
829
830        // Verify the token can be decoded
831        use jsonwebtoken::{decode, DecodingKey, Validation};
832        let decoding_key = DecodingKey::from_secret(secret.as_bytes());
833        let mut validation = Validation::new(Algorithm::HS256);
834        validation.set_issuer(&["https://test.issuer.com"]);
835        validation.set_audience(&["test-audience"]);
836
837        let decoded =
838            decode::<HashMap<String, serde_json::Value>>(&token_str, &decoding_key, &validation);
839        assert!(decoded.is_ok());
840
841        let claims = decoded.unwrap().claims;
842        assert_eq!(claims.get("sub").unwrap(), "user123");
843        assert_eq!(claims.get("iss").unwrap(), "https://test.issuer.com");
844        assert_eq!(claims.get("aud").unwrap(), "test-audience");
845        assert!(claims.contains_key("iat"));
846        assert!(claims.contains_key("exp"));
847    }
848
849    #[test]
850    fn test_generate_signed_jwt_without_expiration() {
851        let mut claims = HashMap::new();
852        claims.insert("sub".to_string(), json!("user123"));
853
854        let secret = "test-secret-key";
855        let encoding_key = EncodingKey::from_secret(secret.as_bytes());
856
857        let token =
858            generate_signed_jwt(claims, None, Algorithm::HS256, &encoding_key, None, None, None);
859
860        assert!(token.is_ok());
861        let token_str = token.unwrap();
862
863        // Verify the token has iat but no exp
864        let parts: Vec<&str> = token_str.split('.').collect();
865        assert_eq!(parts.len(), 3);
866
867        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
868        let payload_json: serde_json::Value = serde_json::from_slice(&payload).unwrap();
869        assert!(payload_json.get("iat").is_some());
870    }
871
872    #[test]
873    fn test_generate_oidc_token_basic() {
874        let state = OidcState::default_mock().unwrap();
875
876        let token = generate_oidc_token(&state, "user123".to_string(), None, Some(3600), None);
877
878        assert!(token.is_ok());
879        let token_str = token.unwrap();
880        assert!(!token_str.is_empty());
881
882        // Decode and verify claims
883        let parts: Vec<&str> = token_str.split('.').collect();
884        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
885        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
886
887        assert_eq!(claims.get("sub").unwrap(), "user123");
888        assert_eq!(claims.get("iss").unwrap(), &state.config.issuer);
889        assert!(claims.get("exp").is_some());
890        assert!(claims.get("iat").is_some());
891    }
892
893    #[test]
894    fn test_generate_oidc_token_with_additional_claims() {
895        let state = OidcState::default_mock().unwrap();
896
897        let mut additional = HashMap::new();
898        additional.insert("email".to_string(), json!("user@example.com"));
899        additional.insert("role".to_string(), json!("admin"));
900
901        let token =
902            generate_oidc_token(&state, "user123".to_string(), Some(additional), Some(3600), None);
903
904        assert!(token.is_ok());
905        let token_str = token.unwrap();
906
907        let parts: Vec<&str> = token_str.split('.').collect();
908        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
909        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
910
911        assert_eq!(claims.get("email").unwrap(), "user@example.com");
912        assert_eq!(claims.get("role").unwrap(), "admin");
913    }
914
915    #[test]
916    fn test_generate_oidc_token_with_multi_tenant() {
917        let config = OidcConfig {
918            enabled: true,
919            issuer: "https://test.example.com".to_string(),
920            jwks: JwksConfig {
921                keys: vec![JwkKey {
922                    kid: "test-key".to_string(),
923                    alg: "HS256".to_string(),
924                    public_key: "secret".to_string(),
925                    private_key: Some("secret".to_string()),
926                    kty: "oct".to_string(),
927                    use_: "sig".to_string(),
928                }],
929            },
930            claims: ClaimsConfig {
931                default: vec!["sub".to_string()],
932                custom: HashMap::new(),
933            },
934            multi_tenant: Some(MultiTenantConfig {
935                enabled: true,
936                org_id_claim: "org_id".to_string(),
937                tenant_id_claim: Some("tenant_id".to_string()),
938            }),
939        };
940
941        let state = OidcState::new(config).unwrap();
942
943        let tenant_context = TenantContext {
944            org_id: Some("org-123".to_string()),
945            tenant_id: Some("tenant-456".to_string()),
946        };
947
948        let token = generate_oidc_token(
949            &state,
950            "user123".to_string(),
951            None,
952            Some(3600),
953            Some(tenant_context),
954        );
955
956        assert!(token.is_ok());
957        let token_str = token.unwrap();
958
959        let parts: Vec<&str> = token_str.split('.').collect();
960        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
961        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
962
963        assert_eq!(claims.get("org_id").unwrap(), "org-123");
964        assert_eq!(claims.get("tenant_id").unwrap(), "tenant-456");
965    }
966
967    #[test]
968    fn test_generate_oidc_token_multi_tenant_defaults() {
969        let config = OidcConfig {
970            enabled: true,
971            issuer: "https://test.example.com".to_string(),
972            jwks: JwksConfig {
973                keys: vec![JwkKey {
974                    kid: "test-key".to_string(),
975                    alg: "HS256".to_string(),
976                    public_key: "secret".to_string(),
977                    private_key: Some("secret".to_string()),
978                    kty: "oct".to_string(),
979                    use_: "sig".to_string(),
980                }],
981            },
982            claims: ClaimsConfig::default(),
983            multi_tenant: Some(MultiTenantConfig {
984                enabled: true,
985                org_id_claim: "org_id".to_string(),
986                tenant_id_claim: Some("tenant_id".to_string()),
987            }),
988        };
989
990        let state = OidcState::new(config).unwrap();
991
992        // No tenant context provided
993        let token = generate_oidc_token(&state, "user123".to_string(), None, Some(3600), None);
994
995        assert!(token.is_ok());
996        let token_str = token.unwrap();
997
998        let parts: Vec<&str> = token_str.split('.').collect();
999        let payload = general_purpose::STANDARD_NO_PAD.decode(parts[1]).unwrap();
1000        let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
1001
1002        // Should have default values
1003        assert_eq!(claims.get("org_id").unwrap(), "org-default");
1004        assert_eq!(claims.get("tenant_id").unwrap(), "tenant-default");
1005    }
1006
1007    #[test]
1008    fn test_generate_oidc_token_no_signing_keys() {
1009        let config = OidcConfig {
1010            enabled: true,
1011            issuer: "https://test.example.com".to_string(),
1012            jwks: JwksConfig { keys: vec![] },
1013            claims: ClaimsConfig::default(),
1014            multi_tenant: None,
1015        };
1016
1017        let state = OidcState::new(config).unwrap();
1018
1019        let token = generate_oidc_token(&state, "user123".to_string(), None, Some(3600), None);
1020
1021        assert!(token.is_err());
1022    }
1023
1024    #[test]
1025    fn test_tenant_context_creation() {
1026        let context = TenantContext {
1027            org_id: Some("org-1".to_string()),
1028            tenant_id: Some("tenant-1".to_string()),
1029        };
1030
1031        assert_eq!(context.org_id.unwrap(), "org-1");
1032        assert_eq!(context.tenant_id.unwrap(), "tenant-1");
1033    }
1034
1035    #[test]
1036    fn test_claims_config_serialization() {
1037        let config = ClaimsConfig {
1038            default: vec!["sub".to_string(), "iss".to_string()],
1039            custom: {
1040                let mut map = HashMap::new();
1041                map.insert("custom_claim".to_string(), json!("custom_value"));
1042                map
1043            },
1044        };
1045
1046        let serialized = serde_json::to_value(&config).unwrap();
1047        assert_eq!(serialized["default"].as_array().unwrap().len(), 2);
1048        assert_eq!(serialized["custom"]["custom_claim"], "custom_value");
1049    }
1050
1051    #[test]
1052    fn test_multi_tenant_config_serialization() {
1053        let config = MultiTenantConfig {
1054            enabled: true,
1055            org_id_claim: "organization_id".to_string(),
1056            tenant_id_claim: Some("tenant".to_string()),
1057        };
1058
1059        let serialized = serde_json::to_value(&config).unwrap();
1060        assert_eq!(serialized["enabled"], true);
1061        assert_eq!(serialized["org_id_claim"], "organization_id");
1062        assert_eq!(serialized["tenant_id_claim"], "tenant");
1063    }
1064
1065    #[test]
1066    fn test_oidc_discovery_document_serialization() {
1067        let doc = OidcDiscoveryDocument {
1068            issuer: "https://example.com".to_string(),
1069            authorization_endpoint: "https://example.com/auth".to_string(),
1070            token_endpoint: "https://example.com/token".to_string(),
1071            userinfo_endpoint: "https://example.com/userinfo".to_string(),
1072            jwks_uri: "https://example.com/jwks".to_string(),
1073            response_types_supported: vec!["code".to_string()],
1074            subject_types_supported: vec!["public".to_string()],
1075            id_token_signing_alg_values_supported: vec!["RS256".to_string()],
1076            scopes_supported: vec!["openid".to_string()],
1077            claims_supported: vec!["sub".to_string()],
1078            grant_types_supported: vec!["authorization_code".to_string()],
1079        };
1080
1081        let serialized = serde_json::to_value(&doc).unwrap();
1082        assert_eq!(serialized["issuer"], "https://example.com");
1083        assert_eq!(serialized["jwks_uri"], "https://example.com/jwks");
1084    }
1085
1086    #[test]
1087    fn test_jwks_response_serialization() {
1088        let response = JwksResponse {
1089            keys: vec![JwkPublicKey {
1090                kid: "key1".to_string(),
1091                kty: "RSA".to_string(),
1092                alg: "RS256".to_string(),
1093                use_: "sig".to_string(),
1094                n: Some("modulus".to_string()),
1095                e: Some("exponent".to_string()),
1096                crv: None,
1097                x: None,
1098                y: None,
1099            }],
1100        };
1101
1102        let serialized = serde_json::to_value(&response).unwrap();
1103        assert_eq!(serialized["keys"][0]["kid"], "key1");
1104        assert_eq!(serialized["keys"][0]["kty"], "RSA");
1105        assert_eq!(serialized["keys"][0]["use"], "sig");
1106    }
1107
1108    #[test]
1109    fn test_jwk_public_key_rsa() {
1110        let key = JwkPublicKey {
1111            kid: "rsa-key".to_string(),
1112            kty: "RSA".to_string(),
1113            alg: "RS256".to_string(),
1114            use_: "sig".to_string(),
1115            n: Some("modulus-data".to_string()),
1116            e: Some("exponent-data".to_string()),
1117            crv: None,
1118            x: None,
1119            y: None,
1120        };
1121
1122        let serialized = serde_json::to_value(&key).unwrap();
1123        assert_eq!(serialized["kty"], "RSA");
1124        assert_eq!(serialized["n"], "modulus-data");
1125        assert_eq!(serialized["e"], "exponent-data");
1126        // EC fields should not be present
1127        assert!(serialized.get("crv").is_none());
1128        assert!(serialized.get("x").is_none());
1129        assert!(serialized.get("y").is_none());
1130    }
1131
1132    #[test]
1133    fn test_jwk_public_key_ec() {
1134        let key = JwkPublicKey {
1135            kid: "ec-key".to_string(),
1136            kty: "EC".to_string(),
1137            alg: "ES256".to_string(),
1138            use_: "sig".to_string(),
1139            n: None,
1140            e: None,
1141            crv: Some("P-256".to_string()),
1142            x: Some("x-coordinate".to_string()),
1143            y: Some("y-coordinate".to_string()),
1144        };
1145
1146        let serialized = serde_json::to_value(&key).unwrap();
1147        assert_eq!(serialized["kty"], "EC");
1148        assert_eq!(serialized["crv"], "P-256");
1149        assert_eq!(serialized["x"], "x-coordinate");
1150        assert_eq!(serialized["y"], "y-coordinate");
1151        // RSA fields should not be present
1152        assert!(serialized.get("n").is_none());
1153        assert!(serialized.get("e").is_none());
1154    }
1155}