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