Recently reviewing few different projects I’ve noticed that method has_secure_token
provided by ActiveRecord is used a lot here and there for different kind of situations where it’s actually not very good idea to use it. Specifically, different scenarios of email / role confirmation or even some user authentication flows.
TL;DR. Please, do not use this method for generating reset password tokens or anything like this. That’s it. Now if you would like to know more about why not and how you could replace it then everything written below is for you.
My guess is that a lot of misunderstanding around has_secure_token is coming from another helper method available in ActiveRecord models and called very-very much similar - has_secure_password
.
As you may assume has_secure_password
provides basically complete flow of password based authentication for the model. Here is the usage example of this method you may typically see:
class User < ApplicationRecord
validates :email, presence: true
has_secure_password
end
user = User.create!(email: "test@example.com", password: "qwerty")
if user.authenticate("qwerty")
# the password is good
end
The best thing about has_secure_password
is that it takes care about security concerns you really would like to address for storing user’s passwords in 2020. Specifically, instead of saving actual password this method under the hood is transforming it into the digest generated by bcrypt and stores only this hash. So even if somebody will steal your entire database they will see only digests and won’t be able to reverse passwords out of hashes.
Good, back to has_secure_token
. Here is the basic example you can meet in a real project:
class User < ApplicationRecord
validates :email, presence: true
has_secure_password
has_secure_token :reset_password_token
end
user = User.create!(email: "test@example.com", password: "qwerty")
user.reset_password_token # some random 24 chars long token
Now open reset password implementation flow and you’ll see there something like:
if user.reset_password_token == params[:reset_password_token]
user.reset_password_token = nil
user.password = params[:password]
user.password_confirmation = params[:password_confirmation]
user.save!
end
The problem here is that in comparison to has_secure_password
method has_secure_token
does pretty much nothing to protect developers from simple mistakes. It stores generated token simply as it is. In plain text. I would call it has_unsecure_token
instead to be honest!
What’s amazing here is that if you ask somebody around is it good to openly store passwords in database the answer most likely will be “no”. In the same moment by some reason same people assume it’s still good enough way of storing the token. Which actually allows to reset our securely digested password. And, in many cases, to get active session out of it. Doesn’t make any sense for me. Doubt it’s true? Then just google has_secure_token
and take a look on examples people are using to demonstrate it.
Here are first 2 links from the top of my google response: here, here. Sure, those texts are written just to highlight the fact that such method exists in ActiveRecord API and they are using resetting password for the sake of example. Any experienced developer would have noticed it without a doubt. But it may hurt a lot of beginners pretty badly!
It’s reasonable to say that such method can be acceptable if there is some few minutes long invalidation period. And in some cases I would agree. But the problem is that has_secure_token
is not taking any responsibility about it as well. In comparison to has_secure_password
this helper is far away from complete production ready implementation. As a result, knowing the facts about has_secure_token
, it’s pretty hard to imagine the situation when it’s actually safe to use it. The only one case I imagine after some thinking is promo code generation or something similar.
Another hard to understand thing for me is why it was decided to go with such poor implementation. Is it hard to write something more or less secure out of the box? What can you do to generate pretty much secure token and validate it?
Well, let’s see. I’ll start from the class which wraps and hides all scary hashing logic from us:
class Token
def self.generate(cost: BCrypt::Engine.cost, valid_till: Time.now.utc + 24.hours)
Secrets::Token.new(SecureRandom.base58(24), valid_till: valid_till, cost: cost)
end
attr_reader :valid_till
def initialize(token, valid_till: nil, cost: BCrypt::Engine.cost)
@token = token
@valid_till = valid_till
@cost = cost
end
def valid?(value)
(@valid_till.nil? || @valid_till.utc > Time.now.utc) && BCrypt::Password.new(value) == @token
end
def to_s
@token.to_s
end
def digest
BCrypt::Password.create(@token.to_s, cost: @cost)
end
end
Now let’s generate our token once user has started forgot password flow:
# /users/password/forgot
user = User.find_by(email: params[:email])
if user.nil?
raise ActiveRecord::RecordNotFound
end
reset_password_token = Token.generate
user.reset_password_token = reset_password_token.digest
user.reset_password_token_valid_till = reset_password_token.valid_till
Important thing to notice here is that I’m storing the digest while validating it against the real token I may have sent in the email as part of the reset password form URL. Basically in the same way has_secure_password
doing it with password_digest and authenticate. Nothing new here.
That’s pretty much it. It’s not a lot of code so I’m publishing it in a form of the snippet instead of packing it into gem. You are free to take it and own it because knowing for sure what happens in such a critical part of you system is very important. Stay of the safe side! See you!