Skip to main content

auth_framework/server/security/
fapi.rs

1//! FAPI 2.0 (Financial-grade API) Security Profile Implementation
2//!
3//! This module implements the Financial-grade API (FAPI) 2.0 Security Profile,
4//! which provides enhanced security requirements for high-risk scenarios like
5//! financial services.
6//!
7//! # Security Features
8//!
9//! - **Enhanced Request Security**: JWS request object signing
10//! - **Response Security**: JWS response signing
11//! - **Advanced Client Authentication**: Enhanced mTLS and private key JWT
12//! - **Threat Protection**: JARM, DPoP, PAR mandatory
13//! - **Compliance Validation**: Automated FAPI 2.0 requirement checking
14//! - **Sender-Constrained Tokens**: Resource server validation of token binding
15//! - **Rich Authorization Requests**: RFC 9396 fine-grained authorization
16//! - **Enhanced Logging**: Detailed audit trails
17//!
18//! # FAPI 2.0 Requirements
19//!
20//! - Mutual TLS (mTLS) for client authentication
21//! - JWS request object signing (RFC 9101)
22//! - DPoP for sender constraining (RFC 9449)
23//! - Pushed Authorization Requests (PAR) (RFC 9126)
24//! - JWT Secured Authorization Response Mode (JARM)
25//! - Enhanced threat modeling and protection
26
27use 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/// FAPI 2.0 Security Profile Manager
40#[derive(Debug, Clone)]
41pub struct FapiManager {
42    /// DPoP manager for sender constraining
43    dpop_manager: Arc<DpopManager>,
44
45    /// Mutual TLS manager for client authentication
46    mtls_manager: Arc<MutualTlsManager>,
47
48    /// PAR manager for pushed authorization requests
49    par_manager: Arc<PARManager>,
50
51    /// Private key JWT manager
52    private_key_jwt_manager: Arc<PrivateKeyJwtManager>,
53
54    /// JWT validator for request object validation
55    jwt_validator: Arc<SecureJwtValidator>,
56
57    /// FAPI configuration
58    config: FapiConfig,
59
60    /// Active FAPI sessions
61    sessions: Arc<RwLock<HashMap<String, FapiSession>>>,
62}
63
64/// FAPI 2.0 Configuration
65#[derive(Clone)]
66pub struct FapiConfig {
67    /// Issuer identifier
68    pub issuer: String,
69
70    /// Request object signing algorithm (required: RS256, PS256, or ES256)
71    pub request_signing_algorithm: Algorithm,
72
73    /// Response signing algorithm
74    pub response_signing_algorithm: Algorithm,
75
76    /// Private key for signing
77    pub private_key: EncodingKey,
78
79    /// Public key for verification
80    pub public_key: DecodingKey,
81
82    /// Maximum request object age (seconds)
83    pub max_request_age: i64,
84
85    /// Require DPoP for all requests
86    pub require_dpop: bool,
87
88    /// Require mTLS for all requests
89    pub require_mtls: bool,
90
91    /// Require PAR for authorization requests
92    pub require_par: bool,
93
94    /// Enable JARM (JWT Secured Authorization Response Mode)
95    pub enable_jarm: bool,
96
97    /// Enhanced audit logging
98    pub enhanced_audit: bool,
99
100    /// Whether the config is running in degraded mode (HMAC placeholder keys
101    /// instead of the required RSA keys). FAPI operations MUST reject requests
102    /// when this flag is true.
103    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    /// Create a new FAPI 2.0 configuration builder.
130    ///
131    /// The builder enforces strict FAPI 2.0 compliance by default (DPoP, mTLS, PAR, JARM enabled).
132    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    /// Load configuration from environment variables.
154    #[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    /// Create a FAPI configuration from standard environment variables.
160    ///
161    /// Reads `FAPI_ISSUER`, `FAPI_PRIVATE_KEY_PATH`, and `FAPI_PUBLIC_KEY_PATH`.
162    pub fn from_env() -> Self {
163        Self::default()
164    }
165}
166
167/// Builder for FAPI 2.0 Configuration
168pub 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    /// Set the request signing algorithm (default: PS256).
185    ///
186    /// FAPI 2.0 requires PS256 or ES256.
187    pub fn request_signing_algorithm(mut self, alg: Algorithm) -> Self {
188        self.request_signing_algorithm = alg;
189        self
190    }
191
192    /// Set the response signing algorithm (default: PS256).
193    pub fn response_signing_algorithm(mut self, alg: Algorithm) -> Self {
194        self.response_signing_algorithm = alg;
195        self
196    }
197
198    /// Set the maximum allowed age for request objects in seconds (default: 60).
199    pub fn max_request_age(mut self, age: i64) -> Self {
200        self.max_request_age = age;
201        self
202    }
203
204    /// Set whether DPoP is required (default: true).
205    pub fn require_dpop(mut self, require: bool) -> Self {
206        self.require_dpop = require;
207        self
208    }
209
210    /// Set whether mTLS is required (default: true).
211    pub fn require_mtls(mut self, require: bool) -> Self {
212        self.require_mtls = require;
213        self
214    }
215
216    /// Set whether PAR is required (default: true).
217    pub fn require_par(mut self, require: bool) -> Self {
218        self.require_par = require;
219        self
220    }
221
222    /// Enable or disable JARM (default: true).
223    pub fn enable_jarm(mut self, enable: bool) -> Self {
224        self.enable_jarm = enable;
225        self
226    }
227
228    /// Enable or disable enhanced audit logging (default: true).
229    pub fn enhanced_audit(mut self, enable: bool) -> Self {
230        self.enhanced_audit = enable;
231        self
232    }
233
234    /// Mark this configuration as degraded (for testing/development only).
235    pub fn degraded(mut self) -> Self {
236        self.is_degraded = true;
237        self
238    }
239
240    /// Build the FapiConfig.
241    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/// FAPI 2.0 Session
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct FapiSession {
262    /// Session ID
263    pub session_id: String,
264
265    /// Client ID
266    pub client_id: String,
267
268    /// User ID
269    pub user_id: String,
270
271    /// Session creation time
272    pub created_at: DateTime<Utc>,
273
274    /// Session expiration time
275    pub expires_at: DateTime<Utc>,
276
277    /// DPoP proof token
278    pub dpop_proof: Option<String>,
279
280    /// Client certificate thumbprint
281    pub cert_thumbprint: Option<String>,
282
283    /// Request object JTI (to prevent replay)
284    pub request_jti: Option<String>,
285
286    /// Authorized scopes
287    pub scopes: Vec<String>,
288
289    /// Session metadata
290    pub metadata: HashMap<String, Value>,
291}
292
293impl FapiSession {
294    /// Create a new builder for a FAPI 2.0 Session.
295    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
317/// Builder for FAPI 2.0 Session
318pub 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    /// Set the DPoP proof token.
333    pub fn dpop_proof(mut self, proof: impl Into<String>) -> Self {
334        self.dpop_proof = Some(proof.into());
335        self
336    }
337
338    /// Set the client certificate thumbprint for mTLS.
339    pub fn cert_thumbprint(mut self, thumbprint: impl Into<String>) -> Self {
340        self.cert_thumbprint = Some(thumbprint.into());
341        self
342    }
343
344    /// Set the request object JTI to prevent replay.
345    pub fn request_jti(mut self, jti: impl Into<String>) -> Self {
346        self.request_jti = Some(jti.into());
347        self
348    }
349
350    /// Add an authorized scope.
351    pub fn add_scope(mut self, scope: impl Into<String>) -> Self {
352        self.scopes.push(scope.into());
353        self
354    }
355
356    /// Add multiple authorized scopes.
357    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    /// Add custom metadata to the session.
367    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    /// Build the FapiSession.
373    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/// FAPI 2.0 Request Object Claims
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct FapiRequestObject {
392    /// Issuer (client_id)
393    pub iss: String,
394
395    /// Audience (authorization server)
396    pub aud: String,
397
398    /// Issued at time
399    pub iat: i64,
400
401    /// Expiration time
402    pub exp: i64,
403
404    /// Not before time
405    pub nbf: Option<i64>,
406
407    /// JWT ID (unique identifier)
408    pub jti: String,
409
410    /// Response type
411    pub response_type: String,
412
413    /// Client ID
414    pub client_id: String,
415
416    /// Redirect URI
417    pub redirect_uri: String,
418
419    /// Scope
420    pub scope: String,
421
422    /// State
423    pub state: Option<String>,
424
425    /// Nonce (for OIDC)
426    pub nonce: Option<String>,
427
428    /// Code challenge (for PKCE)
429    pub code_challenge: Option<String>,
430
431    /// Code challenge method
432    pub code_challenge_method: Option<String>,
433
434    /// Additional claims
435    #[serde(flatten)]
436    pub additional_claims: HashMap<String, Value>,
437}
438
439/// FAPI 2.0 Authorization Response (JARM)
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct FapiAuthorizationResponse {
442    /// Issuer (authorization server)
443    pub iss: String,
444
445    /// Audience (client_id)
446    pub aud: String,
447
448    /// Issued at time
449    pub iat: i64,
450
451    /// Expiration time
452    pub exp: i64,
453
454    /// Authorization code (if successful)
455    pub code: Option<String>,
456
457    /// State parameter
458    pub state: Option<String>,
459
460    /// Error code (if failed)
461    pub error: Option<String>,
462
463    /// Error description
464    pub error_description: Option<String>,
465
466    /// Additional response parameters
467    #[serde(flatten)]
468    pub additional_params: HashMap<String, Value>,
469}
470
471/// FAPI 2.0 Token Response
472#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct FapiTokenResponse {
474    /// Access token
475    pub access_token: String,
476
477    /// Token type (always "DPoP" for FAPI 2.0)
478    pub token_type: String,
479
480    /// Expires in (seconds)
481    pub expires_in: i64,
482
483    /// Refresh token
484    pub refresh_token: Option<String>,
485
486    /// Scope
487    pub scope: Option<String>,
488
489    /// ID token (for OIDC)
490    pub id_token: Option<String>,
491
492    /// Certificate thumbprint confirmation
493    pub cnf: Option<Value>,
494}
495
496impl FapiManager {
497    /// Create a new FAPI manager
498    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    /// Validate FAPI 2.0 authorization request
518    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            // PAR workflow - validate request_uri and retrieve the request
527            if self.config.require_par {
528                // Use PAR manager to consume the pushed request
529                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                // Convert PAR request to FAPI request object
539                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, // 5 minutes
545                    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            // Standard request object validation
568            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            // Validate request object JWT using enhanced validation
575            self.validate_request_object(request_object).await?
576        };
577
578        // Validate mTLS if required
579        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(); // Convert to bytes for validation
584            self.mtls_manager
585                .validate_client_certificate(cert_bytes, &claims.client_id)
586                .await?;
587        }
588
589        // Validate DPoP if required
590        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        // Validate request object claims
606        self.validate_request_claims(&claims).await?;
607
608        Ok(claims)
609    }
610
611    /// Validate request object JWT
612    async fn validate_request_object(&self, request_object: &str) -> Result<FapiRequestObject> {
613        // Use the SecureJwtValidator for enhanced security validation
614        let decoding_key = &self.config.public_key;
615
616        // Validate using SecureJwtValidator with enhanced security features
617        match self
618            .jwt_validator
619            .validate_token(request_object, decoding_key)
620        {
621            Ok(secure_claims) => {
622                // Decode JWT header to get algorithm
623                let header = jsonwebtoken::decode_header(request_object).map_err(|e| {
624                    AuthError::token(format!("Invalid request object header: {}", e))
625                })?;
626
627                // Validate algorithm requirement for FAPI
628                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                // Set up validation with the same algorithm for FAPI-specific claims
638                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                // Decode the FAPI request object structure
644                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                // Use the validated secure claims for enhanced security checks
656                // Ensure the subject matches between secure validation and FAPI claims
657                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                // Use secure claims issuer for additional validation
664                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                // Validate expiry consistency
671                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                // Additional FAPI validations using the validated claims
678                let now = Utc::now().timestamp();
679
680                // Check request object age
681                if now - fapi_claims.iat > self.config.max_request_age {
682                    return Err(AuthError::token("Request object too old".to_string()));
683                }
684
685                // Validate required claims
686                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    /// Validate request object claims against FAPI requirements
722    async fn validate_request_claims(&self, claims: &FapiRequestObject) -> Result<()> {
723        // Validate response_type for FAPI 2.0
724        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        // Validate PKCE for public clients
731        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    /// Authenticate client using private key JWT (RFC 7523)
753    pub async fn authenticate_client_jwt(&self, client_assertion: &str) -> Result<String> {
754        // Use the private key JWT manager for authentication
755        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    /// Validate FAPI token request with enhanced security
786    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        // Client authentication - prefer private key JWT for FAPI 2.0
794        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                // Extract client ID from certificate subject or validate against registration
801                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        // Validate DPoP if required
822        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), // Include authorization code in DPoP validation
832                    None,
833                )
834                .await?;
835        }
836
837        Ok(client_id)
838    }
839
840    /// Generate FAPI 2.0 authorization response (JARM)
841    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); // Response expires in 5 minutes
861
862        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        // Create JWT header
875        let header = Header::new(self.config.response_signing_algorithm);
876
877        // Sign the response
878        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    /// Generate FAPI 2.0 token response
887    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        // Generate access token
896        let access_token = self
897            .generate_access_token(client_id, user_id, &scopes, &cert_thumbprint, &dpop_jkt)
898            .await?;
899
900        // Generate refresh token
901        let refresh_token = self.generate_refresh_token(client_id, user_id).await?;
902
903        // Build confirmation claim
904        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(), // Always DPoP for FAPI 2.0
917            expires_in: 3600,               // 1 hour
918            refresh_token: Some(refresh_token),
919            scope: Some(scopes.join(" ")),
920            id_token: None, // ID token generated by OIDC layer when openid scope present
921            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    /// Generate access token
932    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        // Add confirmation claims
954        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        // Create JWT header
969        let header = Header::new(Algorithm::RS256);
970
971        // Sign the token
972        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    /// Generate refresh token
981    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); // 30 day expiry
984
985        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        // Create JWT header
996        let header = Header::new(Algorithm::RS256);
997
998        // Sign the token
999        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    /// Create FAPI session
1008    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); // 24 hour session
1020
1021        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    /// Get FAPI session
1041    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    /// Validate FAPI session
1047    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        // Check expiration
1054        if Utc::now() > session.expires_at {
1055            return Err(AuthError::validation("Session expired".to_string()));
1056        }
1057
1058        Ok(session)
1059    }
1060
1061    /// Remove FAPI session
1062    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    /// Audit log entry for FAPI compliance
1069    pub async fn audit_log(&self, event: &str, details: &Value) -> Result<()> {
1070        if self.config.enhanced_audit {
1071            // Implement proper audit logging for FAPI compliance
1072            // This should log to secure, tamper-evident storage
1073            // Format: ISO 8601 timestamp, event type, client details, user context
1074            let timestamp = chrono::Utc::now().to_rfc3339();
1075            let audit_entry = format!("[{}] FAPI AUDIT: {} - {}", timestamp, event, details);
1076
1077            // In production, write to secure audit log storage
1078            tracing::info!("{}", audit_entry);
1079        }
1080        Ok(())
1081    }
1082
1083    /// Extract client ID from certificate
1084    async fn extract_client_id_from_certificate(&self, cert_bytes: &[u8]) -> Result<String> {
1085        // Parse the certificate and extract client ID from subject CN or SAN
1086        // For now, implement a basic extraction that works with common certificate formats
1087
1088        // Convert certificate to string for parsing (in production, use proper X.509 parsing)
1089        let cert_str = String::from_utf8_lossy(cert_bytes);
1090
1091        // Look for Common Name (CN) in the certificate subject
1092        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        // Fallback: Look for Subject Alternative Name (SAN) with client ID
1104        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        // If no client ID found, use SHA-256 of the certificate bytes for a stable identifier
1116        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    /// Validate FAPI 2.0 compliance of the current configuration.
1127    ///
1128    /// Returns a list of compliance violations. An empty list means the
1129    /// configuration is fully FAPI 2.0 compliant.
1130    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    /// Returns true if the configuration is fully FAPI 2.0 compliant (no critical violations).
1217    pub fn is_compliant(&self) -> bool {
1218        !self
1219            .check_compliance()
1220            .iter()
1221            .any(|v| matches!(v.severity, FapiViolationSeverity::Critical))
1222    }
1223
1224    /// Validate a sender-constrained access token at a resource server.
1225    ///
1226    /// Verifies that the token's `cnf` claim matches the presented proof
1227    /// (DPoP proof JKT or mTLS certificate thumbprint).
1228    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        // Validate the token's signature and standard claims via SecureJwtValidator
1237        let token_data = self
1238            .jwt_validator
1239            .validate_token(access_token, &self.config.public_key)?;
1240
1241        // Decode the raw JWT payload to access the cnf claim which is not in SecureJwtClaims
1242        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        // Extract cnf claim from the raw payload
1255        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        // Validate DPoP binding if cnf contains jkt
1260        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        // Validate mTLS certificate binding if cnf contains x5t#S256
1272        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            // Compute SHA-256 thumbprint of presented certificate
1279            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        // Return the validated token claims
1294        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    /// Validate a Rich Authorization Request (RFC 9396) authorization_details parameter.
1303    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            // Validate locations if present
1322            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            // Validate actions are non-empty strings
1332            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/// FAPI 2.0 compliance violation
1347#[derive(Debug, Clone)]
1348pub struct FapiComplianceViolation {
1349    /// Which FAPI requirement is violated
1350    pub requirement: String,
1351    /// Severity of the violation
1352    pub severity: FapiViolationSeverity,
1353    /// Human-readable description
1354    pub message: String,
1355}
1356
1357/// Severity of a compliance violation
1358#[derive(Debug, Clone, PartialEq, Eq)]
1359pub enum FapiViolationSeverity {
1360    /// Must be fixed for FAPI 2.0 compliance
1361    Critical,
1362    /// Recommended but not strictly required
1363    Warning,
1364}
1365
1366/// Rich Authorization Request detail (RFC 9396)
1367#[derive(Debug, Clone, Serialize, Deserialize)]
1368pub struct AuthorizationDetail {
1369    /// Type of authorization (e.g. "payment_initiation", "account_information")
1370    pub r#type: String,
1371
1372    /// Resource server locations this authorization applies to
1373    #[serde(default)]
1374    pub locations: Vec<String>,
1375
1376    /// Permitted actions (e.g. "read", "write", "execute")
1377    #[serde(default)]
1378    pub actions: Vec<String>,
1379
1380    /// Data types the client wants to access
1381    #[serde(default)]
1382    pub datatypes: Vec<String>,
1383
1384    /// Unique identifier for the authorization detail
1385    #[serde(skip_serializing_if = "Option::is_none")]
1386    pub identifier: Option<String>,
1387
1388    /// Additional fields specific to the authorization type
1389    #[serde(flatten)]
1390    pub additional_fields: HashMap<String, Value>,
1391}
1392
1393impl Default for FapiConfig {
1394    fn default() -> Self {
1395        // Load configuration from environment variables or use secure defaults
1396        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        // Load private key from environment — FAPI *requires* asymmetric keys.
1402        // There is intentionally no insecure fallback; callers must either set
1403        // FAPI_PRIVATE_KEY_PATH or construct FapiConfig manually.
1404        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        // Same treatment for the public key.
1442        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, // 5 minutes
1481            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); // default is true
1516        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        // Comprehensive FAPI configuration validation
1539        let config = FapiConfig::default();
1540
1541        // Verify FAPI security requirements are enabled by default
1542        assert_eq!(config.issuer, "https://auth.example.com"); // Corrected expected value
1543        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        // Test request object validation according to FAPI requirements
1553        // This should validate JWT request objects, signatures, and claims
1554        let config = FapiConfig::default();
1555
1556        // Mock request object with required FAPI claims
1557        let request_object = r#"{"iss":"client_id","aud":"https://example.com","exp":9999999999,"nbf":1000000000,"iat":1000000000,"jti":"unique_id"}"#;
1558
1559        // SECURITY CRITICAL: Perform comprehensive JWT validation
1560        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        // Ensure basic structure is valid but NEVER skip signature verification in production
1567        assert!(!request_object.is_empty());
1568    }
1569
1570    /// Validate FAPI request object with proper JWT verification
1571    fn validate_fapi_request_object(
1572        request_object: &str,
1573        _config: &FapiConfig,
1574    ) -> Result<(), String> {
1575        // Parse JSON structure
1576        let parsed: serde_json::Value = serde_json::from_str(request_object)
1577            .map_err(|_| "Invalid JSON structure in request object")?;
1578
1579        // Validate required FAPI claims
1580        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        // Validate expiration
1588        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        // In production: Verify JWT signature against client's public key
1596        // For testing: Accept if structure is valid
1597        Ok(())
1598    }
1599
1600    #[tokio::test]
1601    async fn test_fapi_response_generation() {
1602        // Test JARM (JWT Secured Authorization Response Mode) generation
1603        // This should create signed JWT responses for authorization responses
1604        let config = FapiConfig::default();
1605
1606        // Mock authorization response
1607        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        // In real implementation, sign the response as a JWT
1616        assert!(auth_response["code"].is_string());
1617    }
1618
1619    #[tokio::test]
1620    async fn test_fapi_token_generation() {
1621        // Test FAPI-compliant token generation with DPoP and mTLS
1622        let config = FapiConfig::default();
1623
1624        // Mock FAPI token request with required security features
1625        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        // Verify FAPI-specific requirements would be enforced
1631        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        // Test FAPI session management and security requirements
1642        let config = FapiConfig::default();
1643
1644        // Mock FAPI session with enhanced security
1645        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        // Verify session includes FAPI security elements
1654        assert!(session_data["mtls_cert"].is_string());
1655        assert!(session_data["dpop_key"].is_string());
1656        assert!(config.enhanced_audit);
1657    }
1658}