Seamlessly Upgrade a Roll Your Own Authentication System To Devise

04 March 2016 on . 6 minutes to read

Combination Lock

Authentication, encryption and the mathematical side of security are intriguing and incredibly interesting fields. However, they’re systems that you don’t want to implement on your own in a production environment. If you can make something better than the industry, there will be some great signs, like busting curves for every single math test in college and having the NSA offer you scholarships. Those kinds of signs.

Inheriting an app with bad security? Seamlessly upgrade users passwords.

If you want a secured rails app, there’s really only one option, and that’s using Devise. It can be as simple or complex as you need it to be, with a broad range of abilities like password resets, test helpers and controller actions to make your life easier. After all, that’s what brings many of us to Rails in the first place; the ability to quickly create robust applications. Unfortunately, our applications aren’t always sparkling examples of optimal coding practice. On occassions you’ll inherit an application with security flaws that need to be fixed. In this post, I’ll discuss how to take roll your own (or more likely, one that got rolled for you) authentication setup and seamlessly transition it to a Devise secured system.

True Story: A Bad Case of Roll Your Own Security1

A while back, I worked on an app that had Devise. Now, I haven’t looked to see whether Devise was added on top of an existing authentication scheme or if the original developer overrode the default password encryption, but the basic gist of what happened is that everyone’s passwords were salted with the word “password”, and then ‘encrypted’ with a single pass of MD5. Not quite an emergency, but pretty damned close to one.

Almost this bad.

If you’re not familiar with them, Rainbow Tables are awesome. Like most awesome things, they’re incredibly powerful and quite dangerous when aimed at something you care about. They allow you to precalculate encrypted items, and then quickly lookup items. In fact, you can download massive rainbow tables for standard password schemes that will more or less allow you to instantly find a password given you have access to the encrypted version of it. This is why having variables salts and initialization vectors are important. When you have those in place, rainbow tables become essentially useless.2

So, this app was in a very bad place, and needed a fix.

Upgrade Your Passwords Silently

The Devise config in users.rb looked like this:

devise :database_authenticatable, :encryptable, :recoverable,
  :timeoutable, :trackable, :validatable,
  :encryptor => :custom_md5_encryptor

Which made it use the following to encrypt passwords:

module Devise
  module Encryptable
    module Encryptors
      class CustomMD5Encryptor < Base
        def self.digest(password, stretches, salt, pepper)
          string_to_hash = password + "password"

Unfortunately, this is straight out of the example Devise custom encryption how to. Their example is fantastic, because it’s concise and shows the easiest way to roll your own. Unfortunately, the originator of the app’s encryption scheme had no idea of the huge security hole this was. I’ll save my personal rant, because there’s reams upon reams of research papers to look into if you’re interested in the subject. Thankfully, some searching on stackoverflow yielded the answer.

The Fix

The fix for this is fantastically easy. Change user.rb devise config to use the standard encryption.

devise :database_authenticatable, :recoverable,
       :timeoutable, :trackable, :validatable

Then added the following to the user model.

def valid_password?(pwd)
  rescue BCrypt::Errors::InvalidHash
    return false unless Digest::MD5.hexdigest(pwd+"password") == self.encrypted_password "User #{email} is using the old password hashing method, updating attribute."
    self.password = pwd!
alias_method :devise_valid_password?, :valid_password?

And added in this test to user_spec.rb

context "password changes" do
  old_encryption = "391c0bc441a0794ece71cab8d84dc0b5" #Digest::MD5.hexdigest("pwd1234password")
  let!(:user) {FactoryGirl.create(:user, password: "pwd1234", password_confirmation: "pwd1234", encrypted_password: old_encryption)}

  it "Updates the password from the old to the new on a successful login and be able to use the same password." do
    expect(user.devise_valid_password?("pwd1234")).to be_truthy
    expect(user.encrypted_password).to_not eq(old_encryption)
    expect(user.devise_valid_password?("pwd1234")).to be_truthy

Mission accomplished. When Devise checks to see if your password is valid, it will raise a BCrypt::Errors::InvalidHash since the upgraded encryption scheme won’t decrypt successfully for old passwords that MD5’d. From there, if the password that was entered matches the MD5 encrypted version of that password, it simply resaves your password, which converts it to the new encryption scheme. If an incorrect password is entered, it returns false, which prevents login. Seamlessly upgraded.

Final Words: Update Them All

If you find yourself in this situation, I’d recommend sending an email out after a few weeks to anyone who’s not logged in, telling them to simply log in and/or change their password for security purposes. Just find any users which have an updated_at stamp more than a few weeks long, and send them an appropriate password reset link.

  1. Details changed for privacy and security where needed 

  2. Provided someone knows the salt/IV, they can still make a rainbow table for a single user that’s worth the effort such as your root account, but they won’t be able to simultaneously get 90% of the passwords for your users in the time it takes to breathe 

If you enjoy having free time and the peace of mind that a professional is on your side, then you’d love to have me work on your project.

Contact or view a list of available services to see how I’ll make your life better, easier and bring satisfaction back into you running your business.