1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

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 # frozen_string_literal: true
require "active_support/core_ext/hash/except"
module ActiveRecord module ActiveRecord
module SecurePassword module SecurePassword
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -37,15 +35,17 @@ module ActiveRecord
# User.authenticate_by(email: "jdoe@example.com") # => ArgumentError # User.authenticate_by(email: "jdoe@example.com") # => ArgumentError
# User.authenticate_by(password: "abc123") # => ArgumentError # User.authenticate_by(password: "abc123") # => ArgumentError
def authenticate_by(attributes) 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 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 record if passwords.count { |name, value| record.public_send(:"authenticate_#{name}", value) } == passwords.size
else else
self.new(passwords) new(passwords)
nil nil
end end
end end

View file

@ -64,4 +64,16 @@ class SecurePasswordTest < ActiveRecord::TestCase
User.authenticate_by(password: @user.password) User.authenticate_by(password: @user.password)
end end
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 end