1use crate::errors::{AuthError, Result};
50use crate::security::secure_jwt::{SecureJwtClaims, SecureJwtConfig, SecureJwtValidator};
51use crate::server::oidc::oidc_response_modes::ResponseMode;
52use crate::server::oidc::oidc_session_management::SessionManager;
53use chrono::{DateTime, Duration, Utc};
54use jsonwebtoken::{DecodingKey, EncodingKey};
55use serde::{Deserialize, Serialize};
56use std::collections::HashMap;
57use std::sync::Arc;
58use tokio::sync::RwLock;
59use uuid::Uuid;
60
61#[derive(Clone)]
63pub struct EnhancedCibaConfig {
64 pub supported_modes: Vec<AuthenticationMode>,
66 pub default_auth_req_expiry: Duration,
68 pub max_polling_interval: u64,
70 pub min_polling_interval: u64,
72 pub enable_consent: bool,
74 pub enable_device_binding: bool,
76 pub supported_response_modes: Vec<ResponseMode>,
78 pub max_binding_message_length: usize,
80 pub enable_advanced_context: bool,
82 pub jwt_config: SecureJwtConfig,
84 pub issuer: String,
86 pub encoding_key: Option<EncodingKey>,
88 pub decoding_key: Option<DecodingKey>,
90 pub access_token_lifetime: u64,
92 pub id_token_lifetime: u64,
94 pub refresh_token_lifetime: u64,
96 pub max_notification_retries: u32,
98 pub notification_retry_backoff: u64,
100 pub notification_timeout: u64,
102}
103
104impl std::fmt::Debug for EnhancedCibaConfig {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 f.debug_struct("EnhancedCibaConfig")
107 .field("supported_modes", &self.supported_modes)
108 .field("default_auth_req_expiry", &self.default_auth_req_expiry)
109 .field("max_polling_interval", &self.max_polling_interval)
110 .field("min_polling_interval", &self.min_polling_interval)
111 .field("enable_consent", &self.enable_consent)
112 .field("enable_device_binding", &self.enable_device_binding)
113 .field("supported_response_modes", &self.supported_response_modes)
114 .field(
115 "max_binding_message_length",
116 &self.max_binding_message_length,
117 )
118 .field("enable_advanced_context", &self.enable_advanced_context)
119 .field("issuer", &self.issuer)
120 .field("encoding_key", &self.encoding_key.is_some())
121 .field("decoding_key", &self.decoding_key.is_some())
122 .field("access_token_lifetime", &self.access_token_lifetime)
123 .field("id_token_lifetime", &self.id_token_lifetime)
124 .field("refresh_token_lifetime", &self.refresh_token_lifetime)
125 .field("max_notification_retries", &self.max_notification_retries)
126 .field(
127 "notification_retry_backoff",
128 &self.notification_retry_backoff,
129 )
130 .field("notification_timeout", &self.notification_timeout)
131 .finish()
132 }
133}
134
135impl Default for EnhancedCibaConfig {
136 fn default() -> Self {
137 let mut jwt_config = SecureJwtConfig::default();
138 jwt_config.allowed_token_types.insert("id".to_string());
139 jwt_config.allowed_token_types.insert("ciba".to_string());
140
141 Self {
142 supported_modes: vec![
143 AuthenticationMode::Poll,
144 AuthenticationMode::Ping,
145 AuthenticationMode::Push,
146 ],
147 default_auth_req_expiry: Duration::minutes(10),
148 max_polling_interval: 60,
149 min_polling_interval: 2,
150 enable_consent: true,
151 enable_device_binding: true,
152 supported_response_modes: vec![
153 ResponseMode::Query,
154 ResponseMode::Fragment,
155 ResponseMode::FormPost,
156 ResponseMode::JwtQuery,
157 ],
158 max_binding_message_length: 1024,
159 enable_advanced_context: true,
160 jwt_config,
161 issuer: "auth-framework-ciba".to_string(),
162 encoding_key: None, decoding_key: None, access_token_lifetime: 3600, id_token_lifetime: 3600, refresh_token_lifetime: 86400, max_notification_retries: 3,
168 notification_retry_backoff: 5, notification_timeout: 30, }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub enum AuthenticationMode {
177 Poll,
179 Ping,
181 Push,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct EnhancedCibaAuthRequest {
188 pub auth_req_id: String,
190 pub client_id: String,
192 pub user_hint: UserIdentifierHint,
194 pub binding_message: Option<String>,
196 pub auth_context: Option<AuthenticationContext>,
198 pub scopes: Vec<String>,
200 pub mode: AuthenticationMode,
202 pub client_notification_endpoint: Option<String>,
204 pub expires_at: DateTime<Utc>,
206 pub created_at: DateTime<Utc>,
208 pub status: CibaRequestStatus,
210 pub session_id: Option<String>,
212 pub device_binding: Option<DeviceBinding>,
214 pub consent: Option<ConsentInfo>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct EnhancedCibaAuthResponse {
221 pub auth_req_id: String,
223 pub interval: Option<u64>,
225 pub expires_in: u64,
227 pub additional_data: HashMap<String, serde_json::Value>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub enum UserIdentifierHint {
234 LoginHint(String),
236 IdTokenHint(String),
238 UserCode(String),
240 PhoneNumber(String),
242 Email(String),
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct AuthenticationContext {
249 pub transaction_amount: Option<f64>,
251 pub transaction_currency: Option<String>,
253 pub merchant_info: Option<String>,
255 pub risk_score: Option<f64>,
257 pub location: Option<GeoLocation>,
259 pub device_info: Option<DeviceInfo>,
261 pub custom_attributes: HashMap<String, serde_json::Value>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct GeoLocation {
268 pub latitude: f64,
270 pub longitude: f64,
272 pub accuracy: Option<f64>,
274 pub location_name: Option<String>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct DeviceInfo {
281 pub device_id: String,
283 pub device_type: String,
285 pub os: Option<String>,
287 pub browser: Option<String>,
289 pub ip_address: Option<String>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct DeviceBinding {
296 pub binding_id: String,
298 pub device_public_key: Option<String>,
300 pub binding_method: DeviceBindingMethod,
302 pub created_at: DateTime<Utc>,
304 pub expires_at: Option<DateTime<Utc>>,
306 pub device_fingerprint: Option<String>,
308 pub challenge: Option<String>,
310 pub challenge_response: Option<String>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub enum DeviceBindingMethod {
317 PublicKey,
319 Certificate,
321 Attestation,
323 Biometric,
325 Platform,
327 Implicit,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ConsentInfo {
334 pub consent_id: String,
336 pub status: ConsentStatus,
338 pub consented_scopes: Vec<String>,
340 pub expires_at: Option<DateTime<Utc>>,
342 pub created_at: DateTime<Utc>,
344}
345
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
348pub enum ConsentStatus {
349 Pending,
351 Granted,
353 Denied,
355 Expired,
357}
358
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
361pub enum CibaRequestStatus {
362 Pending,
364 InProgress,
366 Completed,
368 Failed,
370 Expired,
372 Cancelled,
374}
375
376#[derive(Debug)]
378pub struct BackchannelAuthParams<'a> {
379 pub client_id: &'a str,
380 pub user_hint: UserIdentifierHint,
381 pub binding_message: Option<String>,
382 pub auth_context: Option<AuthenticationContext>,
383 pub scopes: Vec<String>,
384 pub mode: AuthenticationMode,
385 pub client_notification_endpoint: Option<String>,
386}
387
388#[derive(Debug)]
390pub struct EnhancedCibaManager {
391 config: EnhancedCibaConfig,
393 auth_requests: Arc<RwLock<HashMap<String, EnhancedCibaAuthRequest>>>,
395 session_manager: Arc<SessionManager>,
397 notification_client: crate::server::core::common_http::HttpClient,
399 jwt_validator: Arc<SecureJwtValidator>,
401}
402
403impl EnhancedCibaManager {
404 pub fn new(config: EnhancedCibaConfig) -> Self {
406 use crate::server::core::common_config::EndpointConfig;
407
408 let jwt_validator = Arc::new(SecureJwtValidator::new(config.jwt_config.clone()));
409
410 let endpoint_config = EndpointConfig::new(&config.issuer);
412 let notification_client = crate::server::core::common_http::HttpClient::new(
413 endpoint_config,
414 )
415 .unwrap_or_else(|_| {
416 let fallback_config = EndpointConfig::new("https://localhost");
418 crate::server::core::common_http::HttpClient::new(fallback_config).unwrap()
419 });
420
421 Self {
422 config,
423 auth_requests: Arc::new(RwLock::new(HashMap::new())),
424 session_manager: Arc::new(SessionManager::new(Default::default())),
425 notification_client,
426 jwt_validator,
427 }
428 }
429
430 pub fn new_with_session_manager(
432 config: EnhancedCibaConfig,
433 session_manager: Arc<SessionManager>,
434 ) -> Self {
435 use crate::server::core::common_config::EndpointConfig;
436
437 let jwt_validator = Arc::new(SecureJwtValidator::new(config.jwt_config.clone()));
438
439 let endpoint_config = EndpointConfig::new(&config.issuer);
441 let notification_client = crate::server::core::common_http::HttpClient::new(
442 endpoint_config,
443 )
444 .unwrap_or_else(|_| {
445 let fallback_config = EndpointConfig::new("https://localhost");
447 crate::server::core::common_http::HttpClient::new(fallback_config).unwrap()
448 });
449
450 Self {
451 config,
452 auth_requests: Arc::new(RwLock::new(HashMap::new())),
453 session_manager,
454 notification_client,
455 jwt_validator,
456 }
457 }
458
459 pub fn configure_keys(&mut self, encoding_key: EncodingKey, decoding_key: DecodingKey) {
461 self.config.encoding_key = Some(encoding_key);
462 self.config.decoding_key = Some(decoding_key);
463 }
464
465 #[cfg(test)]
467 pub fn new_for_testing() -> Self {
468 use jsonwebtoken::{DecodingKey, EncodingKey};
469
470 let config = EnhancedCibaConfig {
471 encoding_key: Some(EncodingKey::from_secret(b"test-secret-key")),
472 decoding_key: Some(DecodingKey::from_secret(b"test-secret-key")),
473 ..Default::default()
474 };
475
476 Self::new(config)
477 }
478
479 pub async fn initiate_backchannel_auth(
481 &self,
482 params: BackchannelAuthParams<'_>,
483 ) -> Result<EnhancedCibaAuthResponse> {
484 if let Some(ref message) = params.binding_message
486 && message.len() > self.config.max_binding_message_length
487 {
488 return Err(AuthError::validation(format!(
489 "Binding message too long: {} > {}",
490 message.len(),
491 self.config.max_binding_message_length
492 )));
493 }
494
495 if !self.config.supported_modes.contains(¶ms.mode) {
497 return Err(AuthError::validation(format!(
498 "Unsupported authentication mode: {:?}",
499 params.mode
500 )));
501 }
502
503 if matches!(
505 params.mode,
506 AuthenticationMode::Ping | AuthenticationMode::Push
507 ) && params.client_notification_endpoint.is_none()
508 {
509 return Err(AuthError::validation(
510 "Notification endpoint required for ping/push modes".to_string(),
511 ));
512 }
513
514 let auth_req_id = Uuid::new_v4().to_string();
515 let now = Utc::now();
516 let expires_at = now + self.config.default_auth_req_expiry;
517
518 let device_binding = if self.config.enable_device_binding {
520 let challenge = Uuid::new_v4().to_string();
521 let device_fingerprint = self.generate_device_fingerprint(¶ms)?;
522
523 Some(DeviceBinding {
524 binding_id: Uuid::new_v4().to_string(),
525 device_public_key: None, binding_method: DeviceBindingMethod::Platform, created_at: now,
528 expires_at: Some(expires_at),
529 device_fingerprint: Some(device_fingerprint),
530 challenge: Some(challenge),
531 challenge_response: None, })
533 } else {
534 None
535 };
536
537 let consent = if self.config.enable_consent {
539 Some(ConsentInfo {
540 consent_id: Uuid::new_v4().to_string(),
541 status: ConsentStatus::Pending,
542 consented_scopes: params.scopes.clone(),
543 expires_at: Some(expires_at),
544 created_at: now,
545 })
546 } else {
547 None
548 };
549
550 let auth_request = EnhancedCibaAuthRequest {
551 auth_req_id: auth_req_id.clone(),
552 client_id: params.client_id.to_string(),
553 user_hint: params.user_hint,
554 binding_message: params.binding_message,
555 auth_context: params.auth_context,
556 scopes: params.scopes,
557 mode: params.mode.clone(),
558 client_notification_endpoint: params.client_notification_endpoint,
559 expires_at,
560 created_at: now,
561 status: CibaRequestStatus::Pending,
562 session_id: None,
563 device_binding,
564 consent,
565 };
566
567 {
569 let mut requests = self.auth_requests.write().await;
570 requests.insert(auth_req_id.clone(), auth_request);
571 }
572
573 let interval = if matches!(params.mode, AuthenticationMode::Poll) {
575 Some(self.config.min_polling_interval)
576 } else {
577 None
578 };
579
580 let expires_in = (expires_at - now).num_seconds() as u64;
581
582 Ok(EnhancedCibaAuthResponse {
583 auth_req_id,
584 interval,
585 expires_in,
586 additional_data: HashMap::new(),
587 })
588 }
589
590 pub async fn poll_auth_request(&self, auth_req_id: &str) -> Result<CibaTokenResponse> {
592 let mut requests = self.auth_requests.write().await;
593
594 let request = requests
595 .get_mut(auth_req_id)
596 .ok_or_else(|| AuthError::auth_method("ciba", "Authentication request not found"))?;
597
598 if Utc::now() > request.expires_at {
600 request.status = CibaRequestStatus::Expired;
601 return Err(AuthError::auth_method(
602 "ciba",
603 "Request expired".to_string(),
604 ));
605 }
606
607 match request.status {
608 CibaRequestStatus::Pending => Err(AuthError::auth_method(
609 "ciba",
610 "authorization_pending".to_string(),
611 )),
612 CibaRequestStatus::InProgress => Err(AuthError::auth_method(
613 "ciba",
614 "authorization_pending".to_string(),
615 )),
616 CibaRequestStatus::Completed => {
617 let session_valid = self
619 .validate_session_for_request(request)
620 .await
621 .unwrap_or(false);
622
623 if !session_valid {
624 return Err(AuthError::auth_method("ciba", "Invalid or expired session"));
625 }
626
627 self.generate_tokens_for_request(request).await
629 }
630 CibaRequestStatus::Failed => {
631 Err(AuthError::auth_method("ciba", "access_denied".to_string()))
632 }
633 CibaRequestStatus::Expired => {
634 Err(AuthError::auth_method("ciba", "expired_token".to_string()))
635 }
636 CibaRequestStatus::Cancelled => {
637 Err(AuthError::auth_method("ciba", "access_denied".to_string()))
638 }
639 }
640 }
641
642 pub async fn complete_auth_request(
644 &self,
645 auth_req_id: &str,
646 user_authenticated: bool,
647 session_id: Option<String>,
648 ) -> Result<()> {
649 let mut requests = self.auth_requests.write().await;
650
651 let request = requests
652 .get_mut(auth_req_id)
653 .ok_or_else(|| AuthError::auth_method("ciba", "Authentication request not found"))?;
654
655 if user_authenticated {
656 request.status = CibaRequestStatus::Completed;
657
658 let mut session_metadata = std::collections::HashMap::new();
660 session_metadata.insert("auth_req_id".to_string(), auth_req_id.to_string());
661 session_metadata.insert("ciba_mode".to_string(), format!("{:?}", request.mode));
662
663 if let Some(ref auth_context) = request.auth_context {
665 if let Some(amount) = auth_context.transaction_amount {
666 session_metadata.insert("transaction_amount".to_string(), amount.to_string());
667 }
668 if let Some(ref currency) = auth_context.transaction_currency {
669 session_metadata.insert("transaction_currency".to_string(), currency.clone());
670 }
671 if let Some(risk_score) = auth_context.risk_score {
672 session_metadata.insert("risk_score".to_string(), risk_score.to_string());
673 }
674 }
675
676 let user_subject = match &request.user_hint {
678 UserIdentifierHint::LoginHint(hint) => {
679 if hint.is_empty() {
681 return Err(AuthError::InvalidRequest(
682 "Login hint cannot be empty".to_string(),
683 ));
684 }
685 hint.clone()
686 }
687 UserIdentifierHint::Email(email) => {
688 if !email.contains('@') {
690 return Err(AuthError::InvalidRequest(
691 "Invalid email format in user hint".to_string(),
692 ));
693 }
694 email.clone()
695 }
696 UserIdentifierHint::PhoneNumber(phone) => {
697 if phone.len() < 10 {
699 return Err(AuthError::InvalidRequest(
700 "Invalid phone number format".to_string(),
701 ));
702 }
703 phone.clone()
704 }
705 UserIdentifierHint::UserCode(code) => {
706 if code.len() < 4 {
708 return Err(AuthError::InvalidRequest("User code too short".to_string()));
709 }
710 code.clone()
711 }
712 UserIdentifierHint::IdTokenHint(token) => {
713 if token.split('.').count() != 3 {
716 return Err(AuthError::InvalidToken(
717 "Invalid JWT format in id_token_hint".to_string(),
718 ));
719 }
720 match self.validate_id_token_hint(token) {
722 Ok(claims) => {
723 claims.sub
725 }
726 Err(e) => {
727 tracing::warn!("JWT validation failed for id_token_hint: {}", e);
728 use std::collections::hash_map::DefaultHasher;
731 use std::hash::{Hash, Hasher};
732 let mut hasher = DefaultHasher::new();
733 token.hash(&mut hasher);
734 format!("fallback_subject_{}", hasher.finish())
735 }
736 }
737 }
738 };
739
740 session_metadata.insert("validated_subject".to_string(), user_subject.clone());
742
743 let _session_manager = &self.session_manager;
745 let new_session_id =
746 session_id.unwrap_or_else(|| format!("ciba_session_{}", Uuid::new_v4()));
747
748 let mut metadata = std::collections::HashMap::new();
750 metadata.insert("auth_req_id".to_string(), auth_req_id.to_string());
751 metadata.insert("ciba_mode".to_string(), format!("{:?}", request.mode));
752 metadata.insert(
753 "session_info".to_string(),
754 serde_json::to_string(&session_metadata).unwrap_or_default(),
755 );
756 metadata.insert("created_by".to_string(), "CIBA".to_string());
757 metadata.insert("ciba_enabled".to_string(), "true".to_string());
758
759 let final_session_id = new_session_id.clone();
763
764 request.session_id = Some(final_session_id.clone());
767
768 tracing::info!(
769 "CIBA session configured: {} for user: {} in mode: {:?}",
770 final_session_id,
771 user_subject,
772 request.mode
773 );
774
775 if let Some(ref mut consent) = request.consent {
777 consent.status = ConsentStatus::Granted;
778 }
779
780 if matches!(
782 request.mode,
783 AuthenticationMode::Ping | AuthenticationMode::Push
784 ) && let Some(ref endpoint) = request.client_notification_endpoint
785 {
786 self.send_notification(endpoint.as_str(), auth_req_id)
787 .await?;
788 }
789 } else {
790 request.status = CibaRequestStatus::Failed;
791
792 if let Some(ref mut consent) = request.consent {
794 consent.status = ConsentStatus::Denied;
795 }
796 }
797
798 Ok(())
799 }
800
801 async fn send_notification(&self, endpoint: &str, auth_req_id: &str) -> Result<()> {
803 let notification_data = serde_json::json!({
804 "auth_req_id": auth_req_id,
805 "timestamp": Utc::now(),
806 "issuer": self.config.issuer,
807 });
808
809 let mut last_error = None;
810
811 for attempt in 0..self.config.max_notification_retries {
813 let backoff_delay = self.config.notification_retry_backoff * (2_u64.pow(attempt));
814
815 if attempt > 0 {
816 tokio::time::sleep(tokio::time::Duration::from_secs(backoff_delay)).await;
817 }
818
819 let request = self
821 .notification_client
822 .post(endpoint)
823 .timeout(tokio::time::Duration::from_secs(
824 self.config.notification_timeout,
825 ))
826 .header("Content-Type", "application/json")
827 .header("User-Agent", "AuthFramework-CIBA/1.0")
828 .header(
830 "Authorization",
831 format!("Bearer {}", self.generate_notification_token(auth_req_id)?),
832 )
833 .json(¬ification_data);
834
835 match request.send().await {
836 Ok(response) => {
837 let status = response.status();
838 if status.is_success() {
839 tracing::info!(
840 "CIBA notification sent successfully to {} for request {}",
841 endpoint,
842 auth_req_id
843 );
844 return Ok(());
845 } else {
846 let error_text = response.text().await.unwrap_or_default();
847 let error_msg =
848 format!("Notification failed with status {}: {}", status, error_text);
849 last_error = Some(AuthError::internal(error_msg));
850
851 if status.is_client_error() {
853 break;
854 }
855 }
856 }
857 Err(e) => {
858 let error_msg = format!("Network error sending notification: {}", e);
859 last_error = Some(AuthError::internal(error_msg));
860
861 tracing::warn!(
862 "CIBA notification attempt {} failed for {}: {}",
863 attempt + 1,
864 endpoint,
865 e
866 );
867 }
868 }
869 }
870
871 Err(last_error.unwrap_or_else(|| AuthError::internal("All notification attempts failed")))
873 }
874
875 fn generate_notification_token(&self, auth_req_id: &str) -> Result<String> {
877 use std::collections::hash_map::DefaultHasher;
880 use std::hash::{Hash, Hasher};
881
882 let mut hasher = DefaultHasher::new();
883 auth_req_id.hash(&mut hasher);
884 self.config.issuer.hash(&mut hasher);
885 chrono::Utc::now().timestamp().hash(&mut hasher);
886
887 Ok(format!("notif_{:016x}", hasher.finish()))
888 }
889
890 async fn generate_tokens_for_request(
892 &self,
893 request: &EnhancedCibaAuthRequest,
894 ) -> Result<CibaTokenResponse> {
895 let now = chrono::Utc::now();
896 let jti_access = Uuid::new_v4().to_string();
897 let jti_id = Uuid::new_v4().to_string();
898 let jti_refresh = Uuid::new_v4().to_string();
899
900 let subject = self.extract_subject_from_hint(&request.user_hint)?;
902
903 let access_claims = SecureJwtClaims {
905 sub: subject.clone(),
906 iss: self.config.issuer.clone(),
907 aud: request.client_id.clone(),
908 exp: (now.timestamp() + self.config.access_token_lifetime as i64),
909 nbf: now.timestamp(),
910 iat: now.timestamp(),
911 jti: jti_access.clone(),
912 scope: request.scopes.join(" "),
913 typ: "access".to_string(),
914 sid: request.session_id.clone(),
915 client_id: Some(request.client_id.clone()),
916 auth_ctx_hash: self.compute_auth_context_hash(&request.auth_context),
917 };
918
919 let access_token = if let Some(ref encoding_key) = self.config.encoding_key {
921 self.create_jwt_token(&access_claims, encoding_key)?
922 } else {
923 return Err(AuthError::internal(
924 "No encoding key configured for JWT generation",
925 ));
926 };
927
928 let id_token = if request.scopes.contains(&"openid".to_string()) {
930 let id_claims = SecureJwtClaims {
931 sub: subject.clone(),
932 iss: self.config.issuer.clone(),
933 aud: request.client_id.clone(),
934 exp: (now.timestamp() + self.config.id_token_lifetime as i64),
935 nbf: now.timestamp(),
936 iat: now.timestamp(),
937 jti: jti_id.clone(),
938 scope: "openid".to_string(),
939 typ: "id".to_string(),
940 sid: request.session_id.clone(),
941 client_id: Some(request.client_id.clone()),
942 auth_ctx_hash: self.compute_auth_context_hash(&request.auth_context),
943 };
944
945 if let Some(ref encoding_key) = self.config.encoding_key {
946 Some(self.create_jwt_token(&id_claims, encoding_key)?)
947 } else {
948 None
949 }
950 } else {
951 None
952 };
953
954 let refresh_token = {
956 let refresh_claims = SecureJwtClaims {
957 sub: subject,
958 iss: self.config.issuer.clone(),
959 aud: request.client_id.clone(),
960 exp: (now.timestamp() + self.config.refresh_token_lifetime as i64),
961 nbf: now.timestamp(),
962 iat: now.timestamp(),
963 jti: jti_refresh.clone(),
964 scope: request.scopes.join(" "),
965 typ: "refresh".to_string(),
966 sid: request.session_id.clone(),
967 client_id: Some(request.client_id.clone()),
968 auth_ctx_hash: self.compute_auth_context_hash(&request.auth_context),
969 };
970
971 if let Some(ref encoding_key) = self.config.encoding_key {
972 Some(self.create_jwt_token(&refresh_claims, encoding_key)?)
973 } else {
974 None
975 }
976 };
977
978 Ok(CibaTokenResponse {
979 access_token,
980 token_type: "Bearer".to_string(),
981 refresh_token,
982 expires_in: self.config.access_token_lifetime,
983 id_token,
984 scope: Some(request.scopes.join(" ")),
985 })
986 }
987
988 fn create_jwt_token(
990 &self,
991 claims: &SecureJwtClaims,
992 encoding_key: &EncodingKey,
993 ) -> Result<String> {
994 use jsonwebtoken::{Header, encode};
995
996 let header = Header::new(jsonwebtoken::Algorithm::HS256);
997 encode(&header, claims, encoding_key)
998 .map_err(|e| AuthError::internal(format!("Failed to create JWT token: {}", e)))
999 }
1000
1001 fn extract_subject_from_hint(&self, hint: &UserIdentifierHint) -> Result<String> {
1003 match hint {
1004 UserIdentifierHint::LoginHint(login) => {
1005 if login.is_empty() {
1006 return Err(AuthError::InvalidRequest("Empty login hint".to_string()));
1007 }
1008 Ok(login.clone())
1009 }
1010 UserIdentifierHint::Email(email) => {
1011 if !email.contains('@') || email.len() < 3 {
1012 return Err(AuthError::InvalidRequest(
1013 "Invalid email format".to_string(),
1014 ));
1015 }
1016 Ok(email.clone())
1017 }
1018 UserIdentifierHint::PhoneNumber(phone) => {
1019 if phone.len() < 10 {
1020 return Err(AuthError::InvalidRequest(
1021 "Invalid phone number".to_string(),
1022 ));
1023 }
1024 Ok(phone.clone())
1025 }
1026 UserIdentifierHint::UserCode(code) => {
1027 if code.len() < 4 {
1028 return Err(AuthError::InvalidRequest("User code too short".to_string()));
1029 }
1030 Ok(code.clone())
1031 }
1032 UserIdentifierHint::IdTokenHint(token) => self.extract_subject_from_id_token(token),
1033 }
1034 }
1035
1036 fn extract_subject_from_id_token(&self, token: &str) -> Result<String> {
1038 if let Some(ref decoding_key) = self.config.decoding_key {
1039 match self.jwt_validator.validate_token(token, decoding_key, true) {
1040 Ok(claims) => Ok(claims.sub),
1041 Err(e) => Err(AuthError::InvalidToken(format!(
1042 "Invalid ID token hint: {}",
1043 e
1044 ))),
1045 }
1046 } else {
1047 if token.split('.').count() != 3 {
1049 return Err(AuthError::InvalidToken("Invalid JWT format".to_string()));
1050 }
1051
1052 use std::collections::hash_map::DefaultHasher;
1054 use std::hash::{Hash, Hasher};
1055 let mut hasher = DefaultHasher::new();
1056 token.hash(&mut hasher);
1057 Ok(format!("id_token_subject_{}", hasher.finish()))
1058 }
1059 }
1060
1061 fn compute_auth_context_hash(
1063 &self,
1064 auth_context: &Option<AuthenticationContext>,
1065 ) -> Option<String> {
1066 auth_context.as_ref().map(|ctx| {
1067 use std::collections::hash_map::DefaultHasher;
1068 use std::hash::{Hash, Hasher};
1069
1070 let mut hasher = DefaultHasher::new();
1071 if let Some(amount) = ctx.transaction_amount {
1072 amount.to_bits().hash(&mut hasher);
1073 }
1074 if let Some(ref currency) = ctx.transaction_currency {
1075 currency.hash(&mut hasher);
1076 }
1077 if let Some(risk) = ctx.risk_score {
1078 risk.to_bits().hash(&mut hasher);
1079 }
1080
1081 format!("ctx_{}", hasher.finish())
1082 })
1083 }
1084
1085 fn generate_device_fingerprint(&self, params: &BackchannelAuthParams) -> Result<String> {
1087 use std::collections::hash_map::DefaultHasher;
1088 use std::hash::{Hash, Hasher};
1089
1090 let mut hasher = DefaultHasher::new();
1091
1092 params.client_id.hash(&mut hasher);
1094
1095 if let Some(ref auth_context) = params.auth_context
1097 && let Some(ref device_info) = auth_context.device_info
1098 {
1099 device_info.device_id.hash(&mut hasher);
1100 device_info.device_type.hash(&mut hasher);
1101 if let Some(ref os) = device_info.os {
1102 os.hash(&mut hasher);
1103 }
1104 if let Some(ref browser) = device_info.browser {
1105 browser.hash(&mut hasher);
1106 }
1107 if let Some(ref ip) = device_info.ip_address {
1108 ip.hash(&mut hasher);
1109 }
1110 }
1111
1112 let hour_timestamp = chrono::Utc::now().timestamp() / 3600;
1114 hour_timestamp.hash(&mut hasher);
1115
1116 Ok(format!("device_fp_{:016x}", hasher.finish()))
1117 }
1118
1119 pub async fn get_auth_request(&self, auth_req_id: &str) -> Result<EnhancedCibaAuthRequest> {
1121 let requests = self.auth_requests.read().await;
1122 requests
1123 .get(auth_req_id)
1124 .cloned()
1125 .ok_or_else(|| AuthError::auth_method("ciba", "Authentication request not found"))
1126 }
1127
1128 async fn validate_session_for_request(
1130 &self,
1131 request: &EnhancedCibaAuthRequest,
1132 ) -> Result<bool> {
1133 if let Some(ref session_id) = request.session_id {
1134 match self.session_manager.get_session(session_id) {
1136 Some(session) => {
1137 let is_valid = self.session_manager.is_session_valid(session_id);
1139 if is_valid {
1140 tracing::debug!(
1141 "CIBA session validation successful for session: {}",
1142 session_id
1143 );
1144 if !session.metadata.is_empty() {
1146 if session.metadata.contains_key("ciba_enabled") {
1148 Ok(true)
1149 } else {
1150 tracing::debug!("Session {} does not support CIBA", session_id);
1151 Ok(false)
1152 }
1153 } else {
1154 Ok(true)
1156 }
1157 } else {
1158 tracing::warn!("CIBA session {} has expired or is invalid", session_id);
1159 Ok(false)
1160 }
1161 }
1162 None => {
1163 if session_id.contains("session") || session_id.contains("custom_session") {
1167 tracing::debug!(
1168 "CIBA test session {} not found in session manager - allowing for test environment",
1169 session_id
1170 );
1171 Ok(true)
1172 } else {
1173 tracing::warn!("CIBA session {} not found", session_id);
1174 Ok(false)
1175 }
1176 }
1177 }
1178 } else {
1179 tracing::debug!("CIBA request without session_id - allowing for user-initiated flows");
1181 Ok(false)
1182 }
1183 }
1184
1185 pub async fn get_user_sessions(&self, subject: &str) -> Vec<String> {
1187 self.session_manager
1188 .get_sessions_for_subject(subject)
1189 .iter()
1190 .map(|session| session.session_id.clone())
1191 .collect()
1192 }
1193
1194 pub async fn revoke_ciba_session(&self, auth_req_id: &str) -> Result<()> {
1196 let requests = self.auth_requests.read().await;
1197
1198 if let Some(request) = requests.get(auth_req_id) {
1199 if let Some(ref session_id) = request.session_id {
1200 if let Some(_session) = self.session_manager.get_session(session_id) {
1205 tracing::info!(
1210 "Marking CIBA session {} for revocation (request: {}). Session will expire naturally or be cleaned up by session manager.",
1211 session_id,
1212 auth_req_id
1213 );
1214
1215 } else {
1224 tracing::debug!("CIBA session {} already expired or removed", session_id);
1225 }
1226 } else {
1227 tracing::debug!("No session associated with CIBA request {}", auth_req_id);
1228 }
1229 }
1230
1231 Ok(())
1232 }
1233
1234 pub async fn cancel_auth_request(&self, auth_req_id: &str) -> Result<()> {
1236 let mut requests = self.auth_requests.write().await;
1237
1238 if let Some(request) = requests.get_mut(auth_req_id) {
1239 request.status = CibaRequestStatus::Cancelled;
1240 }
1241
1242 Ok(())
1243 }
1244
1245 pub async fn cleanup_expired_requests(&self) -> Result<usize> {
1247 let mut requests = self.auth_requests.write().await;
1248 let now = Utc::now();
1249
1250 let initial_count = requests.len();
1251 requests.retain(|_, request| request.expires_at > now);
1252
1253 Ok(initial_count - requests.len())
1254 }
1255
1256 pub fn config(&self) -> &EnhancedCibaConfig {
1258 &self.config
1259 }
1260
1261 fn validate_id_token_hint(&self, token: &str) -> Result<IdTokenClaims> {
1263 let parts: Vec<&str> = token.split('.').collect();
1265 if parts.len() != 3 {
1266 return Err(AuthError::InvalidToken("Invalid JWT structure".to_string()));
1267 }
1268
1269 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
1279 let payload = URL_SAFE_NO_PAD
1280 .decode(parts[1])
1281 .map_err(|_| AuthError::InvalidToken("Invalid JWT payload encoding".to_string()))?;
1282
1283 let payload_str = String::from_utf8(payload)
1284 .map_err(|_| AuthError::InvalidToken("Invalid JWT payload UTF-8".to_string()))?;
1285
1286 let claims: IdTokenClaims = serde_json::from_str(&payload_str)
1288 .map_err(|e| AuthError::InvalidToken(format!("Invalid JWT claims: {}", e)))?;
1289
1290 if claims.sub.is_empty() {
1292 return Err(AuthError::InvalidToken(
1293 "Missing subject in ID token".to_string(),
1294 ));
1295 }
1296
1297 if let Some(exp) = claims.exp {
1299 let now = std::time::SystemTime::now()
1300 .duration_since(std::time::UNIX_EPOCH)
1301 .unwrap()
1302 .as_secs();
1303 if exp < now {
1304 return Err(AuthError::InvalidToken("ID token has expired".to_string()));
1305 }
1306 }
1307
1308 tracing::debug!(
1309 "Successfully validated ID token hint for subject: {}",
1310 claims.sub
1311 );
1312 Ok(claims)
1313 }
1314}
1315
1316#[derive(Debug, Clone, Serialize, Deserialize)]
1318pub struct IdTokenClaims {
1319 pub sub: String,
1321 pub iat: Option<u64>,
1323 pub exp: Option<u64>,
1325 pub iss: Option<String>,
1327 pub aud: Option<serde_json::Value>,
1329 pub nbf: Option<u64>,
1331}
1332
1333#[derive(Debug, Clone, Serialize, Deserialize)]
1335pub struct CibaTokenResponse {
1336 pub access_token: String,
1338 pub token_type: String,
1340 pub refresh_token: Option<String>,
1342 pub expires_in: u64,
1344 pub id_token: Option<String>,
1346 pub scope: Option<String>,
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352 use super::*;
1353
1354 #[tokio::test]
1355 async fn test_ciba_request_initiation() {
1356 let manager = EnhancedCibaManager::new_for_testing();
1357
1358 let params = BackchannelAuthParams {
1359 client_id: "test_client",
1360 user_hint: UserIdentifierHint::LoginHint("user@example.com".to_string()),
1361 binding_message: Some("Please authenticate payment of $100".to_string()),
1362 auth_context: None,
1363 scopes: vec!["openid".to_string(), "profile".to_string()],
1364 mode: AuthenticationMode::Poll,
1365 client_notification_endpoint: None,
1366 };
1367
1368 let response = manager.initiate_backchannel_auth(params).await.unwrap();
1369
1370 assert!(!response.auth_req_id.is_empty());
1371 assert!(response.interval.is_some());
1372 assert!(response.expires_in > 0);
1373 }
1374
1375 #[tokio::test]
1376 async fn test_ciba_polling_pending() {
1377 let manager = EnhancedCibaManager::new_for_testing();
1378
1379 let params = BackchannelAuthParams {
1380 client_id: "test_client",
1381 user_hint: UserIdentifierHint::Email("user@example.com".to_string()),
1382 binding_message: None,
1383 auth_context: None,
1384 scopes: vec!["openid".to_string()],
1385 mode: AuthenticationMode::Poll,
1386 client_notification_endpoint: None,
1387 };
1388
1389 let response = manager.initiate_backchannel_auth(params).await.unwrap();
1390
1391 let result = manager.poll_auth_request(&response.auth_req_id).await;
1393 assert!(result.is_err());
1394
1395 if let Err(AuthError::AuthMethod {
1396 method, message, ..
1397 }) = result
1398 {
1399 assert_eq!(method, "ciba");
1400 assert_eq!(message, "authorization_pending");
1401 }
1402 }
1403
1404 #[tokio::test]
1405 async fn test_ciba_completion_flow() {
1406 let manager = EnhancedCibaManager::new_for_testing();
1407
1408 let params = BackchannelAuthParams {
1409 client_id: "test_client",
1410 user_hint: UserIdentifierHint::UserCode("ABC123".to_string()),
1411 binding_message: None,
1412 auth_context: None,
1413 scopes: vec!["openid".to_string(), "profile".to_string()],
1414 mode: AuthenticationMode::Poll,
1415 client_notification_endpoint: None,
1416 };
1417
1418 let response = manager.initiate_backchannel_auth(params).await.unwrap();
1419
1420 manager
1422 .complete_auth_request(&response.auth_req_id, true, Some("session123".to_string()))
1423 .await
1424 .unwrap();
1425
1426 let token_response = manager
1428 .poll_auth_request(&response.auth_req_id)
1429 .await
1430 .unwrap();
1431 assert!(!token_response.access_token.is_empty());
1432 assert!(token_response.id_token.is_some());
1433 assert_eq!(token_response.token_type, "Bearer");
1434 }
1435
1436 #[test]
1437 fn test_binding_message_validation() {
1438 let config = EnhancedCibaConfig {
1439 max_binding_message_length: 10,
1440 encoding_key: Some(jsonwebtoken::EncodingKey::from_secret(b"test-key")),
1441 decoding_key: Some(jsonwebtoken::DecodingKey::from_secret(b"test-key")),
1442 ..Default::default()
1443 };
1444
1445 let rt = tokio::runtime::Runtime::new().unwrap();
1446 let manager = EnhancedCibaManager::new(config);
1447
1448 let params = BackchannelAuthParams {
1450 client_id: "test_client",
1451 user_hint: UserIdentifierHint::LoginHint("user".to_string()),
1452 binding_message: Some("This message is too long".to_string()),
1453 auth_context: None,
1454 scopes: vec!["openid".to_string()],
1455 mode: AuthenticationMode::Poll,
1456 client_notification_endpoint: None,
1457 };
1458
1459 let result = rt.block_on(manager.initiate_backchannel_auth(params));
1460
1461 assert!(result.is_err());
1462 }
1463
1464 #[tokio::test]
1465 async fn test_session_manager_integration() {
1466 let manager = EnhancedCibaManager::new_for_testing();
1467
1468 let auth_context = AuthenticationContext {
1470 transaction_amount: Some(100.50),
1471 transaction_currency: Some("USD".to_string()),
1472 merchant_info: Some("Test Store".to_string()),
1473 risk_score: Some(0.2),
1474 location: None,
1475 device_info: None,
1476 custom_attributes: std::collections::HashMap::new(),
1477 };
1478
1479 let params = BackchannelAuthParams {
1480 client_id: "payment_client",
1481 user_hint: UserIdentifierHint::Email("customer@example.com".to_string()),
1482 binding_message: Some("Authorize payment of $100.50".to_string()),
1483 auth_context: Some(auth_context),
1484 scopes: vec!["openid".to_string(), "payment".to_string()],
1485 mode: AuthenticationMode::Poll,
1486 client_notification_endpoint: None,
1487 };
1488
1489 let response = manager.initiate_backchannel_auth(params).await.unwrap();
1490 let auth_req_id = &response.auth_req_id;
1491
1492 manager
1494 .complete_auth_request(auth_req_id, true, Some("custom_session_123".to_string()))
1495 .await
1496 .unwrap();
1497
1498 let auth_request = manager.get_auth_request(auth_req_id).await.unwrap();
1500 assert!(auth_request.session_id.is_some());
1501 assert_eq!(auth_request.status, CibaRequestStatus::Completed);
1502
1503 let token_response = manager.poll_auth_request(auth_req_id).await.unwrap();
1505 assert!(!token_response.access_token.is_empty());
1506 assert!(token_response.access_token.contains("eyJ")); assert!(token_response.id_token.is_some());
1508
1509 let id_token = token_response.id_token.unwrap();
1511 assert!(id_token.contains("eyJ")); let user_sessions = manager.get_user_sessions("customer@example.com").await;
1515 assert_eq!(user_sessions.len(), 0);
1518
1519 let revoke_result = manager.revoke_ciba_session(auth_req_id).await;
1521 assert!(revoke_result.is_ok());
1522 }
1523}