diff --git a/idp/src/token/service.ts b/idp/src/token/service.ts index 480abbccb..1bec361e6 100644 --- a/idp/src/token/service.ts +++ b/idp/src/token/service.ts @@ -58,8 +58,10 @@ export async function exchange(options: TokenExchangeOptions): Promise { throw newUserSuspendedError() } if (verifyPassword(options.password, user.passwordHash)) { + await resetFailedAttempts(user.id) return newToken(user.id, user.isAdmin) } else { + await increaseFailedAttempts(user.id) throw newInvalidUsernameOrPasswordError() } } else if (options.grant_type === 'refresh_token') { @@ -145,3 +147,19 @@ function newAccessTokenExpiry(): number { now.setSeconds(now.getSeconds() + getConfig().token.accessTokenLifetime) return Math.floor(now.getTime() / 1000) } + +async function increaseFailedAttempts(userId: string): Promise { + const user = await userRepo.findById(userId) + await userRepo.update({ + id: user.id, + failedAttempts: user.failedAttempts + 1, + }) +} + +async function resetFailedAttempts(userId: string): Promise { + const user = await userRepo.findById(userId) + await userRepo.update({ + id: user.id, + failedAttempts: 0, + }) +} diff --git a/idp/src/user/model.ts b/idp/src/user/model.ts index f9e7ebb88..e45f9ad64 100644 --- a/idp/src/user/model.ts +++ b/idp/src/user/model.ts @@ -24,6 +24,7 @@ export type User = { emailUpdateToken?: string emailUpdateValue?: string picture?: string + failedAttempts: number createTime: string updateTime?: string } @@ -60,6 +61,7 @@ export type UpdateOptions = { emailUpdateToken?: string emailUpdateValue?: string picture?: string + failedAttempts?: number createTime?: string updateTime?: string } diff --git a/idp/src/user/repo.ts b/idp/src/user/repo.ts index e0bf24f4e..e0ae3f724 100644 --- a/idp/src/user/repo.ts +++ b/idp/src/user/repo.ts @@ -194,8 +194,9 @@ class UserRepoImpl { email_update_token = $12, email_update_value = $13, picture = $14, - update_time = $15 - WHERE id = $16 + failed_attempts = $15, + update_time = $16 + WHERE id = $17 RETURNING *`, [ entity.fullName, @@ -212,6 +213,7 @@ class UserRepoImpl { entity.emailUpdateToken, entity.emailUpdateValue, entity.picture, + entity.failedAttempts, new Date().toISOString(), entity.id, ], @@ -265,6 +267,7 @@ class UserRepoImpl { emailUpdateToken: row.email_update_token, emailUpdateValue: row.email_update_value, picture: row.picture, + failedAttempts: row.failed_attempts, createTime: row.create_time, updateTime: row.update_time, } diff --git a/migrations/src/lib.rs b/migrations/src/lib.rs index af158f09e..fef86a002 100644 --- a/migrations/src/lib.rs +++ b/migrations/src/lib.rs @@ -29,6 +29,7 @@ mod m20240905_000001_add_user_active_admin_fields; mod m20240907_000001_add_user_force_change_password_field; mod m20240913_000001_drop_segmentation_column; mod m20241114_000001_drop_user_force_change_password_column; +mod m20241209_000001_add_user_failed_attempts_column; #[async_trait::async_trait] impl MigratorTrait for Migrator { @@ -50,6 +51,7 @@ impl MigratorTrait for Migrator { Box::new(m20240907_000001_add_user_force_change_password_field::Migration), Box::new(m20240913_000001_drop_segmentation_column::Migration), Box::new(m20241114_000001_drop_user_force_change_password_column::Migration), + Box::new(m20241209_000001_add_user_failed_attempts_column::Migration), ] } } diff --git a/migrations/src/m20241209_000001_add_user_failed_attempts_column.rs b/migrations/src/m20241209_000001_add_user_failed_attempts_column.rs new file mode 100644 index 000000000..f2bb34ff9 --- /dev/null +++ b/migrations/src/m20241209_000001_add_user_failed_attempts_column.rs @@ -0,0 +1,50 @@ +// Copyright (c) 2023 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. +use sea_orm_migration::prelude::*; + +use crate::models::v1::{User}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up( + &self, + manager: &SchemaManager, + ) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(User::Table) + .add_column(ColumnDef::new(User::FailedAttempts).integer().not_null().default(0)) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down( + &self, + manager: &SchemaManager, + ) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(User::Table) + .drop_column(User::FailedAttempts) + .to_owned(), + ) + .await?; + + Ok(()) + } +} diff --git a/migrations/src/models/v1/user.rs b/migrations/src/models/v1/user.rs index b5e1d1683..c2abd8c83 100644 --- a/migrations/src/models/v1/user.rs +++ b/migrations/src/models/v1/user.rs @@ -28,6 +28,7 @@ pub enum User { IsActive, ForceChangePassword, Picture, + FailedAttempts, CreateTime, UpdateTime, }