1use crate::errors::{AuthError, Result};
28use crate::security::secure_jwt::SecureJwtValidator;
29use crate::server::{DpopManager, MutualTlsManager, PARManager, PrivateKeyJwtManager};
30use chrono::{DateTime, Duration, Utc};
31use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
32use serde::{Deserialize, Serialize};
33use serde_json::{Value, json};
34use std::collections::HashMap;
35use std::sync::Arc;
36use tokio::sync::RwLock;
37use uuid::Uuid;
38
39#[derive(Debug, Clone)]
41pub struct FapiManager {
42 dpop_manager: Arc<DpopManager>,
44
45 mtls_manager: Arc<MutualTlsManager>,
47
48 par_manager: Arc<PARManager>,
50
51 private_key_jwt_manager: Arc<PrivateKeyJwtManager>,
53
54 jwt_validator: Arc<SecureJwtValidator>,
56
57 config: FapiConfig,
59
60 sessions: Arc<RwLock<HashMap<String, FapiSession>>>,
62}
63
64#[derive(Clone)]
66pub struct FapiConfig {
67 pub issuer: String,
69
70 pub request_signing_algorithm: Algorithm,
72
73 pub response_signing_algorithm: Algorithm,
75
76 pub private_key: EncodingKey,
78
79 pub public_key: DecodingKey,
81
82 pub max_request_age: i64,
84
85 pub require_dpop: bool,
87
88 pub require_mtls: bool,
90
91 pub require_par: bool,
93
94 pub enable_jarm: bool,
96
97 pub enhanced_audit: bool,
99
100 pub is_degraded: bool,
104}
105
106impl std::fmt::Debug for FapiConfig {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.debug_struct("FapiConfig")
109 .field("issuer", &self.issuer)
110 .field("request_signing_algorithm", &self.request_signing_algorithm)
111 .field(
112 "response_signing_algorithm",
113 &self.response_signing_algorithm,
114 )
115 .field("private_key", &"<EncodingKey>")
116 .field("public_key", &"<DecodingKey>")
117 .field("max_request_age", &self.max_request_age)
118 .field("require_dpop", &self.require_dpop)
119 .field("require_mtls", &self.require_mtls)
120 .field("require_par", &self.require_par)
121 .field("enable_jarm", &self.enable_jarm)
122 .field("enhanced_audit", &self.enhanced_audit)
123 .field("is_degraded", &self.is_degraded)
124 .finish()
125 }
126}
127
128impl FapiConfig {
129 pub fn builder(
133 issuer: impl Into<String>,
134 private_key: EncodingKey,
135 public_key: DecodingKey,
136 ) -> FapiConfigBuilder {
137 FapiConfigBuilder {
138 issuer: issuer.into(),
139 request_signing_algorithm: Algorithm::PS256,
140 response_signing_algorithm: Algorithm::PS256,
141 private_key,
142 public_key,
143 max_request_age: 60,
144 require_dpop: true,
145 require_mtls: true,
146 require_par: true,
147 enable_jarm: true,
148 enhanced_audit: true,
149 is_degraded: false,
150 }
151 }
152
153 #[deprecated(since = "0.5.0", note = "use FapiConfig::from_env() instead")]
155 pub fn load_from_env() -> Self {
156 Self::from_env()
157 }
158
159 pub fn from_env() -> Self {
163 Self::default()
164 }
165}
166
167pub struct FapiConfigBuilder {
169 issuer: String,
170 request_signing_algorithm: Algorithm,
171 response_signing_algorithm: Algorithm,
172 private_key: EncodingKey,
173 public_key: DecodingKey,
174 max_request_age: i64,
175 require_dpop: bool,
176 require_mtls: bool,
177 require_par: bool,
178 enable_jarm: bool,
179 enhanced_audit: bool,
180 is_degraded: bool,
181}
182
183impl FapiConfigBuilder {
184 pub fn request_signing_algorithm(mut self, alg: Algorithm) -> Self {
188 self.request_signing_algorithm = alg;
189 self
190 }
191
192 pub fn response_signing_algorithm(mut self, alg: Algorithm) -> Self {
194 self.response_signing_algorithm = alg;
195 self
196 }
197
198 pub fn max_request_age(mut self, age: i64) -> Self {
200 self.max_request_age = age;
201 self
202 }
203
204 pub fn require_dpop(mut self, require: bool) -> Self {
206 self.require_dpop = require;
207 self
208 }
209
210 pub fn require_mtls(mut self, require: bool) -> Self {
212 self.require_mtls = require;
213 self
214 }
215
216 pub fn require_par(mut self, require: bool) -> Self {
218 self.require_par = require;
219 self
220 }
221
222 pub fn enable_jarm(mut self, enable: bool) -> Self {
224 self.enable_jarm = enable;
225 self
226 }
227
228 pub fn enhanced_audit(mut self, enable: bool) -> Self {
230 self.enhanced_audit = enable;
231 self
232 }
233
234 pub fn degraded(mut self) -> Self {
236 self.is_degraded = true;
237 self
238 }
239
240 pub fn build(self) -> FapiConfig {
242 FapiConfig {
243 issuer: self.issuer,
244 request_signing_algorithm: self.request_signing_algorithm,
245 response_signing_algorithm: self.response_signing_algorithm,
246 private_key: self.private_key,
247 public_key: self.public_key,
248 max_request_age: self.max_request_age,
249 require_dpop: self.require_dpop,
250 require_mtls: self.require_mtls,
251 require_par: self.require_par,
252 enable_jarm: self.enable_jarm,
253 enhanced_audit: self.enhanced_audit,
254 is_degraded: self.is_degraded,
255 }
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct FapiSession {
262 pub session_id: String,
264
265 pub client_id: String,
267
268 pub user_id: String,
270
271 pub created_at: DateTime<Utc>,
273
274 pub expires_at: DateTime<Utc>,
276
277 pub dpop_proof: Option<String>,
279
280 pub cert_thumbprint: Option<String>,
282
283 pub request_jti: Option<String>,
285
286 pub scopes: Vec<String>,
288
289 pub metadata: HashMap<String, Value>,
291}
292
293impl FapiSession {
294 pub fn builder(
296 session_id: impl Into<String>,
297 client_id: impl Into<String>,
298 user_id: impl Into<String>,
299 expires_in: Duration,
300 ) -> FapiSessionBuilder {
301 let now = Utc::now();
302 FapiSessionBuilder {
303 session_id: session_id.into(),
304 client_id: client_id.into(),
305 user_id: user_id.into(),
306 created_at: now,
307 expires_at: now + expires_in,
308 dpop_proof: None,
309 cert_thumbprint: None,
310 request_jti: None,
311 scopes: Vec::new(),
312 metadata: HashMap::new(),
313 }
314 }
315}
316
317pub struct FapiSessionBuilder {
319 session_id: String,
320 client_id: String,
321 user_id: String,
322 created_at: DateTime<Utc>,
323 expires_at: DateTime<Utc>,
324 dpop_proof: Option<String>,
325 cert_thumbprint: Option<String>,
326 request_jti: Option<String>,
327 scopes: Vec<String>,
328 metadata: HashMap<String, Value>,
329}
330
331impl FapiSessionBuilder {
332 pub fn dpop_proof(mut self, proof: impl Into<String>) -> Self {
334 self.dpop_proof = Some(proof.into());
335 self
336 }
337
338 pub fn cert_thumbprint(mut self, thumbprint: impl Into<String>) -> Self {
340 self.cert_thumbprint = Some(thumbprint.into());
341 self
342 }
343
344 pub fn request_jti(mut self, jti: impl Into<String>) -> Self {
346 self.request_jti = Some(jti.into());
347 self
348 }
349
350 pub fn add_scope(mut self, scope: impl Into<String>) -> Self {
352 self.scopes.push(scope.into());
353 self
354 }
355
356 pub fn add_scopes<I, S>(mut self, scopes: I) -> Self
358 where
359 I: IntoIterator<Item = S>,
360 S: Into<String>,
361 {
362 self.scopes.extend(scopes.into_iter().map(Into::into));
363 self
364 }
365
366 pub fn add_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
368 self.metadata.insert(key.into(), value);
369 self
370 }
371
372 pub fn build(self) -> FapiSession {
374 FapiSession {
375 session_id: self.session_id,
376 client_id: self.client_id,
377 user_id: self.user_id,
378 created_at: self.created_at,
379 expires_at: self.expires_at,
380 dpop_proof: self.dpop_proof,
381 cert_thumbprint: self.cert_thumbprint,
382 request_jti: self.request_jti,
383 scopes: self.scopes,
384 metadata: self.metadata,
385 }
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct FapiRequestObject {
392 pub iss: String,
394
395 pub aud: String,
397
398 pub iat: i64,
400
401 pub exp: i64,
403
404 pub nbf: Option<i64>,
406
407 pub jti: String,
409
410 pub response_type: String,
412
413 pub client_id: String,
415
416 pub redirect_uri: String,
418
419 pub scope: String,
421
422 pub state: Option<String>,
424
425 pub nonce: Option<String>,
427
428 pub code_challenge: Option<String>,
430
431 pub code_challenge_method: Option<String>,
433
434 #[serde(flatten)]
436 pub additional_claims: HashMap<String, Value>,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct FapiAuthorizationResponse {
442 pub iss: String,
444
445 pub aud: String,
447
448 pub iat: i64,
450
451 pub exp: i64,
453
454 pub code: Option<String>,
456
457 pub state: Option<String>,
459
460 pub error: Option<String>,
462
463 pub error_description: Option<String>,
465
466 #[serde(flatten)]
468 pub additional_params: HashMap<String, Value>,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct FapiTokenResponse {
474 pub access_token: String,
476
477 pub token_type: String,
479
480 pub expires_in: i64,
482
483 pub refresh_token: Option<String>,
485
486 pub scope: Option<String>,
488
489 pub id_token: Option<String>,
491
492 pub cnf: Option<Value>,
494}
495
496impl FapiManager {
497 pub fn new(
499 config: FapiConfig,
500 dpop_manager: Arc<DpopManager>,
501 mtls_manager: Arc<MutualTlsManager>,
502 par_manager: Arc<PARManager>,
503 private_key_jwt_manager: Arc<PrivateKeyJwtManager>,
504 jwt_validator: Arc<SecureJwtValidator>,
505 ) -> Self {
506 Self {
507 dpop_manager,
508 mtls_manager,
509 par_manager,
510 private_key_jwt_manager,
511 jwt_validator,
512 config,
513 sessions: Arc::new(RwLock::new(HashMap::new())),
514 }
515 }
516
517 pub async fn validate_authorization_request(
519 &self,
520 request_object: &str,
521 client_cert: Option<&str>,
522 dpop_proof: Option<&str>,
523 request_uri: Option<&str>,
524 ) -> Result<FapiRequestObject> {
525 let claims = if let Some(uri) = request_uri {
526 if self.config.require_par {
528 let par_request = self.par_manager.consume_request(uri).await.map_err(|e| {
530 AuthError::InvalidRequest(format!("PAR request validation failed: {}", e))
531 })?;
532
533 tracing::info!(
534 "FAPI PAR request consumed successfully for client: {}",
535 par_request.client_id
536 );
537
538 let nonce = par_request.additional_params.get("nonce").cloned();
540 FapiRequestObject {
541 iss: par_request.client_id.clone(),
542 aud: self.config.issuer.clone(),
543 iat: Utc::now().timestamp(),
544 exp: Utc::now().timestamp() + 300, nbf: Some(Utc::now().timestamp()),
546 jti: uuid::Uuid::new_v4().to_string(),
547 response_type: par_request.response_type,
548 client_id: par_request.client_id,
549 redirect_uri: par_request.redirect_uri,
550 scope: par_request.scope.unwrap_or_default(),
551 state: par_request.state,
552 nonce,
553 code_challenge: par_request.code_challenge,
554 code_challenge_method: par_request.code_challenge_method,
555 additional_claims: par_request
556 .additional_params
557 .into_iter()
558 .map(|(k, v)| (k, serde_json::Value::String(v)))
559 .collect(),
560 }
561 } else {
562 return Err(AuthError::InvalidRequest(
563 "request_uri provided but PAR not required".to_string(),
564 ));
565 }
566 } else {
567 if self.config.require_par {
569 return Err(AuthError::InvalidRequest(
570 "PAR is required but no request_uri provided".to_string(),
571 ));
572 }
573
574 self.validate_request_object(request_object).await?
576 };
577
578 if self.config.require_mtls {
580 let cert = client_cert.ok_or_else(|| {
581 AuthError::auth_method("mtls", "mTLS certificate required for FAPI 2.0")
582 })?;
583 let cert_bytes = cert.as_bytes(); self.mtls_manager
585 .validate_client_certificate(cert_bytes, &claims.client_id)
586 .await?;
587 }
588
589 if self.config.require_dpop {
591 let proof = dpop_proof.ok_or_else(|| {
592 AuthError::auth_method("dpop", "DPoP proof required for FAPI 2.0")
593 })?;
594 self.dpop_manager
595 .validate_dpop_proof(
596 proof,
597 "POST",
598 &format!("{}/authorize", self.config.issuer),
599 None,
600 None,
601 )
602 .await?;
603 }
604
605 self.validate_request_claims(&claims).await?;
607
608 Ok(claims)
609 }
610
611 async fn validate_request_object(&self, request_object: &str) -> Result<FapiRequestObject> {
613 let decoding_key = &self.config.public_key;
615
616 match self
618 .jwt_validator
619 .validate_token(request_object, decoding_key)
620 {
621 Ok(secure_claims) => {
622 let header = jsonwebtoken::decode_header(request_object).map_err(|e| {
624 AuthError::token(format!("Invalid request object header: {}", e))
625 })?;
626
627 if !matches!(
629 header.alg,
630 Algorithm::RS256 | Algorithm::PS256 | Algorithm::ES256
631 ) {
632 return Err(AuthError::token(
633 "Request object must use RS256, PS256, or ES256".to_string(),
634 ));
635 }
636
637 let mut validation = Validation::new(header.alg);
639 validation.set_audience(&[&self.config.issuer]);
640 validation.validate_exp = true;
641 validation.validate_nbf = true;
642
643 let token_data = jsonwebtoken::decode::<FapiRequestObject>(
645 request_object,
646 &self.config.public_key,
647 &validation,
648 )
649 .map_err(|e| {
650 AuthError::token(format!("Request object validation failed: {}", e))
651 })?;
652
653 let fapi_claims = token_data.claims;
654
655 if secure_claims.sub != fapi_claims.client_id {
658 return Err(AuthError::token(
659 "Subject mismatch between secure validation and FAPI claims".to_string(),
660 ));
661 }
662
663 if secure_claims.iss != fapi_claims.iss {
665 return Err(AuthError::token(
666 "Issuer mismatch between secure validation and FAPI claims".to_string(),
667 ));
668 }
669
670 if secure_claims.exp != fapi_claims.exp {
672 return Err(AuthError::token(
673 "Expiry mismatch between secure validation and FAPI claims".to_string(),
674 ));
675 }
676
677 let now = Utc::now().timestamp();
679
680 if now - fapi_claims.iat > self.config.max_request_age {
682 return Err(AuthError::token("Request object too old".to_string()));
683 }
684
685 if fapi_claims.client_id.is_empty() {
687 return Err(AuthError::token(
688 "client_id required in request object".to_string(),
689 ));
690 }
691
692 if fapi_claims.redirect_uri.is_empty() {
693 return Err(AuthError::token(
694 "redirect_uri required in request object".to_string(),
695 ));
696 }
697
698 if fapi_claims.response_type.is_empty() {
699 return Err(AuthError::token(
700 "response_type required in request object".to_string(),
701 ));
702 }
703
704 tracing::info!(
705 "FAPI request object validated successfully with SecureJwtValidator for client: {}",
706 fapi_claims.client_id
707 );
708
709 Ok(fapi_claims)
710 }
711 Err(e) => {
712 tracing::error!("SecureJwtValidator failed for FAPI request object: {}", e);
713 Err(AuthError::token(format!(
714 "Enhanced JWT validation failed: {}",
715 e
716 )))
717 }
718 }
719 }
720
721 async fn validate_request_claims(&self, claims: &FapiRequestObject) -> Result<()> {
723 if !matches!(claims.response_type.as_str(), "code" | "code id_token") {
725 return Err(AuthError::InvalidRequest(
726 "FAPI 2.0 requires code or code id_token response type".to_string(),
727 ));
728 }
729
730 if claims.code_challenge.is_none() {
732 return Err(AuthError::InvalidRequest(
733 "PKCE required for FAPI 2.0".to_string(),
734 ));
735 }
736
737 if let Some(method) = &claims.code_challenge_method {
738 if method != "S256" {
739 return Err(AuthError::InvalidRequest(
740 "FAPI 2.0 requires S256 code challenge method".to_string(),
741 ));
742 }
743 } else {
744 return Err(AuthError::InvalidRequest(
745 "code_challenge_method required for FAPI 2.0".to_string(),
746 ));
747 }
748
749 Ok(())
750 }
751
752 pub async fn authenticate_client_jwt(&self, client_assertion: &str) -> Result<String> {
754 let auth_result = self
756 .private_key_jwt_manager
757 .authenticate_client(client_assertion)
758 .await
759 .map_err(|e| {
760 AuthError::auth_method(
761 "private_key_jwt",
762 format!("Private key JWT authentication failed: {}", e),
763 )
764 })?;
765
766 match auth_result.authenticated {
767 true => {
768 tracing::info!(
769 "FAPI client authenticated successfully using private key JWT: {}",
770 auth_result.client_id
771 );
772 Ok(auth_result.client_id)
773 }
774 false => {
775 let error_msg = auth_result.errors.join("; ");
776 tracing::error!("FAPI private key JWT authentication failed: {}", error_msg);
777 Err(AuthError::auth_method(
778 "private_key_jwt",
779 format!("Authentication failed: {}", error_msg),
780 ))
781 }
782 }
783 }
784
785 pub async fn validate_token_request(
787 &self,
788 client_assertion: Option<&str>,
789 client_cert: Option<&str>,
790 dpop_proof: Option<&str>,
791 authorization_code: &str,
792 ) -> Result<String> {
793 let client_id = if let Some(assertion) = client_assertion {
795 self.authenticate_client_jwt(assertion).await?
796 } else if self.config.require_mtls {
797 if let Some(cert) = client_cert {
798 let cert_bytes = cert.as_bytes();
799
800 let client_id = self.extract_client_id_from_certificate(cert_bytes).await?;
802
803 self.mtls_manager
804 .validate_client_certificate(cert_bytes, &client_id)
805 .await?;
806
807 client_id.to_string()
808 } else {
809 return Err(AuthError::auth_method(
810 "mtls",
811 "Client certificate required for FAPI 2.0 token request",
812 ));
813 }
814 } else {
815 return Err(AuthError::auth_method(
816 "fapi",
817 "FAPI 2.0 requires either private_key_jwt or mTLS client authentication",
818 ));
819 };
820
821 if self.config.require_dpop {
823 let proof = dpop_proof.ok_or_else(|| {
824 AuthError::auth_method("dpop", "DPoP proof required for FAPI 2.0 token request")
825 })?;
826 self.dpop_manager
827 .validate_dpop_proof(
828 proof,
829 "POST",
830 &format!("{}/token", self.config.issuer),
831 Some(authorization_code), None,
833 )
834 .await?;
835 }
836
837 Ok(client_id)
838 }
839
840 pub async fn generate_authorization_response(
842 &self,
843 client_id: &str,
844 code: Option<&str>,
845 state: Option<&str>,
846 error: Option<&str>,
847 error_description: Option<&str>,
848 ) -> Result<String> {
849 if !self.config.enable_jarm {
850 return Err(AuthError::Configuration {
851 message: "JARM not enabled".to_string(),
852 help: Some("Enable JARM in your configuration to use this feature".to_string()),
853 docs_url: Some("https://docs.auth-framework.com/fapi#jarm".to_string()),
854 source: None,
855 suggested_fix: Some("Set enable_jarm to true in your FAPIConfig".to_string()),
856 });
857 }
858
859 let now = Utc::now();
860 let exp = now + Duration::minutes(5); let response = FapiAuthorizationResponse {
863 iss: self.config.issuer.clone(),
864 aud: client_id.to_string(),
865 iat: now.timestamp(),
866 exp: exp.timestamp(),
867 code: code.map(|c| c.to_string()),
868 state: state.map(|s| s.to_string()),
869 error: error.map(|e| e.to_string()),
870 error_description: error_description.map(|d| d.to_string()),
871 additional_params: HashMap::new(),
872 };
873
874 let header = Header::new(self.config.response_signing_algorithm);
876
877 let token =
879 jsonwebtoken::encode(&header, &response, &self.config.private_key).map_err(|e| {
880 AuthError::TokenGeneration(format!("Failed to sign JARM response: {}", e))
881 })?;
882
883 Ok(token)
884 }
885
886 pub async fn generate_token_response(
888 &self,
889 client_id: &str,
890 user_id: &str,
891 scopes: Vec<String>,
892 cert_thumbprint: Option<String>,
893 dpop_jkt: Option<String>,
894 ) -> Result<FapiTokenResponse> {
895 let access_token = self
897 .generate_access_token(client_id, user_id, &scopes, &cert_thumbprint, &dpop_jkt)
898 .await?;
899
900 let refresh_token = self.generate_refresh_token(client_id, user_id).await?;
902
903 let mut cnf = json!({});
905
906 if let Some(thumbprint) = cert_thumbprint {
907 cnf["x5t#S256"] = Value::String(thumbprint);
908 }
909
910 if let Some(jkt) = dpop_jkt {
911 cnf["jkt"] = Value::String(jkt);
912 }
913
914 let response = FapiTokenResponse {
915 access_token,
916 token_type: "DPoP".to_string(), expires_in: 3600, refresh_token: Some(refresh_token),
919 scope: Some(scopes.join(" ")),
920 id_token: None, cnf: if cnf.as_object().map_or(true, |o| o.is_empty()) {
922 None
923 } else {
924 Some(cnf)
925 },
926 };
927
928 Ok(response)
929 }
930
931 async fn generate_access_token(
933 &self,
934 client_id: &str,
935 user_id: &str,
936 scopes: &[String],
937 cert_thumbprint: &Option<String>,
938 dpop_jkt: &Option<String>,
939 ) -> Result<String> {
940 let now = Utc::now();
941 let exp = now + Duration::hours(1);
942
943 let mut claims = json!({
944 "iss": self.config.issuer,
945 "aud": client_id,
946 "sub": user_id,
947 "iat": now.timestamp(),
948 "exp": exp.timestamp(),
949 "scope": scopes.join(" "),
950 "jti": Uuid::new_v4().to_string(),
951 });
952
953 let mut cnf = json!({});
955
956 if let Some(thumbprint) = cert_thumbprint {
957 cnf["x5t#S256"] = Value::String(thumbprint.clone());
958 }
959
960 if let Some(jkt) = dpop_jkt {
961 cnf["jkt"] = Value::String(jkt.clone());
962 }
963
964 if !cnf.as_object().map_or(true, |o| o.is_empty()) {
965 claims["cnf"] = cnf;
966 }
967
968 let header = Header::new(Algorithm::RS256);
970
971 let token =
973 jsonwebtoken::encode(&header, &claims, &self.config.private_key).map_err(|e| {
974 AuthError::TokenGeneration(format!("Failed to generate access token: {}", e))
975 })?;
976
977 Ok(token)
978 }
979
980 async fn generate_refresh_token(&self, client_id: &str, user_id: &str) -> Result<String> {
982 let now = Utc::now();
983 let exp = now + Duration::days(30); let claims = json!({
986 "iss": self.config.issuer,
987 "aud": client_id,
988 "sub": user_id,
989 "iat": now.timestamp(),
990 "exp": exp.timestamp(),
991 "typ": "refresh_token",
992 "jti": Uuid::new_v4().to_string(),
993 });
994
995 let header = Header::new(Algorithm::RS256);
997
998 let token =
1000 jsonwebtoken::encode(&header, &claims, &self.config.private_key).map_err(|e| {
1001 AuthError::TokenGeneration(format!("Failed to generate refresh token: {}", e))
1002 })?;
1003
1004 Ok(token)
1005 }
1006
1007 pub async fn create_session(
1009 &self,
1010 client_id: &str,
1011 user_id: &str,
1012 scopes: Vec<String>,
1013 dpop_proof: Option<String>,
1014 cert_thumbprint: Option<String>,
1015 request_jti: Option<String>,
1016 ) -> Result<String> {
1017 let session_id = Uuid::new_v4().to_string();
1018 let now = Utc::now();
1019 let expires_at = now + Duration::hours(24); let session = FapiSession {
1022 session_id: session_id.clone(),
1023 client_id: client_id.to_string(),
1024 user_id: user_id.to_string(),
1025 created_at: now,
1026 expires_at,
1027 dpop_proof,
1028 cert_thumbprint,
1029 request_jti,
1030 scopes,
1031 metadata: HashMap::new(),
1032 };
1033
1034 let mut sessions = self.sessions.write().await;
1035 sessions.insert(session_id.clone(), session);
1036
1037 Ok(session_id)
1038 }
1039
1040 pub async fn get_session(&self, session_id: &str) -> Result<Option<FapiSession>> {
1042 let sessions = self.sessions.read().await;
1043 Ok(sessions.get(session_id).cloned())
1044 }
1045
1046 pub async fn validate_session(&self, session_id: &str) -> Result<FapiSession> {
1048 let session = self
1049 .get_session(session_id)
1050 .await?
1051 .ok_or_else(|| AuthError::validation("Session not found".to_string()))?;
1052
1053 if Utc::now() > session.expires_at {
1055 return Err(AuthError::validation("Session expired".to_string()));
1056 }
1057
1058 Ok(session)
1059 }
1060
1061 pub async fn remove_session(&self, session_id: &str) -> Result<()> {
1063 let mut sessions = self.sessions.write().await;
1064 sessions.remove(session_id);
1065 Ok(())
1066 }
1067
1068 pub async fn audit_log(&self, event: &str, details: &Value) -> Result<()> {
1070 if self.config.enhanced_audit {
1071 let timestamp = chrono::Utc::now().to_rfc3339();
1075 let audit_entry = format!("[{}] FAPI AUDIT: {} - {}", timestamp, event, details);
1076
1077 tracing::info!("{}", audit_entry);
1079 }
1080 Ok(())
1081 }
1082
1083 async fn extract_client_id_from_certificate(&self, cert_bytes: &[u8]) -> Result<String> {
1085 let cert_str = String::from_utf8_lossy(cert_bytes);
1090
1091 if let Some(cn_start) = cert_str.find("CN=") {
1093 let cn_section = &cert_str[cn_start + 3..];
1094 if let Some(cn_end) = cn_section.find(',').or_else(|| cn_section.find('\n')) {
1095 let client_id = cn_section[..cn_end].trim().to_string();
1096 if !client_id.is_empty() {
1097 tracing::info!("Extracted client ID from certificate CN: {}", client_id);
1098 return Ok(client_id);
1099 }
1100 }
1101 }
1102
1103 if let Some(san_start) = cert_str.find("DNS:") {
1105 let san_section = &cert_str[san_start + 4..];
1106 if let Some(san_end) = san_section.find(',').or_else(|| san_section.find('\n')) {
1107 let client_id = san_section[..san_end].trim().to_string();
1108 if !client_id.is_empty() && client_id.contains("client") {
1109 tracing::info!("Extracted client ID from certificate SAN: {}", client_id);
1110 return Ok(client_id);
1111 }
1112 }
1113 }
1114
1115 use sha2::{Digest, Sha256};
1117 let cert_hash = format!("cert_client_{}", hex::encode(Sha256::digest(cert_bytes)));
1118
1119 tracing::info!(
1120 "Generated hash-based client ID from certificate: {}",
1121 cert_hash
1122 );
1123 Ok(cert_hash)
1124 }
1125
1126 pub fn check_compliance(&self) -> Vec<FapiComplianceViolation> {
1131 let mut violations = Vec::new();
1132
1133 if self.config.is_degraded {
1134 violations.push(FapiComplianceViolation {
1135 requirement: "crypto-keys".to_string(),
1136 severity: FapiViolationSeverity::Critical,
1137 message: "RSA key pair not properly configured; all FAPI operations will fail"
1138 .to_string(),
1139 });
1140 }
1141
1142 if !self.config.require_par {
1143 violations.push(FapiComplianceViolation {
1144 requirement: "par".to_string(),
1145 severity: FapiViolationSeverity::Critical,
1146 message: "FAPI 2.0 Security Profile requires Pushed Authorization Requests"
1147 .to_string(),
1148 });
1149 }
1150
1151 if !self.config.require_dpop {
1152 violations.push(FapiComplianceViolation {
1153 requirement: "sender-constraint".to_string(),
1154 severity: FapiViolationSeverity::Warning,
1155 message: "DPoP is recommended for sender-constrained tokens".to_string(),
1156 });
1157 }
1158
1159 if !self.config.require_mtls {
1160 violations.push(FapiComplianceViolation {
1161 requirement: "sender-constraint".to_string(),
1162 severity: FapiViolationSeverity::Warning,
1163 message: "mTLS is recommended for client authentication and token binding"
1164 .to_string(),
1165 });
1166 }
1167
1168 if !self.config.require_dpop && !self.config.require_mtls {
1169 violations.push(FapiComplianceViolation {
1170 requirement: "sender-constraint".to_string(),
1171 severity: FapiViolationSeverity::Critical,
1172 message:
1173 "FAPI 2.0 requires at least one sender-constraining mechanism (DPoP or mTLS)"
1174 .to_string(),
1175 });
1176 }
1177
1178 if !self.config.enable_jarm {
1179 violations.push(FapiComplianceViolation {
1180 requirement: "jarm".to_string(),
1181 severity: FapiViolationSeverity::Warning,
1182 message: "JARM is recommended for authorization response integrity".to_string(),
1183 });
1184 }
1185
1186 if !self.config.enhanced_audit {
1187 violations.push(FapiComplianceViolation {
1188 requirement: "audit".to_string(),
1189 severity: FapiViolationSeverity::Warning,
1190 message: "Enhanced audit logging is recommended for FAPI compliance".to_string(),
1191 });
1192 }
1193
1194 if !matches!(
1195 self.config.request_signing_algorithm,
1196 Algorithm::RS256 | Algorithm::PS256 | Algorithm::ES256
1197 ) {
1198 violations.push(FapiComplianceViolation {
1199 requirement: "algorithm".to_string(),
1200 severity: FapiViolationSeverity::Critical,
1201 message: "Request signing must use RS256, PS256, or ES256".to_string(),
1202 });
1203 }
1204
1205 if self.config.max_request_age > 600 {
1206 violations.push(FapiComplianceViolation {
1207 requirement: "request-lifetime".to_string(),
1208 severity: FapiViolationSeverity::Warning,
1209 message: "Max request age exceeds recommended 10 minutes".to_string(),
1210 });
1211 }
1212
1213 violations
1214 }
1215
1216 pub fn is_compliant(&self) -> bool {
1218 !self
1219 .check_compliance()
1220 .iter()
1221 .any(|v| matches!(v.severity, FapiViolationSeverity::Critical))
1222 }
1223
1224 pub async fn validate_sender_constrained_token(
1229 &self,
1230 access_token: &str,
1231 dpop_proof: Option<&str>,
1232 client_cert: Option<&str>,
1233 http_method: &str,
1234 http_uri: &str,
1235 ) -> Result<serde_json::Value> {
1236 let token_data = self
1238 .jwt_validator
1239 .validate_token(access_token, &self.config.public_key)?;
1240
1241 let header = jsonwebtoken::decode_header(access_token)
1243 .map_err(|e| AuthError::token(format!("Invalid token header: {}", e)))?;
1244 let mut validation = Validation::new(header.alg);
1245 validation.set_audience(&[&self.config.issuer]);
1246 validation.validate_exp = true;
1247 let raw_claims = jsonwebtoken::decode::<serde_json::Value>(
1248 access_token,
1249 &self.config.public_key,
1250 &validation,
1251 )
1252 .map_err(|e| AuthError::token(format!("Token decode failed: {}", e)))?;
1253
1254 let cnf = raw_claims.claims.get("cnf").ok_or_else(|| {
1256 AuthError::token("Token is not sender-constrained (missing cnf claim)".to_string())
1257 })?;
1258
1259 if let Some(expected_jkt) = cnf.get("jkt").and_then(|v| v.as_str()) {
1261 let proof = dpop_proof.ok_or_else(|| {
1262 AuthError::token(
1263 "Token is DPoP-bound but no DPoP proof header provided".to_string(),
1264 )
1265 })?;
1266 self.dpop_manager
1267 .validate_dpop_proof(proof, http_method, http_uri, None, Some(expected_jkt))
1268 .await?;
1269 }
1270
1271 if let Some(expected_thumbprint) = cnf.get("x5t#S256").and_then(|v| v.as_str()) {
1273 let cert = client_cert.ok_or_else(|| {
1274 AuthError::token(
1275 "Token is certificate-bound but no client certificate provided".to_string(),
1276 )
1277 })?;
1278 use base64::Engine;
1280 use sha2::{Digest, Sha256};
1281 let presented_thumbprint = base64::engine::general_purpose::URL_SAFE_NO_PAD
1282 .encode(Sha256::digest(cert.as_bytes()));
1283 if !bool::from(subtle::ConstantTimeEq::ct_eq(
1284 presented_thumbprint.as_bytes(),
1285 expected_thumbprint.as_bytes(),
1286 )) {
1287 return Err(AuthError::token(
1288 "Certificate thumbprint does not match token binding".to_string(),
1289 ));
1290 }
1291 }
1292
1293 Ok(serde_json::json!({
1295 "sub": token_data.sub,
1296 "iss": token_data.iss,
1297 "exp": token_data.exp,
1298 "scope": token_data.scope,
1299 }))
1300 }
1301
1302 pub fn validate_authorization_details(
1304 &self,
1305 authorization_details: &[AuthorizationDetail],
1306 ) -> Result<()> {
1307 if authorization_details.is_empty() {
1308 return Err(AuthError::InvalidRequest(
1309 "authorization_details must not be empty".to_string(),
1310 ));
1311 }
1312
1313 for (i, detail) in authorization_details.iter().enumerate() {
1314 if detail.r#type.is_empty() {
1315 return Err(AuthError::InvalidRequest(format!(
1316 "authorization_details[{}]: type is required",
1317 i
1318 )));
1319 }
1320
1321 for location in &detail.locations {
1323 if !location.starts_with("https://") {
1324 return Err(AuthError::InvalidRequest(format!(
1325 "authorization_details[{}]: location must use HTTPS: {}",
1326 i, location
1327 )));
1328 }
1329 }
1330
1331 for action in &detail.actions {
1333 if action.is_empty() {
1334 return Err(AuthError::InvalidRequest(format!(
1335 "authorization_details[{}]: empty action not allowed",
1336 i
1337 )));
1338 }
1339 }
1340 }
1341
1342 Ok(())
1343 }
1344}
1345
1346#[derive(Debug, Clone)]
1348pub struct FapiComplianceViolation {
1349 pub requirement: String,
1351 pub severity: FapiViolationSeverity,
1353 pub message: String,
1355}
1356
1357#[derive(Debug, Clone, PartialEq, Eq)]
1359pub enum FapiViolationSeverity {
1360 Critical,
1362 Warning,
1364}
1365
1366#[derive(Debug, Clone, Serialize, Deserialize)]
1368pub struct AuthorizationDetail {
1369 pub r#type: String,
1371
1372 #[serde(default)]
1374 pub locations: Vec<String>,
1375
1376 #[serde(default)]
1378 pub actions: Vec<String>,
1379
1380 #[serde(default)]
1382 pub datatypes: Vec<String>,
1383
1384 #[serde(skip_serializing_if = "Option::is_none")]
1386 pub identifier: Option<String>,
1387
1388 #[serde(flatten)]
1390 pub additional_fields: HashMap<String, Value>,
1391}
1392
1393impl Default for FapiConfig {
1394 fn default() -> Self {
1395 let issuer =
1397 std::env::var("FAPI_ISSUER").unwrap_or_else(|_| "https://auth.example.com".to_string());
1398
1399 let mut is_degraded = false;
1400
1401 let private_key = if let Ok(key_path) = std::env::var("FAPI_PRIVATE_KEY_PATH") {
1405 std::fs::read(&key_path)
1406 .map_err(|e| tracing::warn!("Failed to load private key from {}: {}", key_path, e))
1407 .and_then(|bytes| {
1408 EncodingKey::from_rsa_pem(&bytes)
1409 .map_err(|e| tracing::warn!("Invalid RSA key format: {}", e))
1410 })
1411 .unwrap_or_else(|_| {
1412 tracing::error!(
1413 "SECURITY CRITICAL: FAPI_PRIVATE_KEY_PATH is set but the key could not \
1414 be loaded. FAPI REQUIRES an RSA private key for request/response signing. \
1415 Using an ephemeral HMAC placeholder — ALL FAPI OPERATIONS WILL BE REJECTED \
1416 until a valid RSA key is provided. Set FAPI_PRIVATE_KEY_PATH to a valid \
1417 PEM-encoded RSA private key file."
1418 );
1419 is_degraded = true;
1420 use ring::rand::{SecureRandom, SystemRandom};
1421 let rng = SystemRandom::new();
1422 let mut bytes = [0u8; 32];
1423 rng.fill(&mut bytes).expect("AuthFramework fatal: system CSPRNG unavailable — the operating system cannot provide cryptographic randomness");
1424 EncodingKey::from_secret(&bytes)
1425 })
1426 } else {
1427 tracing::error!(
1428 "SECURITY CRITICAL: FAPI_PRIVATE_KEY_PATH not set. FAPI REQUIRES an RSA \
1429 private key for request and response signing. Using an ephemeral HMAC \
1430 placeholder — ALL FAPI OPERATIONS WILL BE REJECTED until a valid RSA key \
1431 is provided. Set FAPI_PRIVATE_KEY_PATH to a PEM-encoded RSA private key file."
1432 );
1433 is_degraded = true;
1434 use ring::rand::{SecureRandom, SystemRandom};
1435 let rng = SystemRandom::new();
1436 let mut bytes = [0u8; 32];
1437 rng.fill(&mut bytes).expect("AuthFramework fatal: system CSPRNG unavailable — the operating system cannot provide cryptographic randomness");
1438 EncodingKey::from_secret(&bytes)
1439 };
1440
1441 let public_key = if let Ok(key_path) = std::env::var("FAPI_PUBLIC_KEY_PATH") {
1443 std::fs::read(&key_path)
1444 .map_err(|e| tracing::warn!("Failed to load public key from {}: {}", key_path, e))
1445 .and_then(|bytes| {
1446 DecodingKey::from_rsa_pem(&bytes)
1447 .map_err(|e| tracing::warn!("Invalid RSA public key format: {}", e))
1448 })
1449 .unwrap_or_else(|_| {
1450 tracing::error!(
1451 "SECURITY CRITICAL: FAPI_PUBLIC_KEY_PATH is set but the key could not \
1452 be loaded. FAPI verification will not work correctly."
1453 );
1454 is_degraded = true;
1455 use ring::rand::{SecureRandom, SystemRandom};
1456 let rng = SystemRandom::new();
1457 let mut secret = [0u8; 32];
1458 rng.fill(&mut secret).expect("AuthFramework fatal: system CSPRNG unavailable — the operating system cannot provide cryptographic randomness");
1459 DecodingKey::from_secret(&secret)
1460 })
1461 } else {
1462 tracing::error!(
1463 "SECURITY CRITICAL: FAPI_PUBLIC_KEY_PATH not set. FAPI verification will \
1464 not work correctly. Set FAPI_PUBLIC_KEY_PATH to a PEM-encoded RSA public key file."
1465 );
1466 is_degraded = true;
1467 use ring::rand::{SecureRandom, SystemRandom};
1468 let rng = SystemRandom::new();
1469 let mut secret = [0u8; 32];
1470 rng.fill(&mut secret).expect("AuthFramework fatal: system CSPRNG unavailable — the operating system cannot provide cryptographic randomness");
1471 DecodingKey::from_secret(&secret)
1472 };
1473
1474 Self {
1475 issuer,
1476 request_signing_algorithm: Algorithm::RS256,
1477 response_signing_algorithm: Algorithm::RS256,
1478 private_key,
1479 public_key,
1480 max_request_age: 300, require_dpop: true,
1482 require_mtls: true,
1483 require_par: true,
1484 enable_jarm: true,
1485 enhanced_audit: true,
1486 is_degraded,
1487 }
1488 }
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493 use super::*;
1494
1495 #[test]
1496 fn test_fapi_config_builder() {
1497 use ring::rand::{SecureRandom, SystemRandom};
1498 let rng = SystemRandom::new();
1499 let mut bytes = [0u8; 32];
1500 rng.fill(&mut bytes).unwrap();
1501 let private_key = EncodingKey::from_secret(&bytes);
1502 let public_key = DecodingKey::from_secret(&bytes);
1503
1504 let config = FapiConfig::builder("https://fapi.example.com", private_key, public_key)
1505 .request_signing_algorithm(Algorithm::ES256)
1506 .max_request_age(120)
1507 .require_dpop(false)
1508 .degraded()
1509 .build();
1510
1511 assert_eq!(config.issuer, "https://fapi.example.com");
1512 assert_eq!(config.request_signing_algorithm, Algorithm::ES256);
1513 assert_eq!(config.max_request_age, 120);
1514 assert!(!config.require_dpop);
1515 assert!(config.require_mtls); assert!(config.is_degraded);
1517 }
1518
1519 #[test]
1520 fn test_fapi_session_builder() {
1521 let session = FapiSession::builder("sess_123", "client_456", "user_789", Duration::try_hours(1).unwrap())
1522 .dpop_proof("proof_abc")
1523 .add_scope("openid")
1524 .add_scopes(vec!["profile", "email"])
1525 .add_metadata("custom_flag", json!(true))
1526 .build();
1527
1528 assert_eq!(session.session_id, "sess_123");
1529 assert_eq!(session.client_id, "client_456");
1530 assert_eq!(session.user_id, "user_789");
1531 assert_eq!(session.dpop_proof.as_deref(), Some("proof_abc"));
1532 assert_eq!(session.scopes, vec!["openid", "profile", "email"]);
1533 assert_eq!(session.metadata["custom_flag"], true);
1534 }
1535
1536 #[tokio::test]
1537 async fn test_fapi_manager_creation() {
1538 let config = FapiConfig::default();
1540
1541 assert_eq!(config.issuer, "https://auth.example.com"); assert!(config.require_dpop);
1544 assert!(config.require_mtls);
1545 assert!(config.require_par);
1546 assert!(config.enable_jarm);
1547 assert!(config.enhanced_audit);
1548 }
1549
1550 #[tokio::test]
1551 async fn test_fapi_request_validation() {
1552 let config = FapiConfig::default();
1555
1556 let request_object = r#"{"iss":"client_id","aud":"https://example.com","exp":9999999999,"nbf":1000000000,"iat":1000000000,"jti":"unique_id"}"#;
1558
1559 let validation_result = validate_fapi_request_object(request_object, &config);
1561 assert!(
1562 validation_result.is_ok(),
1563 "FAPI request object validation failed"
1564 );
1565
1566 assert!(!request_object.is_empty());
1568 }
1569
1570 fn validate_fapi_request_object(
1572 request_object: &str,
1573 _config: &FapiConfig,
1574 ) -> Result<(), String> {
1575 let parsed: serde_json::Value = serde_json::from_str(request_object)
1577 .map_err(|_| "Invalid JSON structure in request object")?;
1578
1579 let required_claims = ["iss", "aud", "exp", "iat", "jti"];
1581 for claim in &required_claims {
1582 if parsed.get(claim).is_none() {
1583 return Err(format!("Missing required FAPI claim: {}", claim));
1584 }
1585 }
1586
1587 if let Some(exp) = parsed.get("exp").and_then(|v| v.as_i64()) {
1589 let now = chrono::Utc::now().timestamp();
1590 if exp <= now {
1591 return Err("Request object has expired".to_string());
1592 }
1593 }
1594
1595 Ok(())
1598 }
1599
1600 #[tokio::test]
1601 async fn test_fapi_response_generation() {
1602 let config = FapiConfig::default();
1605
1606 let auth_response = serde_json::json!({
1608 "code": "auth_code_123",
1609 "state": "client_state",
1610 "iss": config.issuer,
1611 "aud": "client_id",
1612 "exp": 9999999999i64
1613 });
1614
1615 assert!(auth_response["code"].is_string());
1617 }
1618
1619 #[tokio::test]
1620 async fn test_fapi_token_generation() {
1621 let config = FapiConfig::default();
1623
1624 let scopes = ["accounts".to_string(), "payments".to_string()];
1626 let client_id = "fapi_client_123";
1627 let user_id = "user_456";
1628 let cert_thumbprint = Some("sha256_cert_thumbprint".to_string());
1629
1630 assert!(config.require_dpop);
1632 assert!(config.require_mtls);
1633 assert!(!scopes.is_empty());
1634 assert!(!client_id.is_empty());
1635 assert!(!user_id.is_empty());
1636 assert!(cert_thumbprint.is_some());
1637 }
1638
1639 #[tokio::test]
1640 async fn test_fapi_session_management() {
1641 let config = FapiConfig::default();
1643
1644 let session_data = serde_json::json!({
1646 "client_id": "fapi_client",
1647 "user_id": "fapi_user",
1648 "scopes": ["accounts", "payments"],
1649 "mtls_cert": "client_certificate",
1650 "dpop_key": "client_dpop_key"
1651 });
1652
1653 assert!(session_data["mtls_cert"].is_string());
1655 assert!(session_data["dpop_key"].is_string());
1656 assert!(config.enhanced_audit);
1657 }
1658}