1

I have an API built with Roda + Sequel stack. Here is how my User model looks like:

# frozen_string_literal: true

# {User} is model responsible for storing {User} authentication informations like email and password.
#
# @!attribute id
#   @return [UUID] ID of the {User} in UUID format.
#
# @!attribute email
#   @return [String] Email of the {User}, it's stored in the PostgreSQL citext column.
#
# @!attribute password_hash
#   @return [String] {User} hashed password with bcrypt.
#
# @!attribute created_at
#   @return [DateTime] Time when {User} was created.
#
# @!attribute updated_at
#   @return [DateTime] Time when {User} was updated
class User < Sequel::Model
  # It returns instance BCrypt::Password based on value in password_hash column.
  #
  # @return [BCrypt::Password ] based on value in password_hash column.
  #
  # @example Get {User} password hash:
  #   User.new(password: 'test').password #=> "$2a$12$FktSw7HPYEUYSPBdmmsiXe26II6UV5gvyn2ECwOflTYHP94Hrm2mS"
  def password
    @password ||= BCrypt::Password.new(password_hash)
  end

  # It sets password_hash column with hashed user password.
  #
  # @return [String] user password hash.
  #
  # @example Set {User} password:
  #   User.new(password: 'test').password_hash #=> "$2a$12$FktSw7HPYEUYSPBdmmsiXe26II6UV5gvyn2ECwOflTYHP94Hrm2mS"
  def password=(new_password)
    @password = BCrypt::Password.create(new_password)

    self.password_hash = @password
  end
end

and I have the following test:

describe 'update user password' do
  let(:params) { { password: 'new-password' } }

  before do
    put '/api/v1/update_password', params

    user.reload
  end

  it 'returns 200 HTTP status' do
    expect(response.status).to eq 200
  end
   
  This one is failing.
  it 'updates user password' do
    expect(user.password == 'new-password').to eq true
  end
  
  # This one is passing.
  it 'updates user password' do
    expect(BCrypt::Password.new(user.password_hash).is_password?('new-password')).to eq true
  end
end

This example is failing:

  it 'updates user password' do
    expect(user.password == 'new-password').to eq true
  end
 

but this one is passing:

 it 'updates user password' do
    expect(BCrypt::Password.new(user.password_hash).is_password?('new-password')).to eq true
  end

Could someone explain to me why my first example is failing?

Mateusz Urbański
  • 7,352
  • 15
  • 68
  • 133
  • [`is_password?`](https://github.com/codahale/bcrypt-ruby/blob/fc652e5248a4132af2c5f5c0b61eeceff02f4316/lib/bcrypt/password.rb#L68) is an alias for `==` so both should pass. What do `user.password` and `BCrypt::Password.new(user.password_hash)` return? (from within your RSpec examples) – Stefan Dec 28 '20 at 12:08
  • There is this sequel plugin https://github.com/mlen/sequel_secure_password. It hasn't been updated in a while but it seems to be working. – Eyeslandic Dec 28 '20 at 21:22

2 Answers2

2

Rewrite this one

it 'updates user password' do
  expect(user.password == 'new-password').to eq true
end

to this one

it 'updates user password' do
  expect(user.password).to eq(BCrypt::Password.create('new-password'))
end

Your ruby object User contains hashed version of that password

user.password
=> "$2a$12$FktSw7HPYEUYSPBdmmsiXe26II6UV5gvyn2ECwOflTYHP94Hrm2mS"
'$2a$12$FktSw7HPYEUYSPBdmmsiXe26II6UV5gvyn2ECwOflTYHP94Hrm2mS' == 'new-password'
=> false

and in order to compare them you have to bcrypt your 'new-password' and compare two hashes. You can easily crypt any string with bcrypt, but decrypt it will take much time (years). It is common before verifying password, encrypt your input password with the same algorithm that your database contains

zhisme
  • 2,368
  • 2
  • 19
  • 28
1

You're answering your question yourself: you're hashing the password before it's saved (in the password setter in User.rb:

  # It sets password_hash column with hashed user password.
  #
  # @return [String] user password hash.
  #
  # @example Set {User} password:
  #   User.new(password: 'test').password_hash #=> "$2a$12$FktSw7HPYEUYSPBdmmsiXe26II6UV5gvyn2ECwOflTYHP94Hrm2mS"
  def password=(new_password)
    @password = BCrypt::Password.create(new_password)

    self.password_hash = @password
  end

In your first test, you're trying to compare a hashed password to an unhashed string.

Cassandra S.
  • 750
  • 5
  • 20