1use crate::errors::{AuthError, Result};
25use crate::security::secure_jwt::SecureJwtValidator;
26use crate::server::{DpopManager, MutualTlsManager, PARManager, PrivateKeyJwtManager};
27use chrono::{DateTime, Duration, Utc};
28use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
29use serde::{Deserialize, Serialize};
30use serde_json::{Value, json};
31use std::collections::HashMap;
32use std::sync::Arc;
33use tokio::sync::RwLock;
34use uuid::Uuid;
35
36#[derive(Debug, Clone)]
38pub struct FapiManager {
39 dpop_manager: Arc<DpopManager>,
41
42 mtls_manager: Arc<MutualTlsManager>,
44
45 par_manager: Arc<PARManager>,
47
48 private_key_jwt_manager: Arc<PrivateKeyJwtManager>,
50
51 jwt_validator: Arc<SecureJwtValidator>,
53
54 config: FapiConfig,
56
57 sessions: Arc<RwLock<HashMap<String, FapiSession>>>,
59}
60
61#[derive(Clone)]
63pub struct FapiConfig {
64 pub issuer: String,
66
67 pub request_signing_algorithm: Algorithm,
69
70 pub response_signing_algorithm: Algorithm,
72
73 pub private_key: EncodingKey,
75
76 pub public_key: DecodingKey,
78
79 pub max_request_age: i64,
81
82 pub require_dpop: bool,
84
85 pub require_mtls: bool,
87
88 pub require_par: bool,
90
91 pub enable_jarm: bool,
93
94 pub enhanced_audit: bool,
96}
97
98impl std::fmt::Debug for FapiConfig {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("FapiConfig")
101 .field("issuer", &self.issuer)
102 .field("request_signing_algorithm", &self.request_signing_algorithm)
103 .field(
104 "response_signing_algorithm",
105 &self.response_signing_algorithm,
106 )
107 .field("private_key", &"<EncodingKey>")
108 .field("public_key", &"<DecodingKey>")
109 .field("max_request_age", &self.max_request_age)
110 .field("require_dpop", &self.require_dpop)
111 .field("require_mtls", &self.require_mtls)
112 .field("require_par", &self.require_par)
113 .field("enable_jarm", &self.enable_jarm)
114 .field("enhanced_audit", &self.enhanced_audit)
115 .finish()
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct FapiSession {
122 pub session_id: String,
124
125 pub client_id: String,
127
128 pub user_id: String,
130
131 pub created_at: DateTime<Utc>,
133
134 pub expires_at: DateTime<Utc>,
136
137 pub dpop_proof: Option<String>,
139
140 pub cert_thumbprint: Option<String>,
142
143 pub request_jti: Option<String>,
145
146 pub scopes: Vec<String>,
148
149 pub metadata: HashMap<String, Value>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct FapiRequestObject {
156 pub iss: String,
158
159 pub aud: String,
161
162 pub iat: i64,
164
165 pub exp: i64,
167
168 pub nbf: Option<i64>,
170
171 pub jti: String,
173
174 pub response_type: String,
176
177 pub client_id: String,
179
180 pub redirect_uri: String,
182
183 pub scope: String,
185
186 pub state: Option<String>,
188
189 pub nonce: Option<String>,
191
192 pub code_challenge: Option<String>,
194
195 pub code_challenge_method: Option<String>,
197
198 #[serde(flatten)]
200 pub additional_claims: HashMap<String, Value>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct FapiAuthorizationResponse {
206 pub iss: String,
208
209 pub aud: String,
211
212 pub iat: i64,
214
215 pub exp: i64,
217
218 pub code: Option<String>,
220
221 pub state: Option<String>,
223
224 pub error: Option<String>,
226
227 pub error_description: Option<String>,
229
230 #[serde(flatten)]
232 pub additional_params: HashMap<String, Value>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct FapiTokenResponse {
238 pub access_token: String,
240
241 pub token_type: String,
243
244 pub expires_in: i64,
246
247 pub refresh_token: Option<String>,
249
250 pub scope: Option<String>,
252
253 pub id_token: Option<String>,
255
256 pub cnf: Option<Value>,
258}
259
260impl FapiManager {
261 pub fn new(
263 config: FapiConfig,
264 dpop_manager: Arc<DpopManager>,
265 mtls_manager: Arc<MutualTlsManager>,
266 par_manager: Arc<PARManager>,
267 private_key_jwt_manager: Arc<PrivateKeyJwtManager>,
268 jwt_validator: Arc<SecureJwtValidator>,
269 ) -> Self {
270 Self {
271 dpop_manager,
272 mtls_manager,
273 par_manager,
274 private_key_jwt_manager,
275 jwt_validator,
276 config,
277 sessions: Arc::new(RwLock::new(HashMap::new())),
278 }
279 }
280
281 pub async fn validate_authorization_request(
283 &self,
284 request_object: &str,
285 client_cert: Option<&str>,
286 dpop_proof: Option<&str>,
287 request_uri: Option<&str>,
288 ) -> Result<FapiRequestObject> {
289 let claims = if let Some(uri) = request_uri {
290 if self.config.require_par {
292 let par_request = self.par_manager.consume_request(uri).await.map_err(|e| {
294 AuthError::InvalidRequest(format!("PAR request validation failed: {}", e))
295 })?;
296
297 tracing::info!(
298 "FAPI PAR request consumed successfully for client: {}",
299 par_request.client_id
300 );
301
302 FapiRequestObject {
305 iss: par_request.client_id.clone(),
306 aud: self.config.issuer.clone(),
307 iat: Utc::now().timestamp(),
308 exp: Utc::now().timestamp() + 300, nbf: Some(Utc::now().timestamp()),
310 jti: uuid::Uuid::new_v4().to_string(),
311 response_type: par_request.response_type,
312 client_id: par_request.client_id,
313 redirect_uri: par_request.redirect_uri,
314 scope: par_request.scope.unwrap_or_default(),
315 state: par_request.state,
316 nonce: None, code_challenge: par_request.code_challenge,
318 code_challenge_method: par_request.code_challenge_method,
319 additional_claims: par_request
320 .additional_params
321 .into_iter()
322 .map(|(k, v)| (k, serde_json::Value::String(v)))
323 .collect(),
324 }
325 } else {
326 return Err(AuthError::InvalidRequest(
327 "request_uri provided but PAR not required".to_string(),
328 ));
329 }
330 } else {
331 if self.config.require_par {
333 return Err(AuthError::InvalidRequest(
334 "PAR is required but no request_uri provided".to_string(),
335 ));
336 }
337
338 self.validate_request_object(request_object).await?
340 };
341
342 if self.config.require_mtls {
344 if client_cert.is_none() {
345 return Err(AuthError::auth_method(
346 "mtls",
347 "mTLS certificate required for FAPI 2.0",
348 ));
349 }
350
351 let cert = client_cert.unwrap();
352 let cert_bytes = cert.as_bytes(); self.mtls_manager
354 .validate_client_certificate(cert_bytes, &claims.client_id)
355 .await?;
356 }
357
358 if self.config.require_dpop {
360 if dpop_proof.is_none() {
361 return Err(AuthError::auth_method(
362 "dpop",
363 "DPoP proof required for FAPI 2.0",
364 ));
365 }
366
367 let proof = dpop_proof.unwrap();
368 self.dpop_manager
369 .validate_dpop_proof(
370 proof,
371 "POST",
372 &format!("{}/authorize", self.config.issuer),
373 None,
374 None,
375 )
376 .await?;
377 }
378
379 self.validate_request_claims(&claims).await?;
381
382 Ok(claims)
383 }
384
385 async fn validate_request_object(&self, request_object: &str) -> Result<FapiRequestObject> {
387 let decoding_key = &self.config.public_key;
389
390 match self
392 .jwt_validator
393 .validate_token(request_object, decoding_key, true)
394 {
395 Ok(secure_claims) => {
396 let header = jsonwebtoken::decode_header(request_object).map_err(|e| {
398 AuthError::InvalidToken(format!("Invalid request object header: {}", e))
399 })?;
400
401 if !matches!(
403 header.alg,
404 Algorithm::RS256 | Algorithm::PS256 | Algorithm::ES256
405 ) {
406 return Err(AuthError::InvalidToken(
407 "Request object must use RS256, PS256, or ES256".to_string(),
408 ));
409 }
410
411 let mut validation = Validation::new(header.alg);
413 validation.set_audience(&[&self.config.issuer]);
414 validation.validate_exp = true;
415 validation.validate_nbf = true;
416
417 let token_data = jsonwebtoken::decode::<FapiRequestObject>(
419 request_object,
420 &self.config.public_key,
421 &validation,
422 )
423 .map_err(|e| {
424 AuthError::InvalidToken(format!("Request object validation failed: {}", e))
425 })?;
426
427 let fapi_claims = token_data.claims;
428
429 if secure_claims.sub != fapi_claims.client_id {
432 return Err(AuthError::InvalidToken(
433 "Subject mismatch between secure validation and FAPI claims".to_string(),
434 ));
435 }
436
437 if secure_claims.iss != fapi_claims.iss {
439 return Err(AuthError::InvalidToken(
440 "Issuer mismatch between secure validation and FAPI claims".to_string(),
441 ));
442 }
443
444 if secure_claims.exp != fapi_claims.exp {
446 return Err(AuthError::InvalidToken(
447 "Expiry mismatch between secure validation and FAPI claims".to_string(),
448 ));
449 }
450
451 let now = Utc::now().timestamp();
453
454 if now - fapi_claims.iat > self.config.max_request_age {
456 return Err(AuthError::InvalidToken(
457 "Request object too old".to_string(),
458 ));
459 }
460
461 if fapi_claims.client_id.is_empty() {
463 return Err(AuthError::InvalidToken(
464 "client_id required in request object".to_string(),
465 ));
466 }
467
468 if fapi_claims.redirect_uri.is_empty() {
469 return Err(AuthError::InvalidToken(
470 "redirect_uri required in request object".to_string(),
471 ));
472 }
473
474 if fapi_claims.response_type.is_empty() {
475 return Err(AuthError::InvalidToken(
476 "response_type required in request object".to_string(),
477 ));
478 }
479
480 tracing::info!(
481 "FAPI request object validated successfully with SecureJwtValidator for client: {}",
482 fapi_claims.client_id
483 );
484
485 Ok(fapi_claims)
486 }
487 Err(e) => {
488 tracing::error!("SecureJwtValidator failed for FAPI request object: {}", e);
489 Err(AuthError::InvalidToken(format!(
490 "Enhanced JWT validation failed: {}",
491 e
492 )))
493 }
494 }
495 }
496
497 async fn validate_request_claims(&self, claims: &FapiRequestObject) -> Result<()> {
499 if !matches!(claims.response_type.as_str(), "code" | "code id_token") {
501 return Err(AuthError::InvalidRequest(
502 "FAPI 2.0 requires code or code id_token response type".to_string(),
503 ));
504 }
505
506 if claims.code_challenge.is_none() {
508 return Err(AuthError::InvalidRequest(
509 "PKCE required for FAPI 2.0".to_string(),
510 ));
511 }
512
513 if let Some(method) = &claims.code_challenge_method {
514 if method != "S256" {
515 return Err(AuthError::InvalidRequest(
516 "FAPI 2.0 requires S256 code challenge method".to_string(),
517 ));
518 }
519 } else {
520 return Err(AuthError::InvalidRequest(
521 "code_challenge_method required for FAPI 2.0".to_string(),
522 ));
523 }
524
525 Ok(())
526 }
527
528 pub async fn authenticate_client_jwt(&self, client_assertion: &str) -> Result<String> {
530 let auth_result = self
532 .private_key_jwt_manager
533 .authenticate_client(client_assertion)
534 .await
535 .map_err(|e| {
536 AuthError::auth_method(
537 "private_key_jwt",
538 format!("Private key JWT authentication failed: {}", e),
539 )
540 })?;
541
542 match auth_result.authenticated {
543 true => {
544 tracing::info!(
545 "FAPI client authenticated successfully using private key JWT: {}",
546 auth_result.client_id
547 );
548 Ok(auth_result.client_id)
549 }
550 false => {
551 let error_msg = auth_result.errors.join("; ");
552 tracing::error!("FAPI private key JWT authentication failed: {}", error_msg);
553 Err(AuthError::auth_method(
554 "private_key_jwt",
555 format!("Authentication failed: {}", error_msg),
556 ))
557 }
558 }
559 }
560
561 pub async fn validate_token_request(
563 &self,
564 client_assertion: Option<&str>,
565 client_cert: Option<&str>,
566 dpop_proof: Option<&str>,
567 authorization_code: &str,
568 ) -> Result<String> {
569 let client_id = if let Some(assertion) = client_assertion {
571 self.authenticate_client_jwt(assertion).await?
572 } else if self.config.require_mtls {
573 if let Some(cert) = client_cert {
574 let cert_bytes = cert.as_bytes();
575
576 let client_id = self.extract_client_id_from_certificate(cert_bytes).await?;
578
579 self.mtls_manager
580 .validate_client_certificate(cert_bytes, &client_id)
581 .await?;
582
583 client_id.to_string()
584 } else {
585 return Err(AuthError::auth_method(
586 "mtls",
587 "Client certificate required for FAPI 2.0 token request",
588 ));
589 }
590 } else {
591 return Err(AuthError::auth_method(
592 "fapi",
593 "FAPI 2.0 requires either private_key_jwt or mTLS client authentication",
594 ));
595 };
596
597 if self.config.require_dpop {
599 if dpop_proof.is_none() {
600 return Err(AuthError::auth_method(
601 "dpop",
602 "DPoP proof required for FAPI 2.0 token request",
603 ));
604 }
605
606 let proof = dpop_proof.unwrap();
607 self.dpop_manager
608 .validate_dpop_proof(
609 proof,
610 "POST",
611 &format!("{}/token", self.config.issuer),
612 Some(authorization_code), None,
614 )
615 .await?;
616 }
617
618 Ok(client_id)
619 }
620
621 pub async fn generate_authorization_response(
623 &self,
624 client_id: &str,
625 code: Option<&str>,
626 state: Option<&str>,
627 error: Option<&str>,
628 error_description: Option<&str>,
629 ) -> Result<String> {
630 if !self.config.enable_jarm {
631 return Err(AuthError::Configuration {
632 message: "JARM not enabled".to_string(),
633 help: Some("Enable JARM in your configuration to use this feature".to_string()),
634 docs_url: Some("https://docs.auth-framework.com/fapi#jarm".to_string()),
635 source: None,
636 suggested_fix: Some("Set enable_jarm to true in your FAPIConfig".to_string()),
637 });
638 }
639
640 let now = Utc::now();
641 let exp = now + Duration::minutes(5); let response = FapiAuthorizationResponse {
644 iss: self.config.issuer.clone(),
645 aud: client_id.to_string(),
646 iat: now.timestamp(),
647 exp: exp.timestamp(),
648 code: code.map(|c| c.to_string()),
649 state: state.map(|s| s.to_string()),
650 error: error.map(|e| e.to_string()),
651 error_description: error_description.map(|d| d.to_string()),
652 additional_params: HashMap::new(),
653 };
654
655 let header = Header::new(self.config.response_signing_algorithm);
657
658 let token =
660 jsonwebtoken::encode(&header, &response, &self.config.private_key).map_err(|e| {
661 AuthError::TokenGeneration(format!("Failed to sign JARM response: {}", e))
662 })?;
663
664 Ok(token)
665 }
666
667 pub async fn generate_token_response(
669 &self,
670 client_id: &str,
671 user_id: &str,
672 scopes: Vec<String>,
673 cert_thumbprint: Option<String>,
674 dpop_jkt: Option<String>,
675 ) -> Result<FapiTokenResponse> {
676 let access_token = self
678 .generate_access_token(client_id, user_id, &scopes, &cert_thumbprint, &dpop_jkt)
679 .await?;
680
681 let refresh_token = self.generate_refresh_token(client_id, user_id).await?;
683
684 let mut cnf = json!({});
686
687 if let Some(thumbprint) = cert_thumbprint {
688 cnf["x5t#S256"] = Value::String(thumbprint);
689 }
690
691 if let Some(jkt) = dpop_jkt {
692 cnf["jkt"] = Value::String(jkt);
693 }
694
695 let response = FapiTokenResponse {
696 access_token,
697 token_type: "DPoP".to_string(), expires_in: 3600, refresh_token: Some(refresh_token),
700 scope: Some(scopes.join(" ")),
701 id_token: None, cnf: if cnf.as_object().unwrap().is_empty() {
703 None
704 } else {
705 Some(cnf)
706 },
707 };
708
709 Ok(response)
710 }
711
712 async fn generate_access_token(
714 &self,
715 client_id: &str,
716 user_id: &str,
717 scopes: &[String],
718 cert_thumbprint: &Option<String>,
719 dpop_jkt: &Option<String>,
720 ) -> Result<String> {
721 let now = Utc::now();
722 let exp = now + Duration::hours(1);
723
724 let mut claims = json!({
725 "iss": self.config.issuer,
726 "aud": client_id,
727 "sub": user_id,
728 "iat": now.timestamp(),
729 "exp": exp.timestamp(),
730 "scope": scopes.join(" "),
731 "jti": Uuid::new_v4().to_string(),
732 });
733
734 let mut cnf = json!({});
736
737 if let Some(thumbprint) = cert_thumbprint {
738 cnf["x5t#S256"] = Value::String(thumbprint.clone());
739 }
740
741 if let Some(jkt) = dpop_jkt {
742 cnf["jkt"] = Value::String(jkt.clone());
743 }
744
745 if !cnf.as_object().unwrap().is_empty() {
746 claims["cnf"] = cnf;
747 }
748
749 let header = Header::new(Algorithm::RS256);
751
752 let token =
754 jsonwebtoken::encode(&header, &claims, &self.config.private_key).map_err(|e| {
755 AuthError::TokenGeneration(format!("Failed to generate access token: {}", e))
756 })?;
757
758 Ok(token)
759 }
760
761 async fn generate_refresh_token(&self, client_id: &str, user_id: &str) -> Result<String> {
763 let now = Utc::now();
764 let exp = now + Duration::days(30); let claims = json!({
767 "iss": self.config.issuer,
768 "aud": client_id,
769 "sub": user_id,
770 "iat": now.timestamp(),
771 "exp": exp.timestamp(),
772 "typ": "refresh_token",
773 "jti": Uuid::new_v4().to_string(),
774 });
775
776 let header = Header::new(Algorithm::RS256);
778
779 let token =
781 jsonwebtoken::encode(&header, &claims, &self.config.private_key).map_err(|e| {
782 AuthError::TokenGeneration(format!("Failed to generate refresh token: {}", e))
783 })?;
784
785 Ok(token)
786 }
787
788 pub async fn create_session(
790 &self,
791 client_id: &str,
792 user_id: &str,
793 scopes: Vec<String>,
794 dpop_proof: Option<String>,
795 cert_thumbprint: Option<String>,
796 request_jti: Option<String>,
797 ) -> Result<String> {
798 let session_id = Uuid::new_v4().to_string();
799 let now = Utc::now();
800 let expires_at = now + Duration::hours(24); let session = FapiSession {
803 session_id: session_id.clone(),
804 client_id: client_id.to_string(),
805 user_id: user_id.to_string(),
806 created_at: now,
807 expires_at,
808 dpop_proof,
809 cert_thumbprint,
810 request_jti,
811 scopes,
812 metadata: HashMap::new(),
813 };
814
815 let mut sessions = self.sessions.write().await;
816 sessions.insert(session_id.clone(), session);
817
818 Ok(session_id)
819 }
820
821 pub async fn get_session(&self, session_id: &str) -> Result<Option<FapiSession>> {
823 let sessions = self.sessions.read().await;
824 Ok(sessions.get(session_id).cloned())
825 }
826
827 pub async fn validate_session(&self, session_id: &str) -> Result<FapiSession> {
829 let session = self
830 .get_session(session_id)
831 .await?
832 .ok_or_else(|| AuthError::validation("Session not found".to_string()))?;
833
834 if Utc::now() > session.expires_at {
836 return Err(AuthError::validation("Session expired".to_string()));
837 }
838
839 Ok(session)
840 }
841
842 pub async fn remove_session(&self, session_id: &str) -> Result<()> {
844 let mut sessions = self.sessions.write().await;
845 sessions.remove(session_id);
846 Ok(())
847 }
848
849 pub async fn audit_log(&self, event: &str, details: &Value) -> Result<()> {
851 if self.config.enhanced_audit {
852 let timestamp = chrono::Utc::now().to_rfc3339();
856 let audit_entry = format!("[{}] FAPI AUDIT: {} - {}", timestamp, event, details);
857
858 log::info!("{}", audit_entry);
860 }
861 Ok(())
862 }
863
864 async fn extract_client_id_from_certificate(&self, cert_bytes: &[u8]) -> Result<String> {
866 let cert_str = String::from_utf8_lossy(cert_bytes);
871
872 if let Some(cn_start) = cert_str.find("CN=") {
874 let cn_section = &cert_str[cn_start + 3..];
875 if let Some(cn_end) = cn_section.find(',').or_else(|| cn_section.find('\n')) {
876 let client_id = cn_section[..cn_end].trim().to_string();
877 if !client_id.is_empty() {
878 tracing::info!("Extracted client ID from certificate CN: {}", client_id);
879 return Ok(client_id);
880 }
881 }
882 }
883
884 if let Some(san_start) = cert_str.find("DNS:") {
886 let san_section = &cert_str[san_start + 4..];
887 if let Some(san_end) = san_section.find(',').or_else(|| san_section.find('\n')) {
888 let client_id = san_section[..san_end].trim().to_string();
889 if !client_id.is_empty() && client_id.contains("client") {
890 tracing::info!("Extracted client ID from certificate SAN: {}", client_id);
891 return Ok(client_id);
892 }
893 }
894 }
895
896 use std::hash::{Hash, Hasher};
898 let mut hasher = std::collections::hash_map::DefaultHasher::new();
899 cert_bytes.hash(&mut hasher);
900 let cert_hash = format!("cert_client_{:x}", hasher.finish());
901
902 tracing::info!(
903 "Generated hash-based client ID from certificate: {}",
904 cert_hash
905 );
906 Ok(cert_hash)
907 }
908}
909
910impl Default for FapiConfig {
911 fn default() -> Self {
912 let issuer =
914 std::env::var("FAPI_ISSUER").unwrap_or_else(|_| "https://auth.example.com".to_string());
915
916 let private_key = if let Ok(key_path) = std::env::var("FAPI_PRIVATE_KEY_PATH") {
918 std::fs::read(&key_path)
919 .map_err(|e| tracing::warn!("Failed to load private key from {}: {}", key_path, e))
920 .and_then(|bytes| {
921 EncodingKey::from_rsa_pem(&bytes)
922 .map_err(|e| tracing::warn!("Invalid RSA key format: {}", e))
923 })
924 .unwrap_or_else(|_| EncodingKey::from_secret(b"dev_fallback_secret"))
925 } else {
926 EncodingKey::from_secret(b"dev_fallback_secret")
927 };
928
929 Self {
930 issuer,
931 request_signing_algorithm: Algorithm::RS256,
932 response_signing_algorithm: Algorithm::RS256,
933 private_key,
934 public_key: DecodingKey::from_secret(b"dev_secret"),
935 max_request_age: 300, require_dpop: true,
937 require_mtls: true,
938 require_par: true,
939 enable_jarm: true,
940 enhanced_audit: true,
941 }
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948
949 #[tokio::test]
950 async fn test_fapi_manager_creation() {
951 let config = FapiConfig::default();
953
954 assert_eq!(config.issuer, "https://auth.example.com"); assert!(config.require_dpop);
957 assert!(config.require_mtls);
958 assert!(config.require_par);
959 assert!(config.enable_jarm);
960 assert!(config.enhanced_audit);
961 }
962
963 #[tokio::test]
964 async fn test_fapi_request_validation() {
965 let config = FapiConfig::default();
968
969 let request_object = r#"{"iss":"client_id","aud":"https://example.com","exp":9999999999,"nbf":1000000000,"iat":1000000000,"jti":"unique_id"}"#;
971
972 let validation_result = validate_fapi_request_object(request_object, &config);
974 assert!(
975 validation_result.is_ok(),
976 "FAPI request object validation failed"
977 );
978
979 assert!(!request_object.is_empty());
981 }
982
983 fn validate_fapi_request_object(
985 request_object: &str,
986 _config: &FapiConfig,
987 ) -> Result<(), String> {
988 let parsed: serde_json::Value = serde_json::from_str(request_object)
990 .map_err(|_| "Invalid JSON structure in request object")?;
991
992 let required_claims = ["iss", "aud", "exp", "iat", "jti"];
994 for claim in &required_claims {
995 if parsed.get(claim).is_none() {
996 return Err(format!("Missing required FAPI claim: {}", claim));
997 }
998 }
999
1000 if let Some(exp) = parsed.get("exp").and_then(|v| v.as_i64()) {
1002 let now = chrono::Utc::now().timestamp();
1003 if exp <= now {
1004 return Err("Request object has expired".to_string());
1005 }
1006 }
1007
1008 Ok(())
1011 }
1012
1013 #[tokio::test]
1014 async fn test_fapi_response_generation() {
1015 let config = FapiConfig::default();
1018
1019 let auth_response = serde_json::json!({
1021 "code": "auth_code_123",
1022 "state": "client_state",
1023 "iss": config.issuer,
1024 "aud": "client_id",
1025 "exp": 9999999999i64
1026 });
1027
1028 assert!(auth_response["code"].is_string());
1030 }
1031
1032 #[tokio::test]
1033 async fn test_fapi_token_generation() {
1034 let config = FapiConfig::default();
1036
1037 let scopes = ["accounts".to_string(), "payments".to_string()];
1039 let client_id = "fapi_client_123";
1040 let user_id = "user_456";
1041 let cert_thumbprint = Some("sha256_cert_thumbprint".to_string());
1042
1043 assert!(config.require_dpop);
1045 assert!(config.require_mtls);
1046 assert!(!scopes.is_empty());
1047 assert!(!client_id.is_empty());
1048 assert!(!user_id.is_empty());
1049 assert!(cert_thumbprint.is_some());
1050 }
1051
1052 #[tokio::test]
1053 async fn test_fapi_session_management() {
1054 let config = FapiConfig::default();
1056
1057 let session_data = serde_json::json!({
1059 "client_id": "fapi_client",
1060 "user_id": "fapi_user",
1061 "scopes": ["accounts", "payments"],
1062 "mtls_cert": "client_certificate",
1063 "dpop_key": "client_dpop_key"
1064 });
1065
1066 assert!(session_data["mtls_cert"].is_string());
1068 assert!(session_data["dpop_key"].is_string());
1069 assert!(config.enhanced_audit);
1070 }
1071}