ActiveRecord has a has_secure_token method to generate secure random tokens since Rails 5. It generates a base58 (URL friendly) string of 24 characters and it’s an easy way add access tokens to a Rails application. However, what happens if a backup of the database is compromised? The tokens might be as sensitive as passwords, but unlike ActiveRecord has_secure_password passwords, they are saved in clear in the database.

For tokens that need to be displayed after they’ve been generated (like API tokens that user can retrieve from their account), an easy solution is to use the attr_encrypted gem.

class User
  attr_encrypted :api_token

  before_create :regenerate_api_token!

  def regenerate_api_token!
    self.api_token = SecureRandom.base58(24)
  end
end

Note: please consider how to rotate the encryption key and saving encryption mode for the future.

You should also NOT use == when verifying the token as it opens the code to a side-channel timing attack. Instead, use a time-constant secure comparison: ActiveSupport::SecurityUtils.secure_compare(provided_token, api_token).

This encrypts the token in the database and decrypts it on demand in the application. However, for one-time tokens sent to validate an email address or reset a password, there is no need to ever decrypt the token again. In fact, for some highly-security-sensitive tokens, it might be a good idea to only display them in full when they are generated. Afterward, you could display only the tokens last 4 characters (a bit like credit card numbers) to the user and force to generate new ones when required.

In this scenario, it makes sense to use a one-way cryptographic HMAC. One might think to use a password hashing function (like Bcrypt for has_secure_password). However, unlike passwords, tokens have a high entropy (> 128 bits in our example) and there is no need to defend against brute-force cracking.

class User
  attr_accessor :api_token

  before_create :regenerate_api_token!

  def token_hmac(token)
    OpenSSL::HMAC.hexdigest(
      'SHA256',
      Rails.application.credentials.token_key,
      token
    )
  end

  def regenerate_api_token!
    api_token = SecureRandom.base58(24)
    self.api_token_digest = token_hmac(api_token)
    
    self.api_token = api_token
  end

  def valid_api_token?(provided_token)
    ActiveSupport::SecurityUtils.secure_compare(
      token_hmac(provided_token),
      api_token_digest
    )
  end
end

Obviously, for production code, you should consider extracting the code in a concern and refactor it and again as for attr_encrypted, please consider saving the cryptographic metadata (HMAC algorithm used, key version, etc) for key rotations in the future.

Finally, for low-entropy data (like emails, social security numbers, phone numbers) that you need to be able to compare (e.g.: against a user-provided value), consider using the blind index gem as it defends against brute-force cracking but without salting.