BLOG
Authentication in Rails 7.1
When securing a web application there are two things that must be taken into consideration: Authentication and Authorization. At a high-level Authentication is “who are you?” and authorization is “are you allowed to be here?”. A great example of this is your driver’s license. You can often use your license to prove who you are (Authentication). But it doesn’t necessarily give you permission to be somewhere (Authorization). If you’re trying to get to a safe deposit box at a bank, your driver’s license may prove who you are, but your bank account lets you into the box.
A while ago, we looked at pundit and cancan for Authorization. Today, we’ll look at a new feature in Rail 7.1 called authenticated_by (and has_secure_password).
TheNew Feature for Authentication in Rails 7.1
has_secure_password
Before we talk about the new feature, we have to talk about has_secure_password
for a moment. This method has been around for a while now. Essentially, you create a model with an attribute password_digest
and then call has_secure_password
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
has_secure_password
# there is an attribute called "password_digest"
end
This allows for some very simple, but secure password-based authentication.
User.find_by(email: "[email protected]")&.authenticate("password123")
authenticated_by
While has_secure_password
does allow for some simple authentication, it also has a pretty solid problem. The code we used to “find_by” will short circuit if no record can be found. Programmatically that is what we want, but it opens you up to a “Timing Based Enumeration Attack”.
In short, the method returns faster if no user exists, and longer if the user exists. Once I know if a user exists, then I can try to get into their account, either by brute forcing, or by trying a number of that user’s known passwords.
That is were authenticated_by
comes in. In a model with has_secure_password
. You can use authenticated_by
to make each check take roughly the same amount of time.
User.authenticate_by(email: '[email protected]', password: 'password123')
It accomplishes this by cryptographically hashing the password attribute, and then doing a lookup based on email and the hashed password. Because it always hashes the password even if the user doesn’t exist there is less variation between a failure because of a bad user and a failure due to an incorrect password.