cercxtrova
cercxtrova

Reputation: 1673

How to override the ActiveModel::SecurePassword.authenticate method?

I'm trying to override the authenticate method in this file: https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activemodel/lib/active_model/secure_password.rb#L61

I have to make sure that if a user without password_digest tries to authenticate, the method returns false instead of raising BCrypt::Errors::InvalidHash

I have to do this because, in the past, we had another authentication system, and many users have a null password_digest. the authenticate method is used in different places and we cannot put an empty password_digest to the users without a password_digest.

I tried this:

# config/initializers/secure_password.rb

module ActiveModel
  module SecurePassword
    class InstanceMethodsOnActivation
      def authenticate(unencrypted_password)
        begin
          BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
        rescue BCrypt::Errors::InvalidHash => e
          false
        end
      end
    end
  end
end

This is my test:

it 'should authenticate with false response if no digest (legacy password system)' do
  user.activate!
  user.update(password_digest: nil)
  expect(user.authenticate('test')).to be false
end

And the error message:

BCrypt::Errors::InvalidHash: invalid hash

  0) User should authenticate with false response if no digest (legacy password system)
     Failure/Error: expect(user.authenticate('test')).to be false

     BCrypt::Errors::InvalidHash:
       invalid hash

I update our app to Rails 6 and SecurePassword has been rewritten, I can't change the method again. I think the problem is that the authenticate method is now defined within the InstanceMethodsOnActivation's initialize method. I find it strange... And I don't know how to modify it.

Please anyone could help me?

Upvotes: 0

Views: 167

Answers (1)

Jay-Ar Polidario
Jay-Ar Polidario

Reputation: 6603

I think you don't need to monkey-patch the gem implementation, because I think the super-trick would already be sufficient enough in your case:

class User < ApplicationRecord
  def authenticate(*args)
    if password_digest.nil? # or `.blank?` (if you have '' as default instead of nil)
      false
    else
      # else, proceed normally
      super(*args)
      # `super` alone also works, as it automatically pass in all arguments
    end
  end
end

Sample Usage:

user = User.new
user.password_digest = nil

puts user.authenticate('somepassword')
# => false

Regarding Your "concerns" problem:

  • It should look like the following:
# app/models/concerns/your_own_named_concern.rb

module YourOwnNamedConcern
  extend ActiveSupport::Concern

  included do
    def authenticate(*args)
      if password_digest.nil?
        false
      else
        super(*args)
      end
    end
  end
end

# app/models/user.rb
class User < ApplicationRecord
  include YourOwnNamedConcern

  has_secure_password
end

# app/models/admin.rb
class Admin < ApplicationRecord
  include YourOwnNamedConcern
end

Sample Usage:

user = User.new
user.password_digest = nil

puts user.authenticate('somepassword')
# => false

admin = Admin.new
admin.password_digest = nil

puts admin.authenticate('somepassword')
# => false

Upvotes: 1

Related Questions