DEV Community

Sergey Tsvetkov
Sergey Tsvetkov

Posted on

Can I use has_secure_token in ActiveRecord?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Oldest comments (0)