Convert attributes to Hash in authenticate_by

Follow-up to #43765.

This ensures that `authenticate_by` supports controller params, e.g.:

```ruby
User.authenticate_by(params.permit(:email, :password))
```

Note that `ActionController::Parameters#to_h` will raise an error when
there are unpermitted params.  This guards against unsafe usage such as:

```ruby
User.authenticate_by(params)
```
This commit is contained in:
Jonathan Hefner 2021-12-05 11:42:16 -06:00
parent 5aeb7d1c40
commit c5bbc81014
2 changed files with 18 additions and 6 deletions

View File

@ -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

View File

@ -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