1use async_trait::async_trait;
18use chrono::{DateTime, Duration, Utc};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use std::collections::HashMap;
22use thiserror::Error;
23use tracing::{debug, info, warn};
24
25use uvb_core::{TenantId, UserId};
26
27#[derive(Debug, Error)]
29pub enum DeviceBindingError {
30 #[error("Storage error: {0}")]
31 Storage(String),
32
33 #[error("Device not found: {0}")]
34 DeviceNotFound(String),
35
36 #[error("Device trust expired (expired at: {0})")]
37 TrustExpired(DateTime<Utc>),
38
39 #[error("Device requires re-authentication (last auth: {0})")]
40 ReauthRequired(DateTime<Utc>),
41
42 #[error("Device is revoked (reason: {0})")]
43 DeviceRevoked(String),
44
45 #[error("MFA required for device registration")]
46 MfaRequired,
47
48 #[error("Operation {0} requires MFA even on trusted devices")]
49 SensitiveOperationBlocked(String),
50
51 #[error("Risk score too high for trusted device: {score} (threshold: {threshold})")]
52 RiskTooHigh { score: u8, threshold: u8 },
53
54 #[error("Device limit reached: {current} (max: {max})")]
55 DeviceLimitReached { current: usize, max: usize },
56
57 #[error("Invalid device fingerprint")]
58 InvalidFingerprint,
59
60 #[error("Device trust not established")]
61 NotTrusted,
62}
63
64#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
66pub enum DeviceType {
67 Desktop,
68 Mobile,
69 Tablet,
70 Unknown,
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
75#[allow(non_camel_case_types)]
76pub enum DevicePlatform {
77 Windows,
78 MacOS,
79 Linux,
80 iOS,
81 Android,
82 ChromeOS,
83 Unknown(String),
84}
85
86impl DevicePlatform {
87 pub fn from_user_agent(user_agent: &str) -> Self {
89 let ua_lower = user_agent.to_lowercase();
90
91 if ua_lower.contains("windows") {
92 Self::Windows
93 } else if ua_lower.contains("mac os") || ua_lower.contains("macos") {
94 Self::MacOS
95 } else if ua_lower.contains("linux") && !ua_lower.contains("android") {
96 Self::Linux
97 } else if ua_lower.contains("iphone") || ua_lower.contains("ipad") {
98 Self::iOS
99 } else if ua_lower.contains("android") {
100 Self::Android
101 } else if ua_lower.contains("cros") {
102 Self::ChromeOS
103 } else {
104 Self::Unknown(user_agent.to_string())
105 }
106 }
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
111pub enum BrowserType {
112 Chrome,
113 Firefox,
114 Safari,
115 Edge,
116 Opera,
117 Brave,
118 Unknown(String),
119}
120
121impl BrowserType {
122 pub fn from_user_agent(user_agent: &str) -> Self {
124 let ua_lower = user_agent.to_lowercase();
125
126 if ua_lower.contains("edg/") || ua_lower.contains("edge/") {
127 Self::Edge
128 } else if ua_lower.contains("brave") {
129 Self::Brave
130 } else if ua_lower.contains("opr/") || ua_lower.contains("opera") {
131 Self::Opera
132 } else if ua_lower.contains("chrome") || ua_lower.contains("crios") {
133 Self::Chrome
134 } else if ua_lower.contains("firefox") || ua_lower.contains("fxios") {
135 Self::Firefox
136 } else if ua_lower.contains("safari") {
137 Self::Safari
138 } else {
139 Self::Unknown(user_agent.to_string())
140 }
141 }
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
146pub struct DeviceFingerprint {
147 pub user_agent: String,
149
150 pub platform: DevicePlatform,
152
153 pub browser: BrowserType,
155
156 pub screen_resolution: Option<String>,
158
159 pub timezone_offset: Option<i32>,
161
162 pub language: Option<String>,
164
165 pub hardware_concurrency: Option<u32>,
167
168 pub webgl_vendor: Option<String>,
170
171 pub webgl_renderer: Option<String>,
173
174 pub canvas_hash: Option<String>,
176
177 pub fonts_hash: Option<String>,
179
180 pub plugins: Vec<String>,
182
183 pub touch_support: bool,
185
186 pub color_depth: Option<u8>,
188}
189
190impl DeviceFingerprint {
191 pub fn generate_device_id(&self) -> String {
193 let mut hasher = Sha256::new();
194
195 hasher.update(self.user_agent.as_bytes());
197 if let Some(ref res) = self.screen_resolution {
198 hasher.update(res.as_bytes());
199 }
200 if let Some(ref webgl_vendor) = self.webgl_vendor {
201 hasher.update(webgl_vendor.as_bytes());
202 }
203 if let Some(ref webgl_renderer) = self.webgl_renderer {
204 hasher.update(webgl_renderer.as_bytes());
205 }
206 if let Some(ref canvas) = self.canvas_hash {
207 hasher.update(canvas.as_bytes());
208 }
209 if let Some(ref fonts) = self.fonts_hash {
210 hasher.update(fonts.as_bytes());
211 }
212
213 hex::encode(hasher.finalize())
214 }
215
216 pub fn similarity(&self, other: &DeviceFingerprint) -> f64 {
218 let mut matches = 0;
219 let mut total = 0;
220
221 total += 1;
223 if self.user_agent == other.user_agent {
224 matches += 1;
225 }
226
227 if self.screen_resolution.is_some() && other.screen_resolution.is_some() {
229 total += 1;
230 if self.screen_resolution == other.screen_resolution {
231 matches += 1;
232 }
233 }
234
235 if self.webgl_vendor.is_some() && other.webgl_vendor.is_some() {
237 total += 1;
238 if self.webgl_vendor == other.webgl_vendor {
239 matches += 1;
240 }
241 }
242
243 if self.webgl_renderer.is_some() && other.webgl_renderer.is_some() {
244 total += 1;
245 if self.webgl_renderer == other.webgl_renderer {
246 matches += 1;
247 }
248 }
249
250 if self.canvas_hash.is_some() && other.canvas_hash.is_some() {
252 total += 2; if self.canvas_hash == other.canvas_hash {
254 matches += 2;
255 }
256 }
257
258 if self.fonts_hash.is_some() && other.fonts_hash.is_some() {
260 total += 1;
261 if self.fonts_hash == other.fonts_hash {
262 matches += 1;
263 }
264 }
265
266 if self.hardware_concurrency.is_some() && other.hardware_concurrency.is_some() {
268 total += 1;
269 if self.hardware_concurrency == other.hardware_concurrency {
270 matches += 1;
271 }
272 }
273
274 if total == 0 {
275 return 0.0;
276 }
277
278 matches as f64 / total as f64
279 }
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize)]
284pub struct TrustedDevice {
285 pub device_id: String,
287
288 pub user_id: UserId,
290
291 pub tenant_id: TenantId,
293
294 pub fingerprint: DeviceFingerprint,
296
297 pub device_type: DeviceType,
299
300 pub device_name: Option<String>,
302
303 pub registered_at: DateTime<Utc>,
305
306 pub expires_at: DateTime<Utc>,
308
309 pub last_auth_at: DateTime<Utc>,
311
312 pub last_ip: Option<String>,
314
315 pub last_location: Option<String>,
317
318 pub is_trusted: bool,
320
321 pub revoked_reason: Option<String>,
323
324 pub revoked_at: Option<DateTime<Utc>>,
326
327 pub usage_count: u64,
329
330 pub risk_score: u8,
332}
333
334impl TrustedDevice {
335 pub fn is_expired(&self) -> bool {
337 Utc::now() > self.expires_at
338 }
339
340 pub fn requires_reauth(&self, reauth_interval: Duration) -> bool {
342 let next_auth_time = self.last_auth_at + reauth_interval;
343 Utc::now() > next_auth_time
344 }
345
346 pub fn is_revoked(&self) -> bool {
348 self.revoked_reason.is_some()
349 }
350
351 pub fn is_valid(&self) -> Result<(), DeviceBindingError> {
353 if self.is_revoked() {
354 return Err(DeviceBindingError::DeviceRevoked(
355 self.revoked_reason.clone().unwrap_or_default(),
356 ));
357 }
358
359 if self.is_expired() {
360 return Err(DeviceBindingError::TrustExpired(self.expires_at));
361 }
362
363 if !self.is_trusted {
364 return Err(DeviceBindingError::NotTrusted);
365 }
366
367 Ok(())
368 }
369
370 pub fn age_days(&self) -> i64 {
372 (Utc::now() - self.registered_at).num_days()
373 }
374}
375
376#[derive(Clone, Debug, Serialize, Deserialize)]
378pub struct DeviceBindingConfig {
379 pub enabled: bool,
381
382 pub trust_expiration_days: i64,
384
385 pub reauth_interval_days: i64,
387
388 pub max_devices_per_user: usize,
390
391 pub risk_threshold: u8,
393
394 pub require_mfa_for_registration: bool,
396
397 pub auto_revoke_on_suspicion: bool,
399
400 pub sensitive_operations: Vec<String>,
402
403 pub min_fingerprint_similarity: f64,
405
406 pub enable_location_risk: bool,
408
409 pub enable_ip_risk: bool,
411}
412
413impl DeviceBindingConfig {
414 pub fn new_default() -> Self {
416 Self {
417 enabled: true,
418 trust_expiration_days: 30,
419 reauth_interval_days: 7,
420 max_devices_per_user: 10,
421 risk_threshold: 70,
422 require_mfa_for_registration: true,
423 auto_revoke_on_suspicion: true,
424 sensitive_operations: vec![
425 "change_password".to_string(),
426 "change_email".to_string(),
427 "change_phone".to_string(),
428 "add_payment_method".to_string(),
429 "delete_account".to_string(),
430 "change_mfa_settings".to_string(),
431 "transfer_funds".to_string(),
432 ],
433 min_fingerprint_similarity: 0.8,
434 enable_location_risk: true,
435 enable_ip_risk: true,
436 }
437 }
438
439 pub fn strict() -> Self {
441 let mut config = Self::new_default();
442 config.trust_expiration_days = 14; config.reauth_interval_days = 3; config.max_devices_per_user = 5;
445 config.risk_threshold = 50; config.min_fingerprint_similarity = 0.9; config
448 }
449
450 pub fn lenient() -> Self {
452 let mut config = Self::new_default();
453 config.trust_expiration_days = 365; config.reauth_interval_days = 30;
455 config.max_devices_per_user = 50;
456 config.risk_threshold = 90; config.require_mfa_for_registration = false;
458 config.auto_revoke_on_suspicion = false;
459 config.sensitive_operations = vec![]; config.min_fingerprint_similarity = 0.5;
461 config.enable_location_risk = false;
462 config.enable_ip_risk = false;
463 config
464 }
465
466 pub fn is_sensitive_operation(&self, operation: &str) -> bool {
468 self.sensitive_operations.iter().any(|op| op == operation)
469 }
470}
471
472#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct DeviceRegistrationRequest {
475 pub user_id: UserId,
476 pub tenant_id: TenantId,
477 pub fingerprint: DeviceFingerprint,
478 pub device_type: DeviceType,
479 pub device_name: Option<String>,
480 pub ip_address: Option<String>,
481 pub location: Option<String>,
482 pub mfa_verified: bool,
483}
484
485#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct DeviceTrustResult {
488 pub device_id: String,
489 pub is_trusted: bool,
490 pub requires_mfa: bool,
491 pub reason: String,
492 pub risk_score: u8,
493 pub expires_at: Option<DateTime<Utc>>,
494}
495
496#[async_trait]
498pub trait DeviceBindingStorage: Send + Sync {
499 async fn save_device(&self, device: &TrustedDevice) -> Result<(), DeviceBindingError>;
501
502 async fn get_device(&self, device_id: &str) -> Result<TrustedDevice, DeviceBindingError>;
504
505 async fn get_user_devices(
507 &self,
508 user_id: &UserId,
509 ) -> Result<Vec<TrustedDevice>, DeviceBindingError>;
510
511 async fn update_last_auth(
513 &self,
514 device_id: &str,
515 timestamp: DateTime<Utc>,
516 ) -> Result<(), DeviceBindingError>;
517
518 async fn revoke_device(
520 &self,
521 device_id: &str,
522 reason: String,
523 revoked_at: DateTime<Utc>,
524 ) -> Result<(), DeviceBindingError>;
525
526 async fn delete_device(&self, device_id: &str) -> Result<(), DeviceBindingError>;
528
529 async fn find_similar_devices(
531 &self,
532 user_id: &UserId,
533 fingerprint: &DeviceFingerprint,
534 min_similarity: f64,
535 ) -> Result<Vec<TrustedDevice>, DeviceBindingError>;
536}
537
538pub struct InMemoryDeviceStorage {
540 devices: tokio::sync::RwLock<HashMap<String, TrustedDevice>>,
541}
542
543impl InMemoryDeviceStorage {
544 pub fn new() -> Self {
545 Self {
546 devices: tokio::sync::RwLock::new(HashMap::new()),
547 }
548 }
549}
550
551impl Default for InMemoryDeviceStorage {
552 fn default() -> Self {
553 Self::new()
554 }
555}
556
557#[async_trait]
558impl DeviceBindingStorage for InMemoryDeviceStorage {
559 async fn save_device(&self, device: &TrustedDevice) -> Result<(), DeviceBindingError> {
560 let mut devices = self.devices.write().await;
561 devices.insert(device.device_id.clone(), device.clone());
562 Ok(())
563 }
564
565 async fn get_device(&self, device_id: &str) -> Result<TrustedDevice, DeviceBindingError> {
566 let devices = self.devices.read().await;
567 devices
568 .get(device_id)
569 .cloned()
570 .ok_or_else(|| DeviceBindingError::DeviceNotFound(device_id.to_string()))
571 }
572
573 async fn get_user_devices(
574 &self,
575 user_id: &UserId,
576 ) -> Result<Vec<TrustedDevice>, DeviceBindingError> {
577 let devices = self.devices.read().await;
578 Ok(devices
579 .values()
580 .filter(|d| d.user_id == *user_id)
581 .cloned()
582 .collect())
583 }
584
585 async fn update_last_auth(
586 &self,
587 device_id: &str,
588 timestamp: DateTime<Utc>,
589 ) -> Result<(), DeviceBindingError> {
590 let mut devices = self.devices.write().await;
591 if let Some(device) = devices.get_mut(device_id) {
592 device.last_auth_at = timestamp;
593 device.usage_count += 1;
594 Ok(())
595 } else {
596 Err(DeviceBindingError::DeviceNotFound(device_id.to_string()))
597 }
598 }
599
600 async fn revoke_device(
601 &self,
602 device_id: &str,
603 reason: String,
604 revoked_at: DateTime<Utc>,
605 ) -> Result<(), DeviceBindingError> {
606 let mut devices = self.devices.write().await;
607 if let Some(device) = devices.get_mut(device_id) {
608 device.is_trusted = false;
609 device.revoked_reason = Some(reason);
610 device.revoked_at = Some(revoked_at);
611 Ok(())
612 } else {
613 Err(DeviceBindingError::DeviceNotFound(device_id.to_string()))
614 }
615 }
616
617 async fn delete_device(&self, device_id: &str) -> Result<(), DeviceBindingError> {
618 let mut devices = self.devices.write().await;
619 devices
620 .remove(device_id)
621 .ok_or_else(|| DeviceBindingError::DeviceNotFound(device_id.to_string()))?;
622 Ok(())
623 }
624
625 async fn find_similar_devices(
626 &self,
627 user_id: &UserId,
628 fingerprint: &DeviceFingerprint,
629 min_similarity: f64,
630 ) -> Result<Vec<TrustedDevice>, DeviceBindingError> {
631 let devices = self.devices.read().await;
632 Ok(devices
633 .values()
634 .filter(|d| {
635 d.user_id == *user_id && fingerprint.similarity(&d.fingerprint) >= min_similarity
636 })
637 .cloned()
638 .collect())
639 }
640}
641
642pub struct DeviceBindingManager<S: DeviceBindingStorage> {
644 storage: S,
645 config: DeviceBindingConfig,
646}
647
648impl<S: DeviceBindingStorage> DeviceBindingManager<S> {
649 pub fn new(storage: S, config: DeviceBindingConfig) -> Self {
651 Self { storage, config }
652 }
653
654 pub async fn register_device(
656 &self,
657 request: DeviceRegistrationRequest,
658 ) -> Result<TrustedDevice, DeviceBindingError> {
659 if !self.config.enabled {
660 return Err(DeviceBindingError::Storage(
661 "Device binding disabled".to_string(),
662 ));
663 }
664
665 if self.config.require_mfa_for_registration && !request.mfa_verified {
667 return Err(DeviceBindingError::MfaRequired);
668 }
669
670 let existing_devices = self.storage.get_user_devices(&request.user_id).await?;
672 if existing_devices.len() >= self.config.max_devices_per_user {
673 return Err(DeviceBindingError::DeviceLimitReached {
674 current: existing_devices.len(),
675 max: self.config.max_devices_per_user,
676 });
677 }
678
679 let device_id = request.fingerprint.generate_device_id();
681
682 let expires_at = Utc::now() + Duration::days(self.config.trust_expiration_days);
684
685 let device = TrustedDevice {
686 device_id: device_id.clone(),
687 user_id: request.user_id,
688 tenant_id: request.tenant_id,
689 fingerprint: request.fingerprint,
690 device_type: request.device_type,
691 device_name: request.device_name,
692 registered_at: Utc::now(),
693 expires_at,
694 last_auth_at: Utc::now(),
695 last_ip: request.ip_address,
696 last_location: request.location,
697 is_trusted: true,
698 revoked_reason: None,
699 revoked_at: None,
700 usage_count: 0,
701 risk_score: 0, };
703
704 self.storage.save_device(&device).await?;
705
706 info!(
707 "Registered new device {} for user {:?}",
708 device_id, device.user_id
709 );
710
711 Ok(device)
712 }
713
714 pub async fn validate_device_trust(
716 &self,
717 device_id: &str,
718 operation: &str,
719 ) -> Result<DeviceTrustResult, DeviceBindingError> {
720 if !self.config.enabled {
721 return Ok(DeviceTrustResult {
722 device_id: device_id.to_string(),
723 is_trusted: false,
724 requires_mfa: true,
725 reason: "Device binding disabled".to_string(),
726 risk_score: 0,
727 expires_at: None,
728 });
729 }
730
731 if self.config.is_sensitive_operation(operation) {
733 return Ok(DeviceTrustResult {
734 device_id: device_id.to_string(),
735 is_trusted: false,
736 requires_mfa: true,
737 reason: format!("Sensitive operation '{}' requires MFA", operation),
738 risk_score: 100,
739 expires_at: None,
740 });
741 }
742
743 let device = self.storage.get_device(device_id).await?;
744
745 if let Err(e) = device.is_valid() {
747 return Ok(DeviceTrustResult {
748 device_id: device_id.to_string(),
749 is_trusted: false,
750 requires_mfa: true,
751 reason: e.to_string(),
752 risk_score: 100,
753 expires_at: Some(device.expires_at),
754 });
755 }
756
757 let reauth_interval = Duration::days(self.config.reauth_interval_days);
759 if device.requires_reauth(reauth_interval) {
760 return Ok(DeviceTrustResult {
761 device_id: device_id.to_string(),
762 is_trusted: true, requires_mfa: true,
764 reason: "Periodic re-authentication required".to_string(),
765 risk_score: device.risk_score,
766 expires_at: Some(device.expires_at),
767 });
768 }
769
770 if device.risk_score > self.config.risk_threshold {
772 warn!(
773 "Device {} risk score {} exceeds threshold {}",
774 device_id, device.risk_score, self.config.risk_threshold
775 );
776
777 if self.config.auto_revoke_on_suspicion {
778 self.storage
779 .revoke_device(
780 device_id,
781 format!(
782 "Automatic revocation due to high risk score: {}",
783 device.risk_score
784 ),
785 Utc::now(),
786 )
787 .await?;
788
789 return Ok(DeviceTrustResult {
790 device_id: device_id.to_string(),
791 is_trusted: false,
792 requires_mfa: true,
793 reason: "Device revoked due to high risk".to_string(),
794 risk_score: device.risk_score,
795 expires_at: None,
796 });
797 }
798
799 return Ok(DeviceTrustResult {
800 device_id: device_id.to_string(),
801 is_trusted: false,
802 requires_mfa: true,
803 reason: format!(
804 "Risk score {} exceeds threshold {}",
805 device.risk_score, self.config.risk_threshold
806 ),
807 risk_score: device.risk_score,
808 expires_at: Some(device.expires_at),
809 });
810 }
811
812 Ok(DeviceTrustResult {
814 device_id: device_id.to_string(),
815 is_trusted: true,
816 requires_mfa: false,
817 reason: "Device is trusted".to_string(),
818 risk_score: device.risk_score,
819 expires_at: Some(device.expires_at),
820 })
821 }
822
823 pub async fn record_authentication(&self, device_id: &str) -> Result<(), DeviceBindingError> {
825 self.storage.update_last_auth(device_id, Utc::now()).await?;
826 debug!("Recorded authentication for device {}", device_id);
827 Ok(())
828 }
829
830 pub async fn revoke_device(
832 &self,
833 device_id: &str,
834 reason: String,
835 ) -> Result<(), DeviceBindingError> {
836 self.storage
837 .revoke_device(device_id, reason.clone(), Utc::now())
838 .await?;
839 info!("Revoked device {}: {}", device_id, reason);
840 Ok(())
841 }
842
843 pub async fn delete_device(&self, device_id: &str) -> Result<(), DeviceBindingError> {
845 self.storage.delete_device(device_id).await?;
846 info!("Deleted device {}", device_id);
847 Ok(())
848 }
849
850 pub async fn get_user_devices(
852 &self,
853 user_id: &UserId,
854 ) -> Result<Vec<TrustedDevice>, DeviceBindingError> {
855 self.storage.get_user_devices(user_id).await
856 }
857
858 pub async fn find_device_by_fingerprint(
860 &self,
861 user_id: &UserId,
862 fingerprint: &DeviceFingerprint,
863 ) -> Result<Option<TrustedDevice>, DeviceBindingError> {
864 let similar = self
865 .storage
866 .find_similar_devices(user_id, fingerprint, self.config.min_fingerprint_similarity)
867 .await?;
868
869 Ok(similar.into_iter().next())
870 }
871
872 pub async fn cleanup_expired_devices(
874 &self,
875 user_id: &UserId,
876 ) -> Result<usize, DeviceBindingError> {
877 let devices = self.storage.get_user_devices(user_id).await?;
878 let mut removed = 0;
879
880 for device in devices {
881 if device.is_expired() {
882 self.storage.delete_device(&device.device_id).await?;
883 removed += 1;
884 }
885 }
886
887 if removed > 0 {
888 info!(
889 "Cleaned up {} expired devices for user {:?}",
890 removed, user_id
891 );
892 }
893
894 Ok(removed)
895 }
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 fn create_test_fingerprint() -> DeviceFingerprint {
903 DeviceFingerprint {
904 user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)".to_string(),
905 platform: DevicePlatform::MacOS,
906 browser: BrowserType::Chrome,
907 screen_resolution: Some("1920x1080".to_string()),
908 timezone_offset: Some(-480),
909 language: Some("en-US".to_string()),
910 hardware_concurrency: Some(8),
911 webgl_vendor: Some("Intel Inc.".to_string()),
912 webgl_renderer: Some("Intel Iris Pro".to_string()),
913 canvas_hash: Some("abc123".to_string()),
914 fonts_hash: Some("def456".to_string()),
915 plugins: vec![],
916 touch_support: false,
917 color_depth: Some(24),
918 }
919 }
920
921 #[test]
922 fn test_device_id_generation() {
923 let fp = create_test_fingerprint();
924 let id1 = fp.generate_device_id();
925 let id2 = fp.generate_device_id();
926
927 assert_eq!(id1, id2);
929 assert_eq!(id1.len(), 64); }
931
932 #[test]
933 fn test_fingerprint_similarity() {
934 let fp1 = create_test_fingerprint();
935 let mut fp2 = fp1.clone();
936
937 assert_eq!(fp1.similarity(&fp2), 1.0);
939
940 fp2.user_agent = "Different".to_string();
942 assert!(fp1.similarity(&fp2) < 1.0);
943 }
944
945 #[test]
946 fn test_platform_detection() {
947 assert_eq!(
948 DevicePlatform::from_user_agent("Mozilla/5.0 (Windows NT 10.0)"),
949 DevicePlatform::Windows
950 );
951 assert_eq!(
952 DevicePlatform::from_user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"),
953 DevicePlatform::MacOS
954 );
955 assert_eq!(
956 DevicePlatform::from_user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)"),
957 DevicePlatform::iOS
958 );
959 }
960
961 #[test]
962 fn test_browser_detection() {
963 assert_eq!(
964 BrowserType::from_user_agent("Chrome/91.0.4472.124"),
965 BrowserType::Chrome
966 );
967 assert_eq!(
968 BrowserType::from_user_agent("Firefox/89.0"),
969 BrowserType::Firefox
970 );
971 assert_eq!(
972 BrowserType::from_user_agent("Edg/91.0.864.59"),
973 BrowserType::Edge
974 );
975 }
976
977 #[test]
978 fn test_config_presets() {
979 let default_config = DeviceBindingConfig::new_default();
980 assert_eq!(default_config.trust_expiration_days, 30);
981 assert!(default_config.require_mfa_for_registration);
982
983 let strict_config = DeviceBindingConfig::strict();
984 assert_eq!(strict_config.trust_expiration_days, 14);
985 assert_eq!(strict_config.reauth_interval_days, 3);
986
987 let lenient_config = DeviceBindingConfig::lenient();
988 assert_eq!(lenient_config.trust_expiration_days, 365);
989 assert!(!lenient_config.require_mfa_for_registration);
990 }
991
992 #[test]
993 fn test_sensitive_operations() {
994 let config = DeviceBindingConfig::new_default();
995 assert!(config.is_sensitive_operation("change_password"));
996 assert!(config.is_sensitive_operation("delete_account"));
997 assert!(!config.is_sensitive_operation("view_profile"));
998 }
999
1000 #[tokio::test]
1001 async fn test_device_registration() {
1002 let storage = InMemoryDeviceStorage::new();
1003 let config = DeviceBindingConfig::new_default();
1004 let manager = DeviceBindingManager::new(storage, config);
1005
1006 let user_id = UserId::new("test_user");
1007 let tenant_id = TenantId::new("test_tenant");
1008 let fingerprint = create_test_fingerprint();
1009
1010 let request = DeviceRegistrationRequest {
1011 user_id: user_id.clone(),
1012 tenant_id,
1013 fingerprint,
1014 device_type: DeviceType::Desktop,
1015 device_name: Some("My Laptop".to_string()),
1016 ip_address: Some("192.0.2.1".to_string()),
1017 location: Some("San Francisco, CA".to_string()),
1018 mfa_verified: true,
1019 };
1020
1021 let device = manager.register_device(request).await.unwrap();
1022 assert_eq!(device.user_id, user_id);
1023 assert!(device.is_trusted);
1024 assert!(!device.is_expired());
1025 }
1026
1027 #[tokio::test]
1028 async fn test_device_limit_enforcement() {
1029 let storage = InMemoryDeviceStorage::new();
1030 let mut config = DeviceBindingConfig::new_default();
1031 config.max_devices_per_user = 2;
1032 let manager = DeviceBindingManager::new(storage, config);
1033
1034 let user_id = UserId::new("test_user");
1035
1036 for i in 0..2 {
1038 let mut fp = create_test_fingerprint();
1039 fp.user_agent = format!("Device {}", i);
1040
1041 let request = DeviceRegistrationRequest {
1042 user_id: user_id.clone(),
1043 tenant_id: TenantId::new("test_tenant"),
1044 fingerprint: fp,
1045 device_type: DeviceType::Desktop,
1046 device_name: Some(format!("Device {}", i)),
1047 ip_address: None,
1048 location: None,
1049 mfa_verified: true,
1050 };
1051
1052 manager.register_device(request).await.unwrap();
1053 }
1054
1055 let mut fp = create_test_fingerprint();
1057 fp.user_agent = "Device 3".to_string();
1058
1059 let request = DeviceRegistrationRequest {
1060 user_id,
1061 tenant_id: TenantId::new("test_tenant"),
1062 fingerprint: fp,
1063 device_type: DeviceType::Desktop,
1064 device_name: Some("Device 3".to_string()),
1065 ip_address: None,
1066 location: None,
1067 mfa_verified: true,
1068 };
1069
1070 let result = manager.register_device(request).await;
1071 assert!(matches!(
1072 result,
1073 Err(DeviceBindingError::DeviceLimitReached { .. })
1074 ));
1075 }
1076
1077 #[tokio::test]
1078 async fn test_sensitive_operation_blocks() {
1079 let storage = InMemoryDeviceStorage::new();
1080 let config = DeviceBindingConfig::new_default();
1081 let manager = DeviceBindingManager::new(storage, config);
1082
1083 let user_id = UserId::new("test_user");
1084 let fingerprint = create_test_fingerprint();
1085
1086 let request = DeviceRegistrationRequest {
1087 user_id,
1088 tenant_id: TenantId::new("test_tenant"),
1089 fingerprint,
1090 device_type: DeviceType::Desktop,
1091 device_name: Some("Test Device".to_string()),
1092 ip_address: None,
1093 location: None,
1094 mfa_verified: true,
1095 };
1096
1097 let device = manager.register_device(request).await.unwrap();
1098
1099 let result = manager
1101 .validate_device_trust(&device.device_id, "change_password")
1102 .await
1103 .unwrap();
1104
1105 assert!(result.requires_mfa);
1106 assert!(result.reason.contains("Sensitive operation"));
1107 }
1108}