Locking a user using a per-user `maximum_attempts` value with Devise for Rails - ruby-on-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.

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

Related

How to select attributes when devise setting current_user?

At this point, I'm not sure how or where the helper current_user is loaded. I'd like to select only certain properties from the user table, but devise to
select * from users table
I want to do something like select (id, email, additional_stuff) from users
I would like to be able to modify the current_user set by devise so I can optimise my application from a security point.
Using Rails version 7.0.4
RUby Version 3.1.3
Devise is build on top of the Warden gem which handles the grunt work of actually authenticating users.
Fetching the user from the database is done by the Warden::Manager.serialize_from_session method which can be reconfigured.
# app/initializers/devise.rb
config.warden do |manager|
manager.serialize_from_session(:users) do |id|
User.select(:id, :email, :additional_stuff)
.find(id)
end
end
However, I'm very sceptical that this will have any real benefits to security and you'll most likely just end up breaking parts of Devise/the rest of your application. For example authenticating the user for password updates may fail unless you load the password digest.
Make sure you have tests covering your whole Devise implementation before you monkey around with it.
It's hard to know what you actually mean by "Certain class in the application use the current_user object, one such class is template creator" . But that smells like a huge gaping security hole in itself. If this is running code that comes from the user it should not have access to the entire view context (for example the current_user method). If the user has access to the actual object then whats preventing them from querying and getting any missing data?
If you really need this feature you should be sandboxing it in a separate renderer (not using Rails build in render methods) which only has access to the context and data you explicitly pass (like a struct or decorator representing a user) which is deemed safe.

How to extend clearance's back door to allow for 2FA

I have an application which uses the Clearance gem for authentication, but also implements 2FA. I want to use the "Backdoor" functionality of Clearance for tests, but am unsure how to do this in conjunction with 2FA.
Is there a way I can "hook into" Clearance's Backdoor functionality and set the required 2FA values whenever it is used to sign in?
Based on the source of Clearance::Backdoor, if you're trying to set extra values on a user model, this might work:
# config/environments/test.rb
MyRailsApp::Application.configure do
# ...
config.middleware.use Clearance::BackDoor do |username|
user = User.find_by(username: username) # or however you'd find a user
# set your extra values
user.x = 'x'
user.y = 'y'
# return the user
user
end
end
If you want to mess with the request I don't think you can use Clearance::Backdoor, but you could add another Rack middleware after it using config.middleware.insert_after(Clearance::Backdoor) (you would have to write your own middleware).
As an alternative, a lot of tests I've seen just mock the piece of code that checks whether a user is signed in, and make it always return true (or whatever indicates success).

Devise User's password gets overwritten upon updating any other attributes

Problem
When I use user.update(not_password_attribute: 'value') for the first time in the session the SQL query includes this:
UPDATE "users" SET "encrypted_password" = $1, so the encrypted password gets overwritten, and the password the user had before is no longer valid (checked with #valid_password? method).
However, when I update the same user for the second time within one session the encrypted_password is no longer added to the SQL query.
But when close the rails console and open it again the first scenario happens again.
Here is the output from the rails console: https://i.stack.imgur.com/ktTgJ.jpg
The same happens if I use user.update_without_password
here is the output
I used the console to demo that the problem is not within controllers but it happens whenever the user is updated from any of the controllers including Active Admin or UsersController etc.
I tried using Devise 4.7.2 which I used in a previous project where this issue did not occur. But the outcome was the same.
Question
How do I update the user without the password being overwritten?
Some explanation why this happens would also help.
I am using
Rails 6.0.3.4
Devise 4.7.3
Active Admin 2.9.0 b9076eb (don't think the issue is in Active Admin but trying to provide all the information here)
Devise Config (in /app/config/inintializers/devise.rb)
config.mailer_sender = 'please-change-me-at-config-initializers-devise#example.com'
require 'devise/orm/active_record'
config.case_insensitive_keys = [:email]
config.strip_whitespace_keys = [:email]
config.skip_session_storage = [:http_auth]
config.stretches = Rails.env.test? ? 1 : 12
config.reconfirmable = true
config.expire_all_remember_me_on_sign_out = true
config.password_length = 6..128
config.email_regexp = /\A[^#\s]+#[^#\s]+\z/
config.reset_password_within = 6.hours
config.sign_out_via = :delete
None of these seem relevant to the problem but, again, trying to provide as much information as possible
User model (in /app/models/user.rb)
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
# some methods here but none overwrites Devise or ActiveRecord methods
end
I cannot say I am completely sure what might be going there but I have a few guesses.
So, it seems like your issue is: user.update(first_name: 'one') is triggering a callback that attempts to update encrypted password even when an actual password is not provided or it is considered blank. But there is more.
Why it doesn't get triggered for the subsequent update (user.update(first_name: 'two')?
Possible explanation for that one is - perhaps callback is not triggered if logic behind it figures out that the password is now the same as the one we have set with the first command (ie blank) or the user object we call the second update on has encrypted password field populated after we executed the first update.
Ok, but then, rerunning the console again, and for the same user that we previously set blank password for, we again get encrypted_password update?
Looking at the encrypted password being set in the new console and observing that it is different than the previous one, we can reason that either blank case is handled by assigning random encrypted password, or there is a monkey-patch causing DeviseEncryptor (see https://github.com/heartcombo/devise/blob/98fc5e8e396b66b826528811287ea6680a6d0757/lib/devise/models/database_authenticatable.rb#L71) to somehow generate different result based on current session (this would probably break a lot more things thus highly-unlikely).
The weirdest one is update_without_password still adding encrypted password to query???
Explanation - whatever was previously mentioned seems to affect update_without_password in a similar manner.
The solution
I think you can solve your issue if you use update methods that skip callbacks like: update_column or update_columns (see: https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks)
If you really want to understand what is going on exactly I would suggest to take a look at this file from devise source: https://github.com/heartcombo/devise/blob/master/lib/devise/models/database_authenticatable.rb. Then, after you get a grasp of the code there, you can perhaps find the same source file in your local environment (wherever devise gem is installed), and instrument it with logs (puts) and check / patch it out yourself.
Note: a few nice 'techniques' are described here:
https://tenderlovemaking.com/2016/02/05/i-am-a-puts-debuggerer.html
It would also be a good idea to disable spring while debugging weirdnesses like this. Good luck!
As a bonus - you can get some more context about why things around devise user update work the way they do by reading this discussion: https://github.com/heartcombo/devise/issues/5033 I believe ActiveAdmin was also mentioned in the discussion. Tbh, I originally thought you had the same issue as them but the affected version does not match with the one you shared.

How can I allow Devise users to log in when they're outside my default scope?

I have a Rails 4 app which uses Devise 3.4 for authentication, which I've customized with the ability to ban users (using a simple boolean column users.banned, default false). The User model also has a default_scope which only returns non-banned users.
Here's the problem - I still want my banned users to be able to log in, even though they can't do anything after logging in. (They essentially just see a page saying "you've been banned"). But it seems that the default_scope is tripping up Devise. When you log in or call e.g. authenticate_user!, Devise tries to find the current user using one of the basic ActiveRecord methods like find or find_by, but can't because they lie outside the default scope. Thus Devise concludes that the user doesn't exist, and the login fails.
How can I make Devise ignore the default scope?
After a long time digging around in the Devise and Warden source code, I finally found a solution.
Short Answer:
Add this to the User class:
def self.serialize_from_session(key, salt)
record = to_adapter.klass.unscoped.find(key[0])
record if record && record.authenticatable_salt == salt
end
(Note that I've only tested this for ActiveRecord; if you're using a different ORM adapter you probably need to change the first line of the method... but then I'm not sure if other ORM adapters even have the concept of a "default so
Long Answer:
serialize_from_session is mixed into the User class from -Devise::Models::Authenticatable::ClassMethods. Honestly, I'm not sure what it's actually supposed to do, but it's a public method and documented (very sparsely) in the Devise API, so I don't think there's much chance of it being removed from Devise without warning.
Here's the original source code as of Devise 3.4.1:
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
The problem lies with to_adapter.get(key). to_adapter returns an instance of OrmAdapter::ActiveRecord wrapped around the User class, and to_adapter.get is essentially the same as calling User.find. (Devise uses the orm_adapter gem to keep it flexible; the above method will work without modification whether you're using ActiveRecord, Mongoid or any other OrmAdapter-compatible ORM.)
But, of course, User.find only searches within the default_scope, which is why it can't find my banned users. Calling to_adapter.klass returns the User class directly, and from then I can call unscoped.find to search all my users and make the banned ones visible to Devise. So the working line is:
record = to_adapter.klass.unscoped.find(key[0])
Note that I'm passing key[0] instead of key, because key is an Array (in this case with one element) and passing an Array to find will return an Array, which isn't what we want.
Also note that calling klass within the real Devise source code would be a bad idea, as it means you lose the advantages of OrmAdapter. But within your own app, where you know with certainty which ORM you're using (something Devise doesn't know), it's safe to be specific.

Rails - Devise and acts_as_audited

Hey, I want to use Devise and acts_as_audited together but when I try and link them using -
class ApplicationController < ActionController::Base
audit Candidate
protected
def current_user
#user = User.find(user_session)
end
I get this error.
stack level too deep
Do I need to do it differently ?
Thanks
This is an old question, but still rears its ugly head. Here's a different, possibly more appropriate workaround which worked for me.
First, as others describe, the bug happens when using audited (formerly acts_as_audited) with devise (and potentially other authentication gems), and then when you are auditing any of the column Devise uses on your User model (last_sign_in_at, last_sign_in_ip, etc).
Devise tries to authenticate the user (using its authenticate_user! before_filter).
Devise tries to update/save the user's sign in info (last_sign_in_at, ip, etc) for posterity
As part of that save, Audited then tries to create an audit for that change.
Audited tries to set the User for that Audit, to indicate who made the change. How does it do that?
Audited calls current_user, a Devise method. Devise isn't yet done with its authenticate method from step 1 - Audited stepped in and is doing its thing. So,
The current_user method repeats step #1 (the authenticate_user! method), creating an infinite loop
Your application errors with Stack Level Too Deep
#DGM's workaround simply tells Audited to not audit this change, which might work for you. However, in my application, I need to audit that change.
Audited allows you to specify a different method to use for current_user.
In application controller, add your new method, referring to the current_user instance variable.
def my_cool_method
#current_user
end
And then, in config/initializers/audited.rb, tell Audited to use your new method:
Audited.current_user_method = :my_cool_method
With this change, Audited will still audit the change, but it will not try to set the audit's user (the person who made the change) - that will be nil.
Another advantage of this change over the alternate solution by DGM is that we aren't overriding Devise's current_user method, which is similar to monkey patching in that it could cause unintended consequences later.
Just to close this off.
stack level too deep is caused because devise has built in auditing on the current_user variable.
So every time you access the variable it causes an infinite loop.
Further explaining - acts_as_audited calls current_user before checking what to ignore, and if current_user triggers a table change, calls an audit again, poof. Infinite loop.
My workaround with the same problem with authlogic is to disable auditing while setting up the session:
def current_user
return #current_user if defined?(#current_user)
User.without_auditing do
#current_user = current_user_session && current_user_session.user
end
#current_user
end
However, I still hit some other callbacks that I'd rather not hit. This is authlogic's problem, not act_as_audited's.
Ultimately, I would prefer that the auditing done by devise or authlogic do so in a manner that bypassed validations, callbacks, and timestamps.
Same thing happens with Authlogic alone. Solution is to add the :except argument with the following fields(see below). Perhaps something similar will work with Devise as well.
# Define explicitly otherwise "Stack
Level Too Deep"
acts_as_audited
:except => [ :persistence_token,
:perishable_token, :login_count,
:failed_login_count,
:last_request_at, :current_login_at,
:last_login_at, :current_login_ip,
:last_login_ip ]

Resources