Rails adds `authenticate_by` method when using `has_secure_password`


In this exponentially growing digital world, everybody is worried about one thing.

Yes, it is SECURITY.

The need for a security is inevitable in today’s cyber ecosystem of phishing and breaches.

The fundamental mechanism used by most websites to achieve security is Authentication. In simple terms, the user/customer needs to log in using a username/email and password to access various contents of the website.

Before:

Let’s say, we have a Customer model with the email attribute. It has a record with the email "richard_roe@example.com". We can create this customer record as follows:

Customer.create(name: "Richard Roe", email: "richard_roe@example.com", password: "password123")

Note: We are using the following code snippet to authenticate the customer. The following code returns early when a customer with a matching email is not present.

Customer.find_by(email: "richard_roe@example.com")&.authenticate("password123")

Now, the above code snippet is vulnerable to timing-based enumeration attacks, wherein an attacker can determine if a customer with the given email exists or not.

It is a common human tendency to re-use the same password across multiple websites instead of using password manager applications.

After confirming that an account exists in the database, the attacker can try a password associated with that same email address from other leaked databases over the world wide web. If an account email address is known, it allows the attacker to attempt a targeted brute force or phishing (“spear-phishing”) attack as well.

After:

Rails introduces a new class method authenticate_by.

Customer.authenticate_by(email: "richard_roe@example.com", password: "password123")

authenticate_by will cryptographically digest the given password attributes, which helps mitigate timing-based enumeration attacks. This method finds a record using the non-password attributes and then authenticates that record using the password attributes.

It returns the record, if authentication succeeds; otherwise, it returns nil.

class Customer < ActiveRecord::Base
  has_secure_password
end

Customer.create(name: "Richard Roe", email: "richard_roe@example.com", password: "password123")

Customer.authenticate_by(email: "richard_roe@example.com", password: "password123").name
# => "Richard Roe"

Customer.authenticate_by(email: "richard_roe@example.com", password: "invalid_password")
# => nil

Customer.authenticate_by(email: "invalid@example.com", password: "password123")
# => nil

This method raises an ArgumentError if the set of attributes doesn’t contain at least one password attribute and one non-password attribute.

Customer.authenticate_by(email: "richard_roe@example.com")
# => ArgumentError

Customer.authenticate_by(password: "password123")
# => ArgumentError

Check out the PR (including #43779, #43958, and #43997) that made this happen!

Join Our Newsletter