user8460690
user8460690

Reputation:

How to customize bcrypt?

I have built a working API using rails using this PluralSight blog. In my app for user verification I've used gems like bcrypt and jwt. I am using MySQL as my database and have a User table named users.

Which looks like this:

Current User Table
But then I saw my production Database which looks like this: Desired User Table

Since then I have done quite a research and found out that while using latest version of bcrypt it is must to have password_digest as a column, I cant store Encrypted Password and Password Salt separately.
But the problem is I can't change the schema of existing table. Kindly let me know if there's any work around? Or there exists any better approach to tackle this issue.

Upvotes: 1

Views: 953

Answers (1)

Greg Navis
Greg Navis

Reputation: 2934

It is possible and isn't actually difficult. I'll show you a step-by-step procedure that helps understand how to do that.

How passwords work under the hood

We start by locating has_secure_password. It's defined in ActiveModel::SecurePassword::ClassMethods. ActiveModel::SecurePassword is an Active Model concern responsible for passwords. It's included into ActiveRecord::Base.

If we look at the source code we can see that it includes InstanceMethodsOnActivation. It defines four instance methods:

  1. authenticate - it build BCrypt::Password based on the value returned by password_digest (normally this is a column accessor) and tests whether it matches unencrypted_password.
  2. password - an ordinary attribute reader.
  3. password= - a custom attribute setter. It converts an assignment to password into an assignment to password_digest. Remember that in Ruby all these "assignments" are really method calls. In other words, this method translates a call to #password= into a call to #password_digest=.
  4. password_confirmation= - an ordinary attribute writer.

Based on the above code, you can see that you the model needs to implement two methods to be compatible with Rails secure passwords. These methods are #password_digest and #password_digest=. ActiveModel::SecurePassword doesn't care whether they are backed by database columns! From its perspective, they are perfectly ordinary Ruby methods.

Implementation

We know that #password_digest needs to assemble a BCrypt digest based on the two columns in our database. Conversely, #password_digest= needs to disassemble a BCrypt digest into salt and checksum.

Let's write a simple test case help us ensure the code works. Our goal is to make this test pass.

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  NAME = 'gregnavis'.freeze
  PASSWORD = '1234abcd'.freeze

  test 'passwords work' do
    # First, we store the user in the database to check that the fields are
    # serialized correctly.
    User.create!(name: NAME,
                 password: PASSWORD,
                 password_confirmation: PASSWORD)

    # Second, we reload the model to ensure that it works even if we reset
    # state that isn't serialized to the database.
    user = User.find_by_name!(NAME)

    # Third, let's see whether authentication works.
    assert(user.authenticate(PASSWORD))
    assert_not(user.authenticate("#{PASSWORD}1"))
  end
end

Looking at the source code of #password= we can see the method receives an instance of BCrypt::Password as an argument. The docs for BCrypt::Password list salt (which includes the salt, version, and cost) and checksum as attributes. This means the following implementation should pass the test:

class User < ActiveRecord::Base
  has_secure_password

  def password_digest
    # We need to glue the two parts.
    "#{password_salt}#{password_hash}"
  end

  def password_digest=(digest)
    # We need to split the BCrypt::Password into two parts.
    self.password_salt = digest.salt
    self.password_hash = digest.checksum
  end
end

That's it! The test is green. I also confirmed that joining digest.salt and digest.checksum gives the complete hash. I did that in the console.

Upvotes: 2

Related Questions