Feature: Anti-Bruteforce protection for login
This commit is contained in:
parent
a10ab4e058
commit
5e262dd5cd
|
@ -1,3 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP table locked_logins;
|
||||
DROP TABLE login_attempts;
|
|
@ -4,12 +4,4 @@ create table login_attempts
|
|||
id uuid default uuid_generate_v1() primary key,
|
||||
email text not null,
|
||||
timestamp timestamp default now() not null
|
||||
);
|
||||
|
||||
create table locked_logins
|
||||
(
|
||||
email text not null
|
||||
constraint locked_logins_pk
|
||||
primary key,
|
||||
locked_until timestamp not null
|
||||
);
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
use rocket::State;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use diesel::{sql_query, RunQueryDsl, ExpressionMethods};
|
||||
use diesel::sql_types::{Integer, Text};
|
||||
use crate::database::model::login_protection::LoginAttemptsResult;
|
||||
use chrono::{NaiveDateTime, Duration};
|
||||
use std::ops::Add;
|
||||
use crate::schema::login_attempts::dsl::login_attempts;
|
||||
|
||||
/// Checks if maximum login attempts exceeded. Locks account if exceeded
|
||||
/// Returns:
|
||||
/// * Ok(true) if login attempts exceeded and account got locked
|
||||
/// * Ok(false) if login attempts not exceeded
|
||||
/// * Err(diesel::result::Error) if database error occured
|
||||
pub fn login_attempts_exceeded(settings: &State<Settings>, email: String) -> Result<bool, diesel::result::Error>{
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
let result : Result<LoginAttemptsResult, diesel::result::Error> = sql_query("SELECT COUNT(*) AS count, MAX(timestamp) AS last_timestamp FROM login_attempts WHERE email=$1 AND timestamp > (now() - interval '1800 seconds')").bind::<Text, _>(email.clone()).get_result(&connection);
|
||||
let result = match result{
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
error!("Couldn't check for login attempts: {}", e);
|
||||
return Err(e)
|
||||
}
|
||||
};
|
||||
|
||||
if result.count > settings.application.max_login_attempts as i64 {
|
||||
Ok(true)
|
||||
}else{
|
||||
add_login_attempt(settings, email)?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_login_attempt(settings: &State<Settings>, email2: String) -> Result<(), diesel::result::Error>{
|
||||
use crate::schema::login_attempts::dsl::{login_attempts, email};
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
match diesel::insert_into(login_attempts).values(email.eq(email2)).execute(&connection){
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
error!("Couldn't write login attempt into DB! {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,4 +15,5 @@ pub mod roles;
|
|||
pub mod users;
|
||||
pub mod units;
|
||||
pub mod units_members;
|
||||
pub mod create_member;
|
||||
pub mod create_member;
|
||||
pub mod login_protection;
|
|
@ -0,0 +1,11 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use crate::schema::login_attempts;
|
||||
use diesel::sql_types::*;
|
||||
|
||||
#[derive(QueryableByName)]
|
||||
pub struct LoginAttemptsResult{
|
||||
#[sql_type = "BigInt"]
|
||||
pub count : i64,
|
||||
#[sql_type = "Nullable<Timestamp>"]
|
||||
pub last_timestamp: Option<NaiveDateTime>,
|
||||
}
|
|
@ -8,4 +8,5 @@ pub mod member_qualifications;
|
|||
pub mod members;
|
||||
pub mod roles;
|
||||
pub mod users;
|
||||
pub mod units;
|
||||
pub mod units;
|
||||
pub mod login_protection;
|
|
@ -13,8 +13,8 @@ pub struct Application {
|
|||
pub loglevel: String,
|
||||
pub session_timeout: i64,
|
||||
pub upload_path: String,
|
||||
pub max_login_attempts: i64,
|
||||
pub login_lock_duration: i64,
|
||||
pub max_login_attempts: i32,
|
||||
pub login_lock_duration: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
|
|
|
@ -35,7 +35,7 @@ pub fn handle_view(
|
|||
},
|
||||
],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(member.clone());
|
||||
let sidebar = Sidebar::new(member.clone());
|
||||
|
||||
let age = match member.date_of_birth {
|
||||
Some(date) => Some(calculate_age(date)),
|
||||
|
@ -89,7 +89,7 @@ pub fn handle_edit(
|
|||
},
|
||||
],
|
||||
};
|
||||
let mut sidebar = Sidebar::new(member.clone());
|
||||
let sidebar = Sidebar::new(member.clone());
|
||||
|
||||
let age = match member.date_of_birth {
|
||||
Some(date) => Some(calculate_age(date)),
|
||||
|
|
|
@ -40,16 +40,12 @@ pub fn member_management_add_member_post(
|
|||
None => return Err(Status::Unauthorized),
|
||||
};
|
||||
|
||||
debug!("Test");
|
||||
|
||||
if !caller.has_permission("modules.member_management.profile.create".to_string()){
|
||||
return Err(Status::Unauthorized)
|
||||
}
|
||||
|
||||
debug!("Test2");
|
||||
|
||||
match create_member(&settings, create_member_form.into_inner()){
|
||||
Ok(entity_id) => Ok(Redirect::to(format!("/portal/mm/profile?action=view&id={}",entity_id))),
|
||||
Err(e) => Err(Status::InternalServerError),
|
||||
Err(_) => Err(Status::InternalServerError),
|
||||
}
|
||||
}
|
|
@ -1,20 +1,53 @@
|
|||
use crate::database::controller::users::get_user_by_email;
|
||||
use crate::database::model::users::User;
|
||||
use crate::helper::session_cookies::model::SessionCookieStorage;
|
||||
use crate::modules::welcome::model::login_error_type::LoginError;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::modules::welcome::model::login_form::LoginForm;
|
||||
use chrono::{Duration, Utc};
|
||||
use rocket::http::{Cookie, Cookies};
|
||||
use rocket::State;
|
||||
use crate::database::controller::login_protection::login_attempts_exceeded;
|
||||
|
||||
pub fn check_login(login_form: LoginForm, settings: &State<Settings>) -> Result<User, LoginError> {
|
||||
let user: User = match get_user_by_email(login_form.login_email.clone(), &settings){
|
||||
Some(user) => match login_attempts_exceeded(settings, login_form.login_email){
|
||||
Ok(result) => {
|
||||
if result{
|
||||
return Err(LoginError::MaxLoginAttemptsExceeded)
|
||||
}else{
|
||||
user
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(LoginError::DatabaseError)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
match login_attempts_exceeded(settings, login_form.login_email){
|
||||
Ok(result) => {
|
||||
if result{
|
||||
return Err(LoginError::MaxLoginAttemptsExceeded)
|
||||
}else{
|
||||
return Err(LoginError::UserNotFound)
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(LoginError::DatabaseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let password_hash = match user.password.clone(){
|
||||
None => {return Err(LoginError::NoPassword);}
|
||||
Some(pw) => pw
|
||||
};
|
||||
|
||||
pub fn check_login(login_form: LoginForm, settings: &State<Settings>) -> Option<User> {
|
||||
let user: User = get_user_by_email(login_form.login_email, &settings)?;
|
||||
let password_hash = user.password.clone()?;
|
||||
trace!("Comparing password hash for {}", user.id);
|
||||
if argon2::verify_encoded(&password_hash, login_form.login_password.as_ref()).unwrap() {
|
||||
Some(user)
|
||||
Ok(user)
|
||||
} else {
|
||||
None
|
||||
Err(LoginError::PasswordIncorrect)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
use std::error;
|
||||
use std::error::Error as _;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginError {
|
||||
UserNotFound,
|
||||
MaxLoginAttemptsExceeded,
|
||||
PasswordIncorrect,
|
||||
DatabaseError,
|
||||
NoPassword
|
||||
}
|
||||
|
||||
impl fmt::Display for LoginError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
LoginError::UserNotFound =>
|
||||
write!(f, "User not found"),
|
||||
LoginError::MaxLoginAttemptsExceeded =>
|
||||
write!(f, "Maximum login attempts exceeded"),
|
||||
LoginError::PasswordIncorrect =>
|
||||
write!(f, "Password incorrect"),
|
||||
LoginError::DatabaseError => write!(f, "Database Error occured!"),
|
||||
LoginError::NoPassword => write!(f, "User missing password! No login possible."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for LoginError {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod login_form;
|
||||
pub mod welcome_module;
|
||||
pub mod login_error_type;
|
|
@ -9,6 +9,7 @@ use rocket::request::Form;
|
|||
use rocket::response::Redirect;
|
||||
use rocket::State;
|
||||
use rocket_contrib::templates::Template;
|
||||
use crate::modules::welcome::model::login_error_type::LoginError;
|
||||
|
||||
#[post("/", data = "<login_form>")]
|
||||
pub fn welcome_post(
|
||||
|
@ -19,21 +20,38 @@ pub fn welcome_post(
|
|||
) -> Result<Redirect, Template> {
|
||||
let user = check_login(login_form.into_inner(), &settings);
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
match user.clone() {
|
||||
Some(user) => trace!("LOGIN: {}", user.id),
|
||||
None => trace!("LOGIN FAILED"),
|
||||
}
|
||||
}
|
||||
let alert: Option<Alert> = match user {
|
||||
Some(user) => {
|
||||
Ok(user) => {
|
||||
add_session_cookie(user, &settings, cookie_storage, cookies);
|
||||
return Ok(Redirect::to("/loginselect"));
|
||||
}
|
||||
None => Some(Alert::new(
|
||||
AlertClass::Danger,
|
||||
"Login failed. Incorrect email or password!".to_string(),
|
||||
)),
|
||||
Err(e) => match e{
|
||||
LoginError::UserNotFound => {
|
||||
Some(Alert::new(
|
||||
AlertClass::Danger,
|
||||
"Anmelden fehlgeschlagen! Email oder Passwort falsch!".to_string()))
|
||||
},
|
||||
LoginError::MaxLoginAttemptsExceeded => {
|
||||
Some(Alert::new(
|
||||
AlertClass::Danger,
|
||||
format!("Es wurden zu viele Anmeldeversuche durchgeführt. Der Account wurde für {} Minuten gesperrt! Bitte nutzen Sie ggf. die Passwort vergessen Funktion!", settings.application.login_lock_duration/60)))
|
||||
},
|
||||
LoginError::PasswordIncorrect => {
|
||||
Some(Alert::new(
|
||||
AlertClass::Danger,
|
||||
"Anmelden fehlgeschlagen! Email oder Passwort falsch!".to_string()))
|
||||
},
|
||||
LoginError::DatabaseError => {
|
||||
Some(Alert::new(
|
||||
AlertClass::Danger,
|
||||
"Es konnte keine Datenbankverbindung hergestellt werden!".to_string()))
|
||||
}
|
||||
LoginError::NoPassword => {
|
||||
Some(Alert::new(
|
||||
AlertClass::Danger,
|
||||
"Anmelden fehlgeschlagen! Email oder Passwort falsch!".to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Err(Template::render("module_welcome", &get_context(alert)))
|
||||
|
|
Loading…
Reference in New Issue