Reputation: 5897
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.
Upvotes: 167
Views: 136140
Reputation: 6476
-- Update EOY 2022 --
It's been some time since I answered this. So much so that I've not even taken a look at this answer for ~7 years. I have also seen this code used in many organizations that rely on Rails to run their business.
TBH, these days I wouldn't consider my earlier solution, or how Rails implemented it, a great one. Its uses callbacks which can be PITA to debug and is pessimistic 🙁 in nature, even though there is a very low chance of collision for SecureRandom.urlsafe_base64
. This holds true for both long and short-lived tokens.
What I would suggest as a potentially better approach is to be optimistic 😊 about it. Set a unique constraint on the token in the database of choice and then just attempt to save it. If saving produces an exception, retry until it succeeds.
class ModelName < ActiveRecord::Base
def persist_with_random_token!(attempts = 10)
retries ||= 0
self.token = SecureRandom.urlsafe_base64(nil, false)
save!
rescue ActiveRecord::RecordNotUnique => e
raise if (retries += 1) > attempts
Rails.logger.warn("random token, unlikely collision number #{retries}")
retry
end
end
What is the result of this?
SecureRandom.urlsafe_base64(32, false)
?).-- 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
Upvotes: 349
Reputation: 1643
Rails 7, has this functionality baked in. See the example below:
# Schema: User(token:string, auth_token:string)
class User < ActiveRecord::Base
has_secure_token
has_secure_token :auth_token, length: 36
end
user = User.new
user.save
user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
user.auth_token # => "tU9bLuZseefXQ4yQxQo8wjtBvsAfPc78os6R"
user.regenerate_token # => true
user.regenerate_auth_token # => true
Upvotes: 3
Reputation: 2444
I think token should be handled just like password. As such, they should be encrypted in DB.
I'n doing something like this to generate a unique new token for a model:
key = ActiveSupport::KeyGenerator
.new(Devise.secret_key)
.generate_key("put some random or the name of the key")
loop do
raw = SecureRandom.urlsafe_base64(nil, false)
enc = OpenSSL::HMAC.hexdigest('SHA256', key, raw)
break [raw, enc] unless Model.exist?(token: enc)
end
Upvotes: -1
Reputation: 1951
Try this way:
As of Ruby 1.9, uuid generation is built-in. Use the SecureRandom.uuid
function.
Generating Guids in Ruby
This was helpful for me
Upvotes: 7
Reputation: 80041
There are some pretty slick ways of doing this demonstrated in this article:
My favorite listed is this:
rand(36**8).to_s(36)
=> "uur0cj2h"
Upvotes: 30
Reputation: 207
you can user has_secure_token https://github.com/robertomiranda/has_secure_token
is really simple to use
class User
has_secure_token :token1, :token2
end
user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
Upvotes: 6
Reputation: 1461
This might be a late response but in order to avoid using a loop you can also call the method recursively. It looks and feels slightly cleaner to me.
class ModelName < ActiveRecord::Base
before_create :generate_token
protected
def generate_token
self.token = SecureRandom.urlsafe_base64
generate_token if ModelName.exists?(token: self.token)
end
end
Upvotes: 36
Reputation: 145
def generate_token
self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
Upvotes: 1
Reputation: 1880
To create a proper, mysql, varchar 32 GUID
SecureRandom.uuid.gsub('-','').upcase
Upvotes: 5
Reputation: 5335
Ryan Bates uses a nice little bit of code in his Railscast on beta invitations. This produces a 40 character alphanumeric string.
Digest::SHA1.hexdigest([Time.now, rand].join)
Upvotes: 51
Reputation: 5961
This may be helpful :
SecureRandom.base64(15).tr('+/=', '0aZ')
If you want to remove any special character than put in first argument '+/=' and any character put in second argument '0aZ' and 15 is the length here .
And if you want to remove the extra spaces and new line character than add the things like :
SecureRandom.base64(15).tr('+/=', '0aZ').strip.delete("\n")
Hope this will help to anybody.
Upvotes: 13
Reputation: 3298
If you want something that will be unique you can use something like this:
string = (Digest::MD5.hexdigest "#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}")
however this will generate string of 32 characters.
There is however other way:
require 'base64'
def after_create
update_attributes!(:token => Base64::encode64(id.to_s))
end
for example for id like 10000, generated token would be like "MTAwMDA=" (and you can easily decode it for id, just make
Base64::decode64(string)
Upvotes: 17