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.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With