Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lockable is not working in Devise

I have implemented Devise on two user accounts Admin and Customer. Register sign_in functions are working fine. I'm trying to implement lockable on an admin account. I'm using Devise 3.2.4.

After entering wrong credentials for specific time the account is still active and it doesn't record failed_attempts.

I have followed this guide HERE.

My devise.rb:

Devise.setup do |config|
  config.secret_key = 'XXXXX_the_secret_key_XXXXXXX'

  config.mailer_sender = '[email protected]'

  require 'devise/orm/active_record'

  # config.authentication_keys = [ :email ]

  # config.request_keys = []

  config.case_insensitive_keys = [ :email ]

  config.strip_whitespace_keys = [ :email ]

  # config.params_authenticatable = true

  # config.http_authenticatable = false

  # config.http_authenticatable_on_xhr = true

  # config.http_authentication_realm = 'Application'

  # config.paranoid = true

  # passing :skip => :sessions to `devise_for` in your config/routes.rb
  config.skip_session_storage = [:http_auth]

  # config.clean_up_csrf_token_on_authentication = true

  config.stretches = Rails.env.test? ? 1 : 10

  # config.pepper = '38635688e9d775b28e8da07b695dfced7b3bd4899c0a9a2a0f9b5ed5a8113e79864f76039166f827ef0134452fc0080f279adc4d1724362e079d0af3361edaf5'

  # config.allow_unconfirmed_access_for = 2.days

  # config.confirm_within = 3.days

  config.reconfirmable = true

  # config.confirmation_keys = [ :email ]

  # config.remember_for = 2.weeks

  # config.extend_remember_period = false

  # config.rememberable_options = {}

  # Range for password length.
  config.password_length = 8..128

  # config.email_regexp = /\A[^@]+@[^@]+\z/

  # config.timeout_in = 30.minutes

  # config.expire_auth_token_on_timeout = false

  # :failed_attempts = Locks an account after a number of failed attempts to sign in.
  # :none            = No lock strategy. You should handle locking by yourself.
  config.lock_strategy = :failed_attempts

  # Defines which key will be used when locking and unlocking an account
  config.unlock_keys = [ :email ]
  # config.unlock_keys = [ :time ]

  config.unlock_strategy = :both
  # config.unlock_strategy = :time

  config.maximum_attempts = 3

  config.unlock_in = 2.hour

  # config.last_attempt_warning = false

  config.reset_password_within = 24.hours

  # config.encryptor = :sha512

  config.sign_out_via = :delete

end

My Admin model:

class Admin < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :lockable
end

My migration to add lockable on admin:

class AddLockableToAdmin < ActiveRecord::Migration
  def change
    add_column :admins, :failed_attempts, :integer, default: 0
    add_column :admins, :unlock_token, :string
    add_column :admins, :locked_at, :datetime
  end
end

My routes.rb:

devise_for :admins
like image 406
Deepu Avatar asked Jun 26 '26 05:06

Deepu


2 Answers

STEP 1: verify that devise is correctly installed

1- You are missing null: false in the failed_attempts field of the migration.

add_column :admins, :failed_attempts, :integer, default: 0, null: false

Fix it and rerun your migration

2- Update all existing records in your console:

Admin.update_all failed_attempts: 0

3- Shutdown your server, console and anything else that uses or preload your application (spring, zeus etc...)

4- in your rails console, verify that devise is correctly installed

Admin.new.respond_to? :failed_attempts should return true

5- Still in your console, verify that Admin can be locked manually:

Admin.first.lock_access!

You should see the SQL updating locked_at and unlock_token fields of your records

6- Start your server and try again entering a wrong password (using another user for which you locked manually off course), see if the value of failed_attempts changes

=> result: All work, but logging-in with wrong credential does not increment failed_attempts


STEP2: Verify where devise fails

Brute-force debugging

I don't know if you have a debugger, so we are going to edit temporarily the method responsible of incrementing failed_attempts, and see where it stops. Open in devise gem the file "lib/devise/models/lockable.rb" and edit it like this:

def valid_for_authentication?
  puts 'mark 1'
  return super unless persisted? && lock_strategy_enabled?(:failed_attempts)
  puts 'mark 2'

  # Unlock the user if the lock is expired, no matter
  # if the user can login or not (wrong password, etc)
  unlock_access! if lock_expired?

  if super && !access_locked?
    puts 'mark 3 (you should not see me)'
    true
  else
    puts 'mark 4 (you are supposed to see me)'
    self.failed_attempts ||= 0
    self.failed_attempts += 1
    if attempts_exceeded?
      puts 'mark 5 (you should not see me)'
      lock_access! unless access_locked?
    else
      puts 'mark 6 (you are supposed to see me)'
      save(validate: false)
    end
    false
  end
end

As you can see I added "marks" to see where the execution pass. Note that depending on your version of devise, the content of the method may be slightly different, you just need to add the "marks".

Restart your server, try one log-in with incorrect credential, and look at your console to see which marks are displayed.

After our test you can revert this file to remove the marks

=> Result: None of the mark is displayed in console during a log-in with wrong credential

Execute in console Admin.first.valid_for_authentication?

=> Result: The marks 1, 2, 4, 6 are displayed and the failed_attempts is incremented in database


SOLUTION (Still to be confirmed)

The form used for authentication have an action value which is not redirecting to the devise controller. It seems that you are using api_console that is generating the form for authentication.

like image 112
Benj Avatar answered Jun 27 '26 20:06

Benj


After following the advice of Benj, I failed to get it to work. When debugging lib/devise/models/lockable.rb the code entered the if super && !access_locked? condition.

It turns out my issue was in the sign-in procedure, where I used the valid_for_authentication? function incorrectly. I.e. I did a check similar to this:

if user.valid_for_authentication? && user.valid_password?(params[:password])

instead of supplying password verification in a block:

if user.valid_for_authentication? { user.valid_password?(params[:password]) }
like image 33
garac Avatar answered Jun 27 '26 19:06

garac



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!