The Devise and “attr_encrypted” Gem Integration Headache

The Devise and “attr_encrypted” Gem Integration Headache

In the Ruby on Rails world, the devise gem is a widespread way to handle user authentication, registration, confirmation, invitations, and more. Another fairly widespread gem is attr_encrypted, which is great for encrypting model attributes in the database for security/liability reasons.

The Problem:


Unfortunately, these gems do not play nicely together when you want to encrypt the email attribute on a devise-based user model. Devise uses the email attribute by default for lookups and verification purposes internally, but attr_encrypted replaces the “email” column with “encrypted_email.” Expect many COLUMN NOT EXISTS errors.

The Solution:


Monkey-patching. Devise attaches many methods to the model it is used on, in our case, the User model, and it’s only a few of these methods which are broken by using attr_encrypted on the email. By overriding these methods with our attr_encrypted compatible methods, we can make the two gems play nicely together.

Which methods you’ll need to override will depend on which parts of devise you’re using, so be prepared to just test every devise flow you have and read through stacktraces, but for your convenience, here are the methods we needed to tweak:

def email_changed?
encrypted_email_changed?
end

def self.find_for_authentication(tainted_conditions)
User.find_by_email(tainted_conditions[:email])
end

def self.find_first_by_auth_conditions(tainted_conditions, opts={})
if tainted_conditions[:email]
User.find_by_email(tainted_conditions[:email])
else
to_adapter.find_first(devise_parameter_filter.filter(tainted_conditions).merge(opts))
end
end

def password_required?
!persisted? || !password.nil? || !password_confirmation.nil?
end
def email_was
User.decrypt_email(encrypted_email_was)
end

def apply_to_attribute_or_variable(attr, method)
if attr.to_s == "email"
return self.email.try(method)
end
if self[attr]
self[attr].try(method)

# Use respond_to? here to avoid a regression where globally
# configured strip_whitespace_keys or case_insensitive_keys were
# attempting to strip! or downcase! when a model didn't have the
# globally configured key.
elsif respond_to?(attr)
send(attr).try(method)
end
end

As you can see if you compare these methods to their devise counterparts, we mostly were keeping the same functionality, but just adding an if condition for the email, which now needed to be handled differently. Of note, we had to explicitly use

User.find_by_email()
instead of devise’s

 find_first()

method because attr_encrypted only overrides the

find_by_email()

helper.

These two gems individually make life a lot easier, but when taken together, caused a big headache. Hopefully, this helps.

To learn more about how we do data science at People Pattern, check out our post by Elias Ponvert, Director of Data Science, here, or request a demo of the platform below.