Implement password authentication
This commit is contained in:
		@@ -1,7 +1,8 @@
 | 
				
			|||||||
use crate::constants::StaticConstraints;
 | 
					use crate::constants::StaticConstraints;
 | 
				
			||||||
use crate::controllers::HttpResult;
 | 
					use crate::controllers::HttpResult;
 | 
				
			||||||
 | 
					use crate::models::{User, UserID};
 | 
				
			||||||
use crate::services::rate_limiter_service::RatedAction;
 | 
					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_remote_ip::RemoteIP;
 | 
				
			||||||
use actix_web::{web, HttpResponse};
 | 
					use actix_web::{web, HttpResponse};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -173,3 +174,56 @@ pub async fn reset_password(remote_ip: RemoteIP, req: web::Json<ResetPasswordBod
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Ok(HttpResponse::Accepted().finish())
 | 
					    Ok(HttpResponse::Accepted().finish())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
 | 
					pub struct PasswordLoginQuery {
 | 
				
			||||||
 | 
					    mail: String,
 | 
				
			||||||
 | 
					    password: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Handle login with password
 | 
				
			||||||
 | 
					pub async fn password_login(remote_ip: RemoteIP, req: web::Json<PasswordLoginQuery>) -> 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?,
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,10 @@ async fn main() -> std::io::Result<()> {
 | 
				
			|||||||
                "/auth/reset_password",
 | 
					                "/auth/reset_password",
 | 
				
			||||||
                web::post().to(auth_controller::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())?
 | 
					    .bind(AppConfig::get().listen_address.as_str())?
 | 
				
			||||||
    .run()
 | 
					    .run()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,18 @@ impl User {
 | 
				
			|||||||
    pub fn id(&self) -> UserID {
 | 
					    pub fn id(&self) -> UserID {
 | 
				
			||||||
        UserID(self.id)
 | 
					        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)]
 | 
					#[derive(Insertable)]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										58
									
								
								geneit_backend/src/services/login_token_service.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								geneit_backend/src/services/login_token_service.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<Self> {
 | 
				
			||||||
 | 
					        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<String> {
 | 
				
			||||||
 | 
					    let (key, token) = LoginToken::new(user);
 | 
				
			||||||
 | 
					    redis_connection::set_value(&key, &token, token.lifetime()).await?;
 | 
				
			||||||
 | 
					    Ok(key)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
//! # Backend services
 | 
					//! # Backend services
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod login_token_service;
 | 
				
			||||||
pub mod mail_service;
 | 
					pub mod mail_service;
 | 
				
			||||||
pub mod rate_limiter_service;
 | 
					pub mod rate_limiter_service;
 | 
				
			||||||
pub mod users_service;
 | 
					pub mod users_service;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ pub enum RatedAction {
 | 
				
			|||||||
    CreateAccount,
 | 
					    CreateAccount,
 | 
				
			||||||
    CheckResetPasswordTokenFailed,
 | 
					    CheckResetPasswordTokenFailed,
 | 
				
			||||||
    RequestNewPasswordResetLink,
 | 
					    RequestNewPasswordResetLink,
 | 
				
			||||||
 | 
					    FailedPasswordLogin,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl RatedAction {
 | 
					impl RatedAction {
 | 
				
			||||||
@@ -16,6 +17,7 @@ impl RatedAction {
 | 
				
			|||||||
            RatedAction::CreateAccount => "create-account",
 | 
					            RatedAction::CreateAccount => "create-account",
 | 
				
			||||||
            RatedAction::CheckResetPasswordTokenFailed => "check-reset-password-token",
 | 
					            RatedAction::CheckResetPasswordTokenFailed => "check-reset-password-token",
 | 
				
			||||||
            RatedAction::RequestNewPasswordResetLink => "req-pwd-reset-lnk",
 | 
					            RatedAction::RequestNewPasswordResetLink => "req-pwd-reset-lnk",
 | 
				
			||||||
 | 
					            RatedAction::FailedPasswordLogin => "failed-login",
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,6 +26,7 @@ impl RatedAction {
 | 
				
			|||||||
            RatedAction::CreateAccount => 5,
 | 
					            RatedAction::CreateAccount => 5,
 | 
				
			||||||
            RatedAction::CheckResetPasswordTokenFailed => 100,
 | 
					            RatedAction::CheckResetPasswordTokenFailed => 100,
 | 
				
			||||||
            RatedAction::RequestNewPasswordResetLink => 5,
 | 
					            RatedAction::RequestNewPasswordResetLink => 5,
 | 
				
			||||||
 | 
					            RatedAction::FailedPasswordLogin => 15,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user