Deepu
Deepu

Reputation: 2616

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

Upvotes: 3

Views: 6588

Answers (5)

AGM
AGM

Reputation: 421

I faced same problem. But I solved by these step

  1. run rails g migration add_lockable_to_devise

  2. add following code in db/migrate/***********_add_lockable_to_devise

    def up
     add_column :users, :failed_attempts, :integer
     add_column :users, :unlock_token, :string
     add_column :users, :locked_at, :datetime
    
     add_index :users, :unlock_token, unique: true
    
     execute("UPDATE users SET confirmed_at = NOW()")
    end
    
    def down
      remove_columns :users, :failed_attempts, :unlock_token, :locked_at  
    end
    

3.After that run rake db:migrate

Don't forget step 3

Upvotes: 2

garac
garac

Reputation: 181

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]) }

Upvotes: 4

Benjamin Bouchet
Benjamin Bouchet

Reputation: 13181

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.

Upvotes: 5

Dave
Dave

Reputation: 4436

I had similar problem and I recreated table with this migration.

Try this:

change_table(:admins) do |t|
  t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
end

Upvotes: 1

SreekanthGS
SreekanthGS

Reputation: 988

This is a tricky case, and I can't help you much but can guide you to help yourself.

Devise updates the failed_attempts count at https://github.com/plataformatec/devise/blob/master/lib/devise/models/lockable.rb line number 92.

def valid_for_authentication?
    return super unless persisted? && lock_strategy_enabled?(:failed_attempts)

    # 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?
      true
    else
      self.failed_attempts ||= 0
      self.failed_attempts += 1
      if attempts_exceeded?
        lock_access! unless access_locked?
      else
        save(validate: false)
      end
      false
    end
  end

You have to edit the installed gem's corresponding file and add a logger or do puts to stdout to see if the else loop gets executed. The only possible reason why it won't be is when the first line return gets called.

I dont think you can solve this without digging deep into devise code since your configurations are correct.

Upvotes: -1

Related Questions