Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions server/app/interactors/authentication/login.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion server/config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions server/spec/interactors/authentication/login_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading