forked from Txio-labs/txio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathotp_service.rs
More file actions
86 lines (71 loc) · 2.65 KB
/
Copy pathotp_service.rs
File metadata and controls
86 lines (71 loc) · 2.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
use crate::model::otp::OTP;
use crate::repositories::otp_repository::OTPRepository;
use crate::utils::error::AppError;
use crate::utils::generate_otp::generate_otp;
use chrono::{Duration, Utc};
const OTP_LENGTH: usize = 6;
const OTP_VALIDITY_MINUTES: i64 = 5;
const OTP_SEND_COOLDOWN_SECONDS: i64 = 60;
const OTP_MAX_FAILED_ATTEMPTS: i32 = 5;
#[derive(Clone)]
pub struct OTPService {
repository: OTPRepository,
}
impl OTPService {
pub fn new(repository: OTPRepository) -> Self {
OTPService { repository }
}
pub async fn generate_otp(&self, email: &str) -> Result<String, AppError> {
let now = Utc::now();
if let Ok(existing_otp) = self.repository.find_by_email(email).await {
if now < existing_otp.created_at + Duration::seconds(OTP_SEND_COOLDOWN_SECONDS) {
return Err(AppError::BadRequest(
"OTP request rate limit exceeded. Please try again later.".into(),
));
}
let _ = self.repository.delete_by_email(email).await;
}
let code = generate_otp(OTP_LENGTH);
let otp = OTP::new(email.to_string(), code.clone());
self.repository.save(&otp).await?;
Ok(code)
}
pub async fn verify_otp(&self, email: &str, code: &str) -> Result<bool, AppError> {
let otp = match self.repository.find_by_email(email).await {
Ok(otp) => otp,
Err(AppError::NotFound(_)) => return Ok(false),
Err(e) => return Err(e),
};
let now = Utc::now();
if now > otp.created_at + Duration::minutes(OTP_VALIDITY_MINUTES) {
let _ = self.repository.delete_by_email(email).await;
return Ok(false);
}
if !constant_time_eq(&otp.otp, code) {
let failed_attempts = otp.failed_attempts + 1;
if failed_attempts >= OTP_MAX_FAILED_ATTEMPTS {
let _ = self.repository.delete_by_email(email).await;
} else {
self.repository
.update_failed_attempts(email, failed_attempts)
.await?;
}
return Ok(false);
}
self.repository.delete_by_email(email).await?;
Ok(true)
}
}
/// Compares two strings in constant time relative to their length, so that
/// early-exit timing cannot be used to learn how much of a secret matched.
/// Shared by OTP verification and OAuth CSRF-state checks.
pub(crate) fn constant_time_eq(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.bytes().zip(b.bytes()) {
diff |= x ^ y;
}
diff == 0
}