Search code examples
ruby-on-railsrubyruby-on-rails-3devise

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 = 'mymail@domain.com'

  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

Solution

  • 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.