diff --git a/server/app/interactors/authentication/login.rb b/server/app/interactors/authentication/login.rb index e0608c4e6..2ab2a4fe3 100644 --- a/server/app/interactors/authentication/login.rb +++ b/server/app/interactors/authentication/login.rb @@ -18,6 +18,7 @@ def call end def authenticate(user) + reset_expired_lock(user) if account_locked?(user) handle_account_locked elsif valid_password?(user) @@ -43,6 +44,13 @@ def handle_failed_attempt(user) end end + def reset_expired_lock(user) + return unless user + return unless user.locked_at.present? && !user.access_locked? + + user.unlock_access! + end + def account_locked?(user) user&.access_locked? end @@ -67,6 +75,18 @@ def issue_token_and_update_user(user) token, payload = Warden::JWTAuth::UserEncoder.new.call(user, :user, nil) user.update!(unique_id: SecureRandom.uuid) if user.unique_id.nil? user.update!(jti: payload["jti"]) +<<<<<<< HEAD +======= + + # Reset lock state on successful login + if user.locked_at.present? + user.unlock_access! + elsif user.failed_attempts.positive? + user.update!(failed_attempts: 0) + end + + log_successful_login(user) +>>>>>>> 3b944b881 (fix(CE): unlock user account after lock expiry before re-authentication (#1762)) context.token = token end diff --git a/server/config/initializers/devise.rb b/server/config/initializers/devise.rb index 6b75ccec1..a74eca975 100644 --- a/server/config/initializers/devise.rb +++ b/server/config/initializers/devise.rb @@ -260,7 +260,7 @@ config.maximum_attempts = 5 # Time interval to unlock the account if :time is enabled as unlock_strategy. - config.unlock_in = 1.hour + config.unlock_in = 30.minutes # Warn on the last attempt before the account is locked. # config.last_attempt_warning = true diff --git a/server/spec/interactors/authentication/login_spec.rb b/server/spec/interactors/authentication/login_spec.rb index 1832de419..a944348f7 100644 --- a/server/spec/interactors/authentication/login_spec.rb +++ b/server/spec/interactors/authentication/login_spec.rb @@ -108,6 +108,63 @@ expect(context.token).to be_present end end + + it "resets failed_attempts and locked_at on successful login after lock expiry" do + travel_to 2.hours.from_now do + context + user.reload + expect(user.failed_attempts).to eq(0) + expect(user.locked_at).to be_nil + end + end + end + + context "when user lock has expired and wrong password is provided" do + let(:params) { { email: user.email, password: "wrong_password" } } + + before do + user.update(failed_attempts: Devise.maximum_attempts, locked_at: Time.current, confirmed_at: Time.current) + end + + it "allows fresh attempts instead of immediately re-locking" do + travel_to 2.hours.from_now do + context + user.reload + expect(user.failed_attempts).to eq(1) + expect(user.access_locked?).to be(false) + end + end + + it "returns invalid credentials error, not locked error" do + travel_to 2.hours.from_now do + expect(context).to be_failure + expect(context.error).to eq("Invalid login credentials, please try again") + end + end + end + + context "when lock has not yet expired" do + let(:params) { { email: user.email, password: "Password@123" } } + + before do + user.update(failed_attempts: Devise.maximum_attempts, locked_at: Time.current, confirmed_at: Time.current) + end + + it "stays locked before unlock_in period" do + travel_to 15.minutes.from_now do + expect(context).to be_failure + expect(context.error).to eq("Account is locked due to multiple login attempts. Please retry after sometime") + expect(user.reload.access_locked?).to be(true) + end + end + + it "unlocks at exactly the unlock_in boundary" do + travel_to(30.minutes.from_now + 1.second) do + context + expect(context).to be_success + expect(user.reload.access_locked?).to be(false) + end + end end context "handling exceptions" do