Feature: Anti-Bruteforce protection for login

This commit is contained in:
Keanu D?lle 2020-12-22 03:41:36 +01:00
parent a10ab4e058
commit 5e262dd5cd
13 changed files with 170 additions and 38 deletions

View File

@ -1,3 +1,2 @@
-- This file should undo anything in `up.sql`
DROP table locked_logins;
DROP TABLE login_attempts;

View File

@ -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
);
);

View File

@ -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)
}
}
}

View File

@ -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;

View File

@ -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>,
}

View File

@ -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;

View File

@ -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)]

View File

@ -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)),

View File

@ -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),
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -1,2 +1,3 @@
pub mod login_form;
pub mod welcome_module;
pub mod login_error_type;

View File

@ -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)))