Usage of authenticate_by for preventing timing-based enum attacks ⏳ 🙌🏼
  • Add authenticate_by when using has_secure_password.

    authenticate_by is intended to replace code like the following, which returns early when a user with a matching email is not found:

    User.find_by(email: "...")&.authenticate("...")
    

    Such code is vulnerable to timing-based enumeration attacks, wherein an attacker can determine if a user account with a given email exists. After confirming that an account exists, the attacker can try passwords associated with that email address from other leaked databases, in case the user re-used a password across multiple sites (a common practice). Additionally, knowing an account email address allows the attacker to attempt a targeted phishing (“spear phishing”) attack.

    authenticate_by addresses the vulnerability by taking the same amount of time regardless of whether a user with a matching email is found:

    User.authenticate_by(email: "...", password: "...")
    

Reference - Rails PR #43765

Code snippet 📌

# Version: Rails 7.0 RC1

class User < ActiveRecord::Base
  has_secure_password
end

User.create(name: "Rishi Pithadiya", email: "rishi@example.com", password: "abc123!@#")

User.authenticate_by(email: "rishi@example.com", password: "abc123").name
 "Rishi Pithadiya"

User.authenticate_by(email: "rishi@example.com", password: "wrong")
 nil

User.authenticate_by(email: "wrong@example.com", password: "abc123")
 nil

User.authenticate_by(email: "rishi@example.com")
 ArgumentError

User.authenticate_by(password: "abc123!@#")
 ArgumentError