Ferdinand Rosario Ferdinand Rosario - 5 months ago 29
Ruby Question

Locking a user using a per-user `maximum_attempts` value with Devise for Rails

As a de-facto standard, we all using Devise for login in our Rails application and will use the Lockable module to lock users after a particular number of failed attempts.

From Devise’s source code and the configuration option

config.maximum_attempts = 20
, I came to understand how Devise performs locking when the user tries to give wrong login credentials. Configuration is statically defined at Rails application boot time in
initializers
.

My expectation is to set the
maximum_attempts
dynamically – is this possible? If so, please guide me.

I have a superadmin and user below each admin. Based on the super admin I would like to set a different
failed_attempt
value for each user during runtime.

Answer

One possible way is to monkey-patch the Devise code that you linked to, where attempts_exceeded? is defined. Here’s a guess at what needs to be overridden:

module Devise::Models::Lockable
  # assumes that the User model has a `superadmin` relation
  #  that has a `maximum_attempts` attribute

  def attempts_exceeded?        
    self.failed_attempts >= self.superadmin.maximum_attempts
  end

  def last_attempt?
    self.failed_attempts == self.superadmin.maximum_attempts - 1
  end
end

This should work, but it would mean that whenever you update Devise, there is a risk of related code breaking, with unknown consequences. So you would have to review the changes to Devise before every update. And if you are discouraged from updating Devise because of this, that may eventually cause security problems if you are too slow to update to a version of Devise with a fixed security problem. So beware of those possible problems.

A safer way that requires more work up-front is to lock the user manually from your own code. The documentation for Devise::Models::Lockable mentions a public method lock_access! that locks the user when you call it. You can set the global config.maximum_attempts to some really high value such as 25. Then, in some callback on the model (I’m not sure which callback), call a method lock_access_based_on_superadmin_limit! that calls lock_access! if necessary according to your custom rules. The following definition is adapted from part of Devise’s valid_for_authentication?:

class User
  # …

  def lock_access_based_on_superadmin_limit!
    if failed_attempts >= superadmin.maximum_attempts
      lock_access! unless access_locked?
    end
  end
end
Comments