diff --git a/migrations/mysql/2022-09-15-002500_add_login_attempts/down.sql b/migrations/mysql/2022-09-15-002500_add_login_attempts/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2022-09-15-002500_add_login_attempts/up.sql b/migrations/mysql/2022-09-15-002500_add_login_attempts/up.sql new file mode 100644 index 00000000..725ef9d9 --- /dev/null +++ b/migrations/mysql/2022-09-15-002500_add_login_attempts/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN invalid_login_count INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/migrations/postgresql/2022-09-15-002500_add_login_attempts/down.sql b/migrations/postgresql/2022-09-15-002500_add_login_attempts/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2022-09-15-002500_add_login_attempts/up.sql b/migrations/postgresql/2022-09-15-002500_add_login_attempts/up.sql new file mode 100644 index 00000000..725ef9d9 --- /dev/null +++ b/migrations/postgresql/2022-09-15-002500_add_login_attempts/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN invalid_login_count INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/migrations/sqlite/2022-09-15-002500_add_login_attempts/down.sql b/migrations/sqlite/2022-09-15-002500_add_login_attempts/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2022-09-15-002500_add_login_attempts/up.sql b/migrations/sqlite/2022-09-15-002500_add_login_attempts/up.sql new file mode 100644 index 00000000..d870b1a3 --- /dev/null +++ b/migrations/sqlite/2022-09-15-002500_add_login_attempts/up.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN invalid_login_count INTEGER NOT NULL DEFAULT 0; diff --git a/src/api/identity.rs b/src/api/identity.rs index d0a3bcce..399a0b77 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -105,15 +105,36 @@ async fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> Json None => err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)), }; + // Check if the user is disabled + if !user.enabled { + err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username)) + } + // Check password let password = data.password.as_ref().unwrap(); if !user.check_valid_password(password) { - err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)) - } + // When the configuration limits the number of attempts + if CONFIG.login_max_retry() > 0 { + let mut user = user; + let invalid_count = user.invalid_login_count; + //It is already the nth attempts, disable the user ! + if user.invalid_login_count >= CONFIG.login_max_retry() { + user.enabled = false; + user.invalid_login_count = 0; + } else { + user.invalid_login_count += 1; + } - // Check if the user is disabled - if !user.enabled { - err!("This user has been disabled", format!("IP: {}. Username: {}.", ip.ip, username)) + if let Err(e) = user.save(&conn).await { + error!("Error updating user: {:#?}", e); + } + + if !user.enabled { + err!("Too many failed login attempts. User has been disabled", format!("IP: {}. Username: {}. Invalid logins: {}.", ip.ip, username, invalid_count)) + } + } + + err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username)) } let now = Utc::now().naive_utc(); @@ -121,7 +142,7 @@ async fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> Json if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { if user.last_verifying_at.is_none() || now.signed_duration_since(user.last_verifying_at.unwrap()).num_seconds() - > CONFIG.signups_verify_resend_time() as i64 + > CONFIG.signups_verify_resend_time() as i64 { let resend_limit = CONFIG.signups_verify_resend_limit() as i32; if resend_limit == 0 || user.login_verify_count < resend_limit { @@ -184,6 +205,13 @@ async fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> Json result["TwoFactorToken"] = Value::String(token); } + // Reset the number of attempts on success logins + let mut user = user; + user.invalid_login_count = 0; + if let Err(e) = user.save(&conn).await { + error!("Error updating user: {:#?}", e); + } + info!("User {} logged in successfully. IP: {}", username, ip.ip); Ok(Json(result)) } diff --git a/src/config.rs b/src/config.rs index b8f3246b..0ad29ae0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -543,6 +543,9 @@ make_config! { admin_ratelimit_seconds: u64, false, def, 300; /// Max burst size for login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds` admin_ratelimit_max_burst: u32, false, def, 3; + + /// Max number of login retries before user being disabled |> Limit the number of login attempts before the user is disabled automatically. 0 means no login attempts limits. Greater equal 1 means that user can retry 1 or more time before the account being locked. + login_max_retry: i32, false, def, 0; }, /// Yubikey settings diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 9e692a3f..dbccba95 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -17,6 +17,7 @@ db_object! { pub verified_at: Option, pub last_verifying_at: Option, pub login_verify_count: i32, + pub invalid_login_count: i32, pub email: String, pub email_new: Option, @@ -86,6 +87,8 @@ impl User { verified_at: None, last_verifying_at: None, login_verify_count: 0, + invalid_login_count: 0, + name: email.clone(), email, akey: String::new(), diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index a49159f2..111178b0 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -159,6 +159,7 @@ table! { verified_at -> Nullable, last_verifying_at -> Nullable, login_verify_count -> Integer, + invalid_login_count -> Integer, email -> Text, email_new -> Nullable, email_new_token -> Nullable, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 9fd6fd97..31dbefb4 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -159,6 +159,7 @@ table! { verified_at -> Nullable, last_verifying_at -> Nullable, login_verify_count -> Integer, + invalid_login_count -> Integer, email -> Text, email_new -> Nullable, email_new_token -> Nullable, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 9fd6fd97..31dbefb4 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -159,6 +159,7 @@ table! { verified_at -> Nullable, last_verifying_at -> Nullable, login_verify_count -> Integer, + invalid_login_count -> Integer, email -> Text, email_new -> Nullable, email_new_token -> Nullable,