login_service.rs
1 use chrono::{DateTime, Duration, Utc}; 2 use sqlx::Pool; 3 4 use crate::{ 5 db::{login_details_repository, new_transaction, DB}, 6 models::login_details::LoginDetails, 7 util::{accounts_error::AccountsError, config::Config}, 8 }; 9 10 use super::password_service; 11 12 #[derive(Debug, thiserror::Error)] 13 pub enum LoginError { 14 #[error("An internal error occurred")] 15 Internal, 16 #[error("Invalid email/password")] 17 InvalidEmailPassword, 18 #[error("Account is locked due to excessive incorrect login attempts")] 19 AccountLocked, 20 #[error("Account has not yet been activated")] 21 AccountNotActivated, 22 } 23 24 impl From<AccountsError> for LoginError { 25 fn from(_: AccountsError) -> Self { 26 LoginError::Internal 27 } 28 } 29 impl From<sqlx::Error> for LoginError { 30 fn from(_: sqlx::Error) -> Self { 31 LoginError::Internal 32 } 33 } 34 35 pub async fn validate_login( 36 config: &Config, 37 db_pool: &Pool<DB>, 38 email: String, 39 password: String, 40 ) -> Result<LoginDetails, LoginError> { 41 let mut transaction = new_transaction(db_pool).await?; 42 43 let login_details = 44 match login_details_repository::get_by_email(&mut transaction, &email).await? { 45 None => return Err(LoginError::InvalidEmailPassword), 46 Some(l) => l, 47 }; 48 49 if !password_service::verify_password( 50 password.to_owned(), 51 login_details.password.to_owned(), 52 login_details.password_nonces.to_owned(), 53 config, 54 ) { 55 // Password incorrect: 56 // - Increase the invalid password count 57 // - Set an appropriate account lockout depending on the number of incorrect password count. 58 let new_invalid_password_count = login_details.incorrect_password_count + 1; 59 let account_lockout_until = get_account_lockout(new_invalid_password_count); 60 login_details_repository::set_account_lockout( 61 &mut transaction, 62 login_details.account_id, 63 new_invalid_password_count, 64 account_lockout_until, 65 ) 66 .await?; 67 68 transaction.commit().await?; 69 return Err(LoginError::InvalidEmailPassword); 70 } 71 72 let now = Utc::now(); 73 if let Some(locked_until) = login_details.account_locked_until { 74 if locked_until > now { 75 return Err(LoginError::AccountLocked); 76 } 77 } 78 79 // If we reach here, we now accept the user as authorized with the account 80 81 if login_details.activated_at.is_none() { 82 return Err(LoginError::AccountNotActivated); 83 } 84 85 // Reset account lock on successful login 86 login_details_repository::set_account_lockout( 87 &mut transaction, 88 login_details.account_id, 89 0, 90 Option::None, 91 ) 92 .await?; 93 94 transaction.commit().await?; 95 Ok(login_details) 96 } 97 98 /// Generate a time until which the account will be locked 99 /// Based on the given number of incorrect password count. 100 /// 101 /// Returns an option which if Some(v) will contain the datetime until which the account should be locked 102 /// Or None if the account shouldn't be locked. 103 fn get_account_lockout(invalid_password_count: i32) -> Option<DateTime<Utc>> { 104 let lockout_duration = match invalid_password_count { 105 // We don't need to lock the account on the first couple of failures. 106 0 => return None, 107 1 => Duration::seconds(1), 108 2 => Duration::seconds(5), 109 3 => Duration::minutes(2), 110 4 => Duration::minutes(15), 111 5 => Duration::minutes(30), 112 6 => Duration::hours(2), 113 7 => Duration::days(1), 114 8 => Duration::weeks(1), 115 9 => Duration::weeks(2), 116 _ => Duration::days(365), 117 }; 118 119 let now = Utc::now(); 120 let locked_until = now 121 .checked_add_signed(lockout_duration) 122 .unwrap_or(DateTime::<Utc>::MAX_UTC); 123 Some(locked_until) 124 }