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

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.

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.

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

Customising Whodunnit information for Rails Admin using Paper Trail

I have been able to correctly append the name of a user into the Whodunnit field for actions by adding this to my application_controller
def user_for_paper_trail
current_user ? current_user.name : 'Public user'
end
This works fine.
If a user makes a change using Rails Admin however, the whodunnit is still set to the user id.
Is there a method I can call that would tell paper trail what detail I want to store when a record is being updated via Rails Admin?
RailsAdmin has had some issues with this. You will find the following helpful: Display custom label for User in rails_admin paper_trail history.
Also, because the field is supposed to store IDs (even though it's a string field), I'd keep the IDs and store something nonsensical for the 'Public user' such as '0' (referenced by a constant, of course). I would then override the whodunnit method to return 'Public user' in case super == '0'.
That approach would be particularly helpful if you already have a bunch of versions with IDs in their whodunnit fields or if you have multiple users with the same name.
P.S. To quickly confirm that RailsAdmin is completely ignoring / not ignoring the controller method you've defined, just force-raise an error in the method.

customizing Devise's authenticate and current_user methods to work with soft delete / acts as paranoid

I am using Devise as authentication for a rails 3.2 app. My user model is called User, and I've been trying to implement a soft delete. I am trying to do this by using acts_as_paranoid, which automatically does the work for you. Things work as expected (others can no longer see the "deleted" user), except that I want the deactivated user to still be able to log in and see a "deactivated" screen, and give them the chance to reactivate their account.
The problem is that Devise (Warden?) is no longer able to find the deleted user. Acts_as_paranoid lets you access the soft deleted records if you use the scope "with_deleted". So I am able to get part of the way there with:
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
where(conditions).with_deleted.first
end
I put this into my user model, and so now when I input the log in information, I'll get the flash message that I have successfully logged in, and it'll touch the "updated_at" column in the user model and increment the sign_in_count, etc. However, it doesn't really authenticate in the sense that the authenticated method returns false and the current_user helper method returns nil. So what is my best strategy to get these working? Can I override the current_user method somehow so that it queries the User model with the with_deleted scope? Do I have to do something with warden, such as added conditions like I did with the find_first_by_auth_conditions method? I cannot figure out how to do this. Any help is appreciated!
also, after I do get it working, I would like to automatically send all soft deleted users to a "deactivated" page where their only options are to permanently delete or reactivate. Is there some way to do this with routing via the "authenticated :user do {} end", or do i have to put a before_filter in the application_controller and check for the users at every request?
I can answer your last question, about routing to the "deactivated page". You can use a lambda to isolate different types of users, like this:
authenticated :user, lambda {|u| u.deactivated? } do
root :to => 'application#deactivated'
end
authenticated :user, lambda {|u| u.active? } do
root :to => 'application#active'
end
Thanks to a tip from the Devise group, it turns out I needed to override the serialize_from_session method, which is inside the authenticatable module and looks like this:
def serialize_from_session(key, salt)
record = to_adapter.get(key)
record if record && record.authenticatable_salt == salt
end
I'd been trying with no success to override modules using initializers (I was trying to override existing strategies, and also to try writing a new one for warden); but I kept getting weird name errors. I still haven't figured that out. However, with the tip, I went ahead and overrode the method inside my User model. Since I don't foresee using some other resource, I didn't mind just editing it like this. The code was just:
def self.serialize_from_session(key, salt)
record = with_deleted.find(key).first
record if record && record.authenticatable_salt == salt
end
This skips the to_adapter.get method altogether (which, to anyone interested, is in orm_adapter, not devise; this took me awhile to find as I was thinking I needed to override this). This probably isn't the cleanest way to do this, but it works well enough for me. And even if I do have a different resource, this should only override the User resource, so I think everything would work fine otherwise.
If problems crop up, I'll add them here. If not, hopefully this helps someone in some way. Certainly took me long enough to get to the bottom of!

Rails 3 + Devise: user_signed_in? for a different user in the database?

I'm building an app where I want to add an online status for a given user.
I know that Devise has a method user_signed_in? built in to check if the user who is using the app is signed in or not. But when I try to use it for a different user like this:
user_signed_in?(user)
user.user_signed_in?
I obviously get an undefined method error.
Does Devise have a method for this or do I have to write my own?
One approach was to store the online status of a given user in the user model.
What's the best solution to this?
I have used Devise on my applications and experienced some of the same problems as you when I first began working with it. You are merely using the helper methods incorrectly. If you'd like to check if the current user has a session and is signed in, you use the helper as such:
if user_signed_in?
which is essentially the same statement as:
if !current_user.nil? && current_user.signed_in
If you'd like to check if a user object is signed in, then you call this: (where user is a User Model Object)
if user.signed_in?
I'm not the author of Devise, but from what I can tell of Warden / Devise neither keep track of who is logged in.
The problem with having an is_online column in the User table is that it is difficult to see who is active on the website. I would add a column to your User model called last_seen as a date-time, and update that with Devise every time the user requests a page. You could then easily add a User.online_count method or also see if a user has been seen at the website in the last 5 minutes.
Use devise_for :user in your routes.rb.

Resources