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::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 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::internal(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::internal("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 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 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 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 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 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 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 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 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 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 assert!(serialized.get("n").is_none());
1153 assert!(serialized.get("e").is_none());
1154 }
1155}