ruby-on-railsrubyencryptionaccess-tokenrails-api

Rails 5 has_secure_token encryption


In the Ruby on Rails has_secure_token gem/feature, it creates a unique token upon record creation and stores it in the database as plain text. If I am using that token to grant users access to an API, is there a security risk in storing that token as plain text in the database?

I was hoping there would be a way to encrypt the token column when the has_secure_token method commits the token to the database, similar to how bcrypt encrypts passwords into a database.

I have tried using gems such as attr_encrypted to store a hashed value of the token, but it seems to be incompatible with has_secure_token. Here is how my model is currently set up:

class User < ApplicationRecord
  has_secure_token :token
  # attr_encrypted :token, key: :encrypt_token, attribute: 'token'

  # def encrypt_token
  #   SecureRandom.random_bytes(32)
  # end
end

The commented code is attr_encrypted code that has proven to be incompatible. If someone knew if there was a way to safely encrypt a column in the database, while also using has_secure_token, I would greatly appreciate it!

Let me know if any more information is needed or this is confusing. Thanks in advance!


Solution

  • Rails 6 ActiveModel::SecurePassword#has_secure_password accepts an attribute name argument and will use BCrypt to set the value of a corresponding #{attribute}_digest column. The default attribute name is password and the model must have an accessor for a #{attribute}_digest attribute.

    A simplified example with both a password and an api_token:

    rails generate model User password_digest:string api_token_digest:string
    

    Rails 6

    class User < ApplicationRecord
      has_secure_password #create a `password` attribute
      has_secure_password :api_token, validations: false
    
      before_create do
        self.reset_token = SecureRandom.urlsafe_base64
      end
    end
    

    Prior to Rails 6 you can directly invoke BCrypt to encrypt the token.

    require 'bcrypt'
    
    class User < ApplicationRecord
      has_secure_password #create a `password` attribute
    
      before_create do
        cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost    
        self.api_token = SecureRandom.urlsafe_base64
        self.api_token_digest = BCrypt::Password.create(api_token, cost: cost)
      end
    end