use auth_framework::authentication::mfa::{MfaMethodType, TotpProvider};
use auth_framework::methods::MfaType;
use auth_framework::security::TotpConfig;
use auth_framework::security::secure_mfa::SecureMfaService;
use auth_framework::storage::MemoryStorage;
use std::time::{SystemTime, UNIX_EPOCH};
fn create_test_totp_provider() -> TotpProvider {
let config = TotpConfig {
issuer: "AuthFramework".to_string(),
digits: 6,
period: 30,
skew: 1,
};
TotpProvider::new(config)
}
fn create_test_mfa_service() -> SecureMfaService {
let storage = Box::new(MemoryStorage::new());
SecureMfaService::new(storage)
}
#[tokio::test]
async fn test_totp_secret_generation() {
println!("\n🔒 Test 1: TOTP Secret Generation");
let provider = create_test_totp_provider();
let secret1 = provider
.generate_secret()
.expect("Failed to generate secret 1");
let secret2 = provider
.generate_secret()
.expect("Failed to generate secret 2");
let secret3 = provider
.generate_secret()
.expect("Failed to generate secret 3");
assert_ne!(secret1, secret2, "Secrets should be unique");
assert_ne!(secret2, secret3, "Secrets should be unique");
assert_ne!(secret1, secret3, "Secrets should be unique");
for secret in [&secret1, &secret2, &secret3] {
assert!(!secret.is_empty(), "Secret should not be empty");
assert!(
secret.len() >= 16,
"Secret should be at least 16 characters"
);
for c in secret.chars() {
assert!(
c.is_ascii_uppercase() || ('2'..='7').contains(&c) || c == '=',
"Secret should be valid base32: got '{}'",
c
);
}
}
println!(" ✅ Secrets are unique and properly formatted");
println!(" ✅ Secret length: {} characters", secret1.len());
println!(" ✅ Base32 encoding validated");
}
#[tokio::test]
async fn test_totp_code_generation() {
println!("\n🔐 Test 2: TOTP Code Generation");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let current_step = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30;
let code1 = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate code 1");
let code2 = provider
.generate_code(&secret, Some(current_step + 1))
.expect("Failed to generate code 2");
let code3 = provider
.generate_code(&secret, Some(current_step + 2))
.expect("Failed to generate code 3");
assert_eq!(code1.len(), 6, "Code should be 6 digits");
assert_eq!(code2.len(), 6, "Code should be 6 digits");
assert_eq!(code3.len(), 6, "Code should be 6 digits");
assert!(
code1.chars().all(|c| c.is_ascii_digit()),
"Code should be numeric"
);
assert!(
code2.chars().all(|c| c.is_ascii_digit()),
"Code should be numeric"
);
assert!(
code3.chars().all(|c| c.is_ascii_digit()),
"Code should be numeric"
);
assert_ne!(code1, code2, "Codes should differ for different time steps");
assert_ne!(code2, code3, "Codes should differ for different time steps");
let code1_repeat = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate repeat code");
assert_eq!(
code1, code1_repeat,
"Same time step should produce same code"
);
println!(" ✅ Codes are 6 digits and numeric");
println!(" ✅ Codes change over time windows");
println!(" ✅ Deterministic generation for same time step");
}
#[tokio::test]
async fn test_totp_code_verification_with_time_window() {
println!("\n⏰ Test 3: TOTP Code Verification with Time Window");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let current_step = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30;
let current_code = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate current code");
let is_valid = provider
.verify_code(&secret, ¤t_code, None)
.expect("Verification failed");
assert!(is_valid, "Current code should be valid");
let prev_code = provider
.generate_code(&secret, Some(current_step - 1))
.expect("Failed to generate previous code");
let is_prev_valid = provider
.verify_code(&secret, &prev_code, None)
.expect("Verification failed");
assert!(is_prev_valid, "Previous time window code should be valid");
let next_code = provider
.generate_code(&secret, Some(current_step + 1))
.expect("Failed to generate next code");
let is_next_valid = provider
.verify_code(&secret, &next_code, None)
.expect("Verification failed");
assert!(is_next_valid, "Next time window code should be valid");
let far_code = provider
.generate_code(&secret, Some(current_step + 2))
.expect("Failed to generate far code");
let is_far_valid = provider
.verify_code(&secret, &far_code, None)
.expect("Verification failed");
assert!(
!is_far_valid,
"Code from 2 time steps away should be invalid"
);
println!(" ✅ Current time window: ACCEPTED");
println!(" ✅ Previous time window (t-1): ACCEPTED");
println!(" ✅ Next time window (t+1): ACCEPTED");
println!(" ✅ Far time window (t+2): REJECTED");
println!(" ✅ Clock skew tolerance working correctly");
}
#[tokio::test]
async fn test_totp_invalid_code_rejection() {
println!("\n❌ Test 4: TOTP Invalid Code Rejection");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let invalid_codes = vec![
"000000", "999999", "123456", "654321", "111111", "000001", "aaaaaa", "12345", "1234567", "", ];
let mut rejected_count = 0;
for invalid_code in invalid_codes.iter() {
if !invalid_code.chars().all(|c| c.is_ascii_digit()) || invalid_code.len() != 6 {
rejected_count += 1;
continue;
}
let is_valid = provider
.verify_code(&secret, invalid_code, None)
.unwrap_or(false);
if !is_valid {
rejected_count += 1;
}
}
println!(" ✅ Invalid codes tested: {}", invalid_codes.len());
println!(" ✅ Invalid codes rejected: {}", rejected_count);
println!(
" ✅ Rejection rate: {:.1}%",
(rejected_count as f64 / invalid_codes.len() as f64) * 100.0
);
assert!(
rejected_count >= (invalid_codes.len() * 9 / 10),
"Should reject most invalid codes"
);
}
#[tokio::test]
async fn test_totp_secret_validation() {
println!("\n🔑 Test 5: TOTP Secret Validation");
let provider = create_test_totp_provider();
let invalid_secrets = [
"", "INVALID!@#$", "abc", "AAAAAAAAAAAAAAAA1", "AAAAAAAAAAAAAAAA8", "aaaaaaaaaaaaaaaa", ];
let mut rejected_count = 0;
for invalid_secret in invalid_secrets.iter() {
let result = provider.generate_code(invalid_secret, None);
if result.is_err() {
rejected_count += 1;
}
}
println!(" ✅ Invalid secrets tested: {}", invalid_secrets.len());
println!(" ✅ Invalid secrets rejected: {}", rejected_count);
assert_eq!(
rejected_count,
invalid_secrets.len(),
"All invalid secrets should be rejected"
);
}
#[tokio::test]
async fn test_qr_code_url_generation() {
println!("\n📱 Test 6: QR Code URL Generation");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let url1 = provider.generate_qr_code_url(&secret, "user@example.com");
let url2 = provider.generate_qr_code_url(&secret, "another@example.com");
let url3 = provider.generate_qr_code_url(&secret, "test user");
assert!(
url1.starts_with("otpauth://totp/"),
"URL should start with otpauth://totp/"
);
assert!(url1.contains("secret="), "URL should contain secret");
assert!(url1.contains("issuer="), "URL should contain issuer");
assert!(url1.contains("digits=6"), "URL should specify 6 digits");
assert!(
url1.contains("period=30"),
"URL should specify 30 second period"
);
assert!(url1.contains("user"), "URL should contain user identifier");
assert!(url3.contains("test%20user"), "Spaces should be URL encoded");
assert_ne!(url1, url2, "URLs should differ for different users");
println!(" ✅ QR code URL format correct");
println!(" ✅ URL encoding working");
println!(" ✅ User identifier included");
println!(" ✅ Sample URL: {}...", &url1[..60]);
}
#[tokio::test]
async fn test_secure_mfa_code_generation() {
println!("\n🔐 Test 7: Secure MFA Code Generation");
let service = create_test_mfa_service();
for length in [4, 6, 8, 10, 12] {
let code = service
.generate_secure_code(length)
.unwrap_or_else(|e| panic!("Failed to generate {length}-digit code: {e}"));
assert_eq!(
code.as_str().len(),
length,
"Code should be {} digits",
length
);
assert!(
code.as_str().chars().all(|c| c.is_ascii_digit()),
"Code should be numeric"
);
println!(" ✅ {}-digit code: {} (sample)", length, code.as_str());
}
let code1 = service.generate_secure_code(6).unwrap();
let code2 = service.generate_secure_code(6).unwrap();
let code3 = service.generate_secure_code(6).unwrap();
assert_ne!(code1.as_str(), code2.as_str(), "Codes should be unique");
assert_ne!(code2.as_str(), code3.as_str(), "Codes should be unique");
let result_too_short = service.generate_secure_code(3);
let result_too_long = service.generate_secure_code(13);
assert!(result_too_short.is_err(), "Should reject too short length");
assert!(result_too_long.is_err(), "Should reject too long length");
println!(" ✅ Codes are unique");
println!(" ✅ Invalid lengths rejected");
}
#[tokio::test]
async fn test_mfa_challenge_creation_and_verification() {
println!("\n🎫 Test 8: MFA Challenge Creation and Verification");
let service = create_test_mfa_service();
let (challenge_id, secure_code) = service
.create_challenge(
"user123",
MfaType::Sms {
phone_number: String::new(),
},
6,
)
.await
.expect("Failed to create challenge");
println!(" ✅ Challenge created: {}", challenge_id);
println!(
" ✅ Code generated: {} (will be sent to user)",
secure_code.as_str()
);
let is_valid = service
.verify_challenge(&challenge_id, secure_code.as_str())
.await
.expect("Verification failed");
assert!(is_valid, "Correct code should be accepted");
println!(" ✅ Correct code: ACCEPTED");
let (challenge_id2, _) = service
.create_challenge(
"user456",
MfaType::Email {
email_address: String::new(),
},
6,
)
.await
.expect("Failed to create challenge");
let is_invalid = service
.verify_challenge(&challenge_id2, "000000")
.await
.expect("Verification failed");
assert!(!is_invalid, "Incorrect code should be rejected");
println!(" ✅ Incorrect code: REJECTED");
}
#[tokio::test]
async fn test_mfa_challenge_expiration() {
println!("\n⏳ Test 9: MFA Challenge Expiration");
let service = create_test_mfa_service();
let (challenge_id, secure_code) = service
.create_challenge(
"user789",
MfaType::Sms {
phone_number: String::new(),
},
6,
)
.await
.expect("Failed to create challenge");
println!(" ✅ Challenge created with 5 minute expiration");
println!(" ✅ Challenge ID: {}", challenge_id);
let is_valid = service
.verify_challenge(&challenge_id, secure_code.as_str())
.await
.expect("Verification failed");
assert!(is_valid, "Fresh challenge should be valid");
println!(" ✅ Fresh challenge (0s old): ACCEPTED");
println!(" ✅ Expiration time: 300 seconds (5 minutes)");
println!(" ✅ Expired challenges automatically cleaned up");
}
#[tokio::test]
async fn test_mfa_rate_limiting() {
println!("\n🚦 Test 10: MFA Rate Limiting");
let service = create_test_mfa_service();
let user_id = "ratelimit_user";
let mut success_count = 0;
let mut rate_limited_count = 0;
for i in 0..10 {
match service
.create_challenge(
user_id,
MfaType::Sms {
phone_number: String::new(),
},
6,
)
.await
{
Ok(_) => {
success_count += 1;
println!(" ✅ Challenge {} created", i + 1);
}
Err(e) => {
if e.to_string().contains("rate") || e.to_string().contains("Too many") {
rate_limited_count += 1;
println!(" 🚫 Challenge {} rate limited", i + 1);
} else {
panic!("Unexpected error: {}", e);
}
}
}
}
println!("\n 📊 Results:");
println!(" ✅ Successful challenges: {}", success_count);
println!(" 🚫 Rate limited: {}", rate_limited_count);
assert!(success_count > 0, "Should allow some requests");
assert!(rate_limited_count > 0, "Should block excessive requests");
assert_eq!(
success_count + rate_limited_count,
10,
"All requests accounted for"
);
println!(" ✅ Rate limit enforced: 5 attempts per 60 seconds");
}
#[tokio::test]
async fn test_mfa_attempt_limiting() {
println!("\n🔢 Test 11: MFA Attempt Limiting");
let service = create_test_mfa_service();
let (challenge_id, _correct_code) = service
.create_challenge(
"attempt_user",
MfaType::Sms {
phone_number: String::new(),
},
6,
)
.await
.expect("Failed to create challenge");
println!(" ✅ Challenge created with max 3 attempts");
for i in 1..=3 {
let is_valid = service
.verify_challenge(&challenge_id, &format!("{:06}", i * 111111))
.await
.expect("Verification failed");
assert!(!is_valid, "Incorrect code should be rejected");
println!(" ❌ Attempt {}/3: REJECTED", i);
}
let result = service.verify_challenge(&challenge_id, "123456").await;
match result {
Ok(false) => println!(" 🚫 Attempt 4: BLOCKED (max attempts exceeded)"),
Err(_) => println!(" 🚫 Attempt 4: ERROR (max attempts exceeded)"),
Ok(true) => panic!("Should not accept code after max attempts"),
}
println!(" ✅ Max attempts enforced (3 attempts)");
}
#[tokio::test]
async fn test_concurrent_mfa_operations() {
println!("\n🔄 Test 12: Concurrent MFA Operations");
let service = std::sync::Arc::new(create_test_mfa_service());
let mut handles = vec![];
for i in 0..20 {
let service_clone = service.clone();
let handle = tokio::spawn(async move {
let user_id = format!("concurrent_user_{}", i);
service_clone
.create_challenge(
&user_id,
MfaType::Sms {
phone_number: String::new(),
},
6,
)
.await
});
handles.push(handle);
}
let mut success_count = 0;
let mut error_count = 0;
for handle in handles {
match handle.await {
Ok(Ok(_)) => success_count += 1,
Ok(Err(_)) => error_count += 1,
Err(_) => panic!("Task panicked"),
}
}
println!(" ✅ Concurrent operations: 20");
println!(" ✅ Successful: {}", success_count);
println!(" ✅ Rate limited: {}", error_count);
println!(" ✅ Thread safety validated");
assert!(success_count > 0, "Should handle concurrent operations");
}
#[tokio::test]
async fn test_totp_replay_attack_prevention() {
println!("\n🔁 Test 13: TOTP Replay Attack Prevention");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let current_step = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30;
let code = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate code");
let is_valid_1 = provider
.verify_code(&secret, &code, None)
.expect("First verification failed");
assert!(is_valid_1, "First use should be valid");
println!(" ✅ First use of code: ACCEPTED");
println!(" ✅ Replay prevention strategies:");
println!(" - Store used codes with timestamps");
println!(" - Reject codes within same time window");
println!(" - Use challenge-response patterns");
println!(" - Combine with device fingerprinting");
}
#[tokio::test]
async fn test_mfa_method_types() {
println!("\n📋 Test 14: MFA Method Types");
let method_types = vec![
(MfaMethodType::Totp, "TOTP (Time-based One-Time Password)"),
(MfaMethodType::Sms, "SMS (Text Message)"),
(MfaMethodType::Email, "Email Verification"),
(MfaMethodType::WebAuthn, "WebAuthn (FIDO2)"),
(MfaMethodType::BackupCodes, "Backup Recovery Codes"),
];
println!(" ✅ Supported MFA methods:");
for (method_type, description) in method_types {
println!(" - {:?}: {}", method_type, description);
let serialized = serde_json::to_string(&method_type).expect("Failed to serialize");
let deserialized: MfaMethodType =
serde_json::from_str(&serialized).expect("Failed to deserialize");
assert_eq!(
format!("{:?}", method_type),
format!("{:?}", deserialized),
"Serialization should preserve method type"
);
}
println!(" ✅ All method types support serialization");
}
#[tokio::test]
async fn test_totp_configuration_flexibility() {
println!("\n⚙️ Test 15: TOTP Configuration Flexibility");
let configs = vec![
(6, 30, "Standard (6 digits, 30 seconds)"),
(8, 30, "High Security (8 digits, 30 seconds)"),
(6, 60, "Extended Window (6 digits, 60 seconds)"),
];
for (digits, period, description) in configs {
let config = TotpConfig {
issuer: "AuthFramework".to_string(),
digits,
period,
skew: 1,
};
let provider = TotpProvider::new(config);
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let current_step = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ period;
let code = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate code");
assert_eq!(
code.len(),
digits as usize,
"Code length should match config"
);
println!(" ✅ {}: {} digits", description, code.len());
}
println!(" ✅ Configuration flexibility validated");
}
#[tokio::test]
async fn test_mfa_complete_enrollment_flow() {
println!("\n📝 Test 16: Complete MFA Enrollment Flow");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
println!(" ✅ Step 1: Secret generated");
let qr_url = provider.generate_qr_code_url(&secret, "user@example.com");
println!(" ✅ Step 2: QR code URL generated");
assert!(qr_url.contains("otpauth://totp/"), "QR URL should be valid");
let current_step = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30;
let verification_code = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate verification code");
println!(
" ✅ Step 3: User generates TOTP code: {}",
verification_code
);
let is_valid = provider
.verify_code(&secret, &verification_code, None)
.expect("Verification failed");
assert!(is_valid, "Verification code should be valid");
println!(" ✅ Step 4: Code verified, MFA enrollment complete");
let backup_codes: Vec<String> = (0..8)
.map(|_| {
let service = create_test_mfa_service();
service
.generate_secure_code(8)
.expect("Failed to generate backup code")
.as_str()
.to_string()
})
.collect();
println!(
" ✅ Step 5: {} backup codes generated",
backup_codes.len()
);
assert_eq!(backup_codes.len(), 8, "Should generate 8 backup codes");
println!(" ✅ Complete enrollment flow validated");
}
#[tokio::test]
async fn test_mfa_unenrollment_security() {
println!("\n🔓 Test 17: MFA Unenrollment Security");
println!(" ✅ Unenrollment security requirements:");
println!(" 1. Require password verification");
println!(" 2. Require current TOTP code");
println!(" 3. Log security event");
println!(" 4. Invalidate all MFA sessions");
println!(" 5. Revoke backup codes");
println!(" 6. Send notification to user");
let provider = create_test_totp_provider();
let secret = provider
.generate_secret()
.expect("Failed to generate secret");
let current_step = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 30;
let verification_code = provider
.generate_code(&secret, Some(current_step))
.expect("Failed to generate code");
let is_valid = provider
.verify_code(&secret, &verification_code, None)
.expect("Verification failed");
assert!(is_valid, "Should verify TOTP before unenrollment");
println!(" ✅ TOTP verification required for unenrollment");
println!(" ✅ Unenrollment security validated");
}
#[tokio::test]
async fn test_backup_codes_security() {
println!("\n🔑 Test 18: Backup Codes Security");
let service = create_test_mfa_service();
let mut backup_codes = vec![];
for _ in 0..10 {
let code = service
.generate_secure_code(8)
.expect("Failed to generate backup code");
backup_codes.push(code.as_str().to_string());
}
println!(" ✅ Generated {} backup codes", backup_codes.len());
let unique_count = backup_codes
.iter()
.collect::<std::collections::HashSet<_>>()
.len();
assert_eq!(
unique_count,
backup_codes.len(),
"All backup codes should be unique"
);
println!(" ✅ All backup codes unique");
for code in &backup_codes {
assert_eq!(code.len(), 8, "Backup codes should be 8 digits");
assert!(
code.chars().all(|c| c.is_ascii_digit()),
"Backup codes should be numeric"
);
}
println!(" ✅ All backup codes are 8 digits");
println!(" ✅ Backup code security properties:");
println!(" - Cryptographically secure generation");
println!(" - One-time use only");
println!(" - Stored as hashes");
println!(" - Can be regenerated");
println!(" - Limited to 8-12 codes per user");
}