How to: Migrate passwords from legacy systems to Devise

Published on . Ruby On Rails Devise

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.

$ rails g migration AddLegacyPasswordToUser legacy_password:boolean
      invoke  active_record
      create    db/migrate/20120508083355_add_legacy_password_to_users.rb
$ rake db:migrate

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

Senior full-stack engineer with 5 years of experience building web applications for clients all over the world.

Interested in working together?

Find out what I can do for you or get in touch!

Like this? Sign up to get regular updates