Reputation:
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:
But then I saw my production Database which looks like this:
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
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.
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:
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
.password
- an ordinary attribute reader.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=
.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.
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