diff --git a/geneit_backend/src/controllers/auth_controller.rs b/geneit_backend/src/controllers/auth_controller.rs index e6fd233..64e3342 100644 --- a/geneit_backend/src/controllers/auth_controller.rs +++ b/geneit_backend/src/controllers/auth_controller.rs @@ -1,7 +1,8 @@ use crate::constants::StaticConstraints; use crate::controllers::HttpResult; +use crate::models::{User, UserID}; use crate::services::rate_limiter_service::RatedAction; -use crate::services::{rate_limiter_service, users_service}; +use crate::services::{login_token_service, rate_limiter_service, users_service}; use actix_remote_ip::RemoteIP; use actix_web::{web, HttpResponse}; @@ -173,3 +174,56 @@ pub async fn reset_password(remote_ip: RemoteIP, req: web::Json) -> HttpResult { + // Rate limiting + if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::FailedPasswordLogin) + .await? + { + return Ok(HttpResponse::TooManyRequests().finish()); + } + + // Get user account + let user = match users_service::get_by_mail(&req.mail).await { + Ok(u) => u, + Err(e) => { + log::error!("Auth failed: could not find account by mail! {}", e); + rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin) + .await?; + return Ok(HttpResponse::Unauthorized().json("Identifiants incorrects")); + } + }; + + if !user.check_password(&req.password) { + log::error!("Auth failed: invalid password for mail {}", user.email); + rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin).await?; + return Ok(HttpResponse::Unauthorized().json("Identifiants incorrects")); + } + + finish_login(&user).await +} + +#[derive(serde::Serialize)] +struct LoginResponse { + user_id: UserID, + token: String, +} + +async fn finish_login(user: &User) -> HttpResult { + if !user.active { + log::error!("Auth failed: account for mail {} is disabled!", user.email); + return Ok(HttpResponse::ExpectationFailed().json("Ce compte est désactivé !")); + } + + Ok(HttpResponse::Ok().json(LoginResponse { + user_id: user.id(), + token: login_token_service::gen_new_token(user).await?, + })) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index adbf0df..0b628ca 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -39,6 +39,10 @@ async fn main() -> std::io::Result<()> { "/auth/reset_password", web::post().to(auth_controller::reset_password), ) + .route( + "/auth/password_login", + web::post().to(auth_controller::password_login), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index b4780ae..2c94f5c 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -23,6 +23,18 @@ impl User { pub fn id(&self) -> UserID { UserID(self.id) } + + pub fn check_password(&self, password: &str) -> bool { + self.password + .as_deref() + .map(|hash| { + bcrypt::verify(password, hash).unwrap_or_else(|e| { + log::error!("Failed to validate password! {}", e); + false + }) + }) + .unwrap_or(false) + } } #[derive(Insertable)] diff --git a/geneit_backend/src/services/login_token_service.rs b/geneit_backend/src/services/login_token_service.rs new file mode 100644 index 0000000..6f732a1 --- /dev/null +++ b/geneit_backend/src/services/login_token_service.rs @@ -0,0 +1,58 @@ +//! # User tokens management + +use crate::connections::redis_connection; +use crate::models::{User, UserID}; +use crate::utils::string_utils; +use crate::utils::time_utils::time; +use std::time::Duration; + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +struct LoginToken { + expire: u64, + hb: u64, + user_id: UserID, +} + +impl LoginToken { + pub fn new(user: &User) -> (String, Self) { + let key = format!("tok-{}-{}", user.id().0, string_utils::rand_str(40)); + + ( + key, + Self { + expire: time() + 3600 * 24, + hb: time(), + user_id: user.id(), + }, + ) + } + + pub fn is_valid(&self) -> bool { + self.expire > time() && self.hb + 3600 > time() + } + + pub fn refresh_hb(&self) -> Option { + if self.hb + 60 * 5 < time() { + let mut new = self.clone(); + new.hb = time(); + Some(new) + } else { + None + } + } + + pub fn lifetime(&self) -> Duration { + Duration::from_secs(if self.expire <= time() { + 0 + } else { + self.expire - time() + }) + } +} + +/// Generate a new login token +pub async fn gen_new_token(user: &User) -> anyhow::Result { + let (key, token) = LoginToken::new(user); + redis_connection::set_value(&key, &token, token.lifetime()).await?; + Ok(key) +} diff --git a/geneit_backend/src/services/mod.rs b/geneit_backend/src/services/mod.rs index 0e81e07..c908511 100644 --- a/geneit_backend/src/services/mod.rs +++ b/geneit_backend/src/services/mod.rs @@ -1,5 +1,6 @@ //! # Backend services +pub mod login_token_service; pub mod mail_service; pub mod rate_limiter_service; pub mod users_service; diff --git a/geneit_backend/src/services/rate_limiter_service.rs b/geneit_backend/src/services/rate_limiter_service.rs index 05cc39b..18c51ed 100644 --- a/geneit_backend/src/services/rate_limiter_service.rs +++ b/geneit_backend/src/services/rate_limiter_service.rs @@ -8,6 +8,7 @@ pub enum RatedAction { CreateAccount, CheckResetPasswordTokenFailed, RequestNewPasswordResetLink, + FailedPasswordLogin, } impl RatedAction { @@ -16,6 +17,7 @@ impl RatedAction { RatedAction::CreateAccount => "create-account", RatedAction::CheckResetPasswordTokenFailed => "check-reset-password-token", RatedAction::RequestNewPasswordResetLink => "req-pwd-reset-lnk", + RatedAction::FailedPasswordLogin => "failed-login", } } @@ -24,6 +26,7 @@ impl RatedAction { RatedAction::CreateAccount => 5, RatedAction::CheckResetPasswordTokenFailed => 100, RatedAction::RequestNewPasswordResetLink => 5, + RatedAction::FailedPasswordLogin => 15, } }