Best way to create unique token in Rails?

Slick23 picture Slick23 · May 16, 2011 · Viewed 118.8k times · Source

Here's what I'm using. The token doesn't necessarily have to be heard to guess, it's more like a short url identifier than anything else, and I want to keep it short. I've followed some examples I've found online and in the event of a collision, I think the code below will recreate the token, but I'm not real sure. I'm curious to see better suggestions, though, as this feels a little rough around the edges.

def self.create_token
    random_number = SecureRandom.hex(3)
    "1X#{random_number}"

    while Tracker.find_by_token("1X#{random_number}") != nil
      random_number = SecureRandom.hex(3)
      "1X#{random_number}"
    end
    "1X#{random_number}"
  end

My database column for the token is a unique index and I'm also using validates_uniqueness_of :token on the model, but because these are created in batches automatically based on a user's actions in the app (they place an order and buy the tokens, essentially), it's not feasible to have the app throw an error.

I could also, I guess, to reduce the chance of collisions, append another string at the end, something generated based on the time or something like that, but I don't want the token to get too long.

Answer

Krule picture Krule · Aug 24, 2012

-- Update --

As of January 9th, 2015. the solution is now implemented in Rails 5 ActiveRecord's secure token implementation.

-- Rails 4 & 3 --

Just for future reference, creating safe random token and ensuring it's uniqueness for the model (when using Ruby 1.9 and ActiveRecord):

class ModelName < ActiveRecord::Base

  before_create :generate_token

  protected

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless ModelName.exists?(token: random_token)
    end
  end

end

Edit:

@kain suggested, and I agreed, to replace begin...end..while with loop do...break unless...end in this answer because previous implementation might get removed in the future.

Edit 2:

With Rails 4 and concerns, I would recommend moving this to concern.

# app/models/model_name.rb
class ModelName < ActiveRecord::Base
  include Tokenable
end

# app/models/concerns/tokenable.rb
module Tokenable
  extend ActiveSupport::Concern

  included do
    before_create :generate_token
  end

  protected

  def generate_token
    self.token = loop do
      random_token = SecureRandom.urlsafe_base64(nil, false)
      break random_token unless self.class.exists?(token: random_token)
    end
  end
end