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 -
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: "email@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: "firstname.lastname@example.org", 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_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!