1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct OidcConfig {
19 pub enabled: bool,
21 pub issuer: String,
23 pub jwks: JwksConfig,
25 pub claims: ClaimsConfig,
27 pub multi_tenant: Option<MultiTenantConfig>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct JwksConfig {
34 pub keys: Vec<JwkKey>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct JwkKey {
41 pub kid: String,
43 pub alg: String,
45 pub public_key: String,
47 #[serde(skip_serializing)]
49 pub private_key: Option<String>,
50 pub kty: String,
52 #[serde(default = "default_key_use")]
54 pub use_: String,
55}
56
57fn default_key_use() -> String {
58 "sig".to_string()
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ClaimsConfig {
64 pub default: Vec<String>,
66 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct MultiTenantConfig {
83 pub enabled: bool,
85 pub org_id_claim: String,
87 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#[derive(Debug, Serialize)]
108pub struct OidcDiscoveryDocument {
109 pub issuer: String,
111 pub authorization_endpoint: String,
113 pub token_endpoint: String,
115 pub userinfo_endpoint: String,
117 pub jwks_uri: String,
119 pub response_types_supported: Vec<String>,
121 pub subject_types_supported: Vec<String>,
123 pub id_token_signing_alg_values_supported: Vec<String>,
125 pub scopes_supported: Vec<String>,
127 pub claims_supported: Vec<String>,
129 pub grant_types_supported: Vec<String>,
131}
132
133#[derive(Debug, Serialize)]
135pub struct JwksResponse {
136 pub keys: Vec<JwkPublicKey>,
138}
139
140#[derive(Debug, Serialize)]
142pub struct JwkPublicKey {
143 pub kid: String,
145 pub kty: String,
147 pub alg: String,
149 #[serde(rename = "use")]
151 pub use_: String,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub n: Option<String>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub e: Option<String>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub crv: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub x: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub y: Option<String>,
167}
168
169#[derive(Clone)]
171pub struct OidcState {
172 pub config: OidcConfig,
174 pub signing_keys: Arc<RwLock<HashMap<String, EncodingKey>>>,
176}
177
178impl OidcState {
179 pub fn new(config: OidcConfig) -> Result<Self, Error> {
181 let mut signing_keys = HashMap::new();
182
183 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 pub fn default_mock() -> Result<Self, Error> {
216 use std::env;
217
218 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 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
254pub fn load_oidc_state() -> Option<OidcState> {
263 use std::env;
264
265 if let Ok(disabled) = env::var("MOCKFORGE_OIDC_ENABLED") {
267 if disabled == "false" || disabled == "0" {
268 return None;
269 }
270 }
271
272 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 OidcState::default_mock().ok()
285}
286
287pub async fn get_oidc_discovery() -> Json<OidcDiscoveryDocument> {
289 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
343pub async fn get_jwks() -> Json<JwksResponse> {
345 let jwks = JwksResponse { keys: vec![] };
348
349 Json(jwks)
350}
351
352pub 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 }
365 }
366 }
367
368 Ok(JwksResponse { keys: public_keys })
369}
370
371pub 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 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#[derive(Debug, Clone)]
422pub struct TenantContext {
423 pub org_id: Option<String>,
425 pub tenant_id: Option<String>,
427}
428
429pub 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 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 for claim_name in &oidc_state.config.claims.default {
447 if !claims.contains_key(claim_name) {
448 match claim_name.as_str() {
450 "sub" | "iss" => {} "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 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 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 if let Some(ref mt_config) = oidc_state.config.multi_tenant {
478 if mt_config.enabled {
479 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 if let Some(additional) = additional_claims {
500 for (key, value) in additional {
501 claims.insert(key, value);
502 }
503 }
504
505 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 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
544pub 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 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 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 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 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 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 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 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 assert!(serialized.get("n").is_none());
1093 assert!(serialized.get("e").is_none());
1094 }
1095}