How to: Migrate passwords from legacy systems to Devise

Published on 13 May 2012

When you’re migrating from a custom authentication solution to Devise, it’s highly likely your users’ passwords are hashed or stored differently from how Devise does it.

If the legacy system has a sane, secure solution then you can implement a custom encryptor for Devise. However if that’s not the case or if you want to migrate to the Devise default way-of-life following convention over configuration and reducing maintenance headaches in the future, you should convert the passwords so that Devise can use them.

Since hopefully you stored hashes of your user’s passwords instead of encrypted or plaintext, the only moment you can retrieve the plaintext password for rehashing is when the user tries to log in.  At that point we’ll convert it to Devise. This way when a user logs in his legacy password will be automatically converted to the new system.

First, add the legacy_password boolean attribute to User. This is to mark the passwords that have already been converted. Make sure to set it to true for the existing (legacy) records.
<pre lang=”bash”>$ rails g migration AddLegacyPasswordToUser legacy_password:boolean
invoke active_record
create db/migrate/20120508083355_add_legacy_password_to_users.rb
$ rake db:migrate</pre>
Devise creates the method User#valid_password? to check the password. Let’s override it and add our legacy password check or fall through to the default valid_password? if the password was already converted.  This code assumes the old password is stored in the same field as the converted password — encrypted_password.
“`ruby
class User < ActiveRecord::Base

#…

def valid_password?(password)
if self.legacy_password?
# Use Devise’s secure_compare to avoid timing attacks
if Devise.secure_compare(self.encrypted_password, User.legacy_password(password))

    self.password = password
    self.password_confirmation = password
    self.legacy_password = false
    self.save!

  else
    return false
  end
end

super(password)   end

# Put your legacy password hashing method here
def self.legacy_password(password)
return Digest::MD5.hexdigest(“#{password}-salty-herring”);
end
end
“`
Supporting two different systems like this could soon become a maintenance nightmare After a couple of months, when the most active users have logged in at least once and converted their passwords, you can easily remove the extra code. Legacy users will be forced to reset their password since it won’t match anymore. Using the legacy_user attribute you could send them a targeted mail or alert them of the fact that they won’t be able to log in anymore with their old password.

David Verhasselt

I’m a consultant & entrepreneur. I build webapps and optimize Minimal Viable Products for clients all over the world. If you'd like to chat, hit me up on david@crowdway.com.

Like this? Sign up to get regular updates
David
Verhasselt