diff --git a/activerecord/lib/active_record/secure_password.rb b/activerecord/lib/active_record/secure_password.rb index cb857e8ead..839d849711 100644 --- a/activerecord/lib/active_record/secure_password.rb +++ b/activerecord/lib/active_record/secure_password.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/except" - module ActiveRecord module SecurePassword extend ActiveSupport::Concern @@ -37,15 +35,17 @@ module ActiveRecord # User.authenticate_by(email: "jdoe@example.com") # => ArgumentError # User.authenticate_by(password: "abc123") # => ArgumentError def authenticate_by(attributes) - passwords = attributes.select { |name, value| !has_attribute?(name) && has_attribute?("#{name}_digest") } + passwords, identifiers = attributes.to_h.partition do |name, value| + !has_attribute?(name) && has_attribute?("#{name}_digest") + end.map(&:to_h) raise ArgumentError, "One or more password arguments are required" if passwords.empty? - raise ArgumentError, "One or more finder arguments are required" if passwords.size == attributes.size + raise ArgumentError, "One or more finder arguments are required" if identifiers.empty? - if record = find_by(attributes.except(*passwords.keys)) + if record = find_by(identifiers) record if passwords.count { |name, value| record.public_send(:"authenticate_#{name}", value) } == passwords.size else - self.new(passwords) + new(passwords) nil end end diff --git a/activerecord/test/cases/secure_password_test.rb b/activerecord/test/cases/secure_password_test.rb index db08eea533..39ce38c139 100644 --- a/activerecord/test/cases/secure_password_test.rb +++ b/activerecord/test/cases/secure_password_test.rb @@ -64,4 +64,16 @@ class SecurePasswordTest < ActiveRecord::TestCase User.authenticate_by(password: @user.password) end end + + test "authenticate_by accepts any object that implements to_h" do + params = Enumerator.new { raise "must access via to_h" } + + assert_called_with(params, :to_h, [[]], returns: { token: @user.token, password: @user.password }) do + assert_equal @user, User.authenticate_by(params) + end + + assert_called_with(params, :to_h, [[]], returns: { token: "wrong", password: @user.password }) do + assert_nil User.authenticate_by(params) + end + end end