Best way to create unique token in Rails?
Asked Answered
S

12

165

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.

Silassilastic answered 16/5, 2011 at 17:57 Comment(0)
E
349

-- 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?

  • One query less as we are not checking for the existence of the token beforehand.
  • Quite a bit faster, overall because of it.
  • Not using callbacks, which makes debugging easier.
  • There is a fallback mechanism if a collision happens.
  • A log trace (metric) if a collision does happen
    • Is it time to clean old tokens maybe,
    • or have we hit the unlikely number of records when we need to go to 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
Exophthalmos answered 16/5, 2011 at 17:57 Comment(19)
@Exophthalmos shouldn't it be break unless instead of break if? also the token var is quite shadowed I think – Ref
this exact code won't work since random_token is scoped within the loop. – Bodgie
@Exophthalmos Now that you have turned this into a Concern, shouldn't you also get rid of the ModelName in the method? Maybe replace it with self.class instead? Otherwise, it is not very reusable, is it? – Aft
@Krule, why are we not using SecureRandom.uuid here ? – Carbonize
@Jashwant, I have used urlsafe_base64 here in order to demonstrate a principle. There are no reasons against using uuid that I am aware of. – Exophthalmos
@Krule, thanks for quick reply. I am new to Rails and thus had curiosity. +1 to great answer – Carbonize
@Jashwant, ah yes. uuid is of fixed lenght, while urlsafe_base64(n) lenght is approx 4/3 of n, which can be important if you are, for any reason, limited in regards to token size – Exophthalmos
@Exophthalmos One small question... Will it make things slow in a big database while trying to go through all ids? – Rist
@EApubs Just to be clear, how big database table do you have in mind. If it's "only" couple of billion records in the table, that is not big and should work just as any other select. If you index token, as you should, it would be just fine. If you are talking "big data" range (generating several thousands records every second) I would not use ActiveRecord, or Rails for that matter, to begin with. – Exophthalmos
That update to ActiveRecords secure token IMHO is not as correct as previous answers, because it does not handle a collision at all. Although collisions are highly unlikely, they could still occur, which then will cause the record creation to fail. I am sticking with Edit 2. – Negligent
Likelihood of collision happening is so incredibly small that I have to agree with ActiveRecord team implementation decision. Also, you can provide fallback mechanism in the form of rescue ActiveRecord::RecordInvalid => e. – Exophthalmos
You have mention that as of January 9th, 2015 it is better to use has_secure_token method but when I add it to my model I am getting NoMethodError. In which version of rails was it implemented? – Mazdaism
self.token = loop do simple doesn't work. loop doesn't return anything for me. – Thomasinathomasine
@JoãoPauloMotta loop does not return any value or it does not break? – Exophthalmos
@Exophthalmos loop returned nil. I had to declare the variable outside the loop and assign its value inside. – Thomasinathomasine
The new approach with has_secure_token, is available on which rails version? – Bernadinebernadotte
@AdrianMatteo: It is currently available in master and scheduled for Rails 5. – Exophthalmos
@AdrianMatteo well, you can copy the module from GitHub and use it now in Rails 4 app :) It's only another concern. – Exophthalmos
The solution is not deprecated, Secure Token it is simply implemented in Rails 5, but it can't be used in Rails 4 or Rails 3 (which this question relates to) – Murine
A
51

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)
Aerostatics answered 3/1, 2012 at 3:24 Comment(7)
Yeah, that's not bad. I'm usually looking for much shorter strings, to use as part of an URL. – Silassilastic
Yeah, this is at least easy to read and understand. 40 characters is good in some situations (like beta invites) and this is working well for me so far. – Aerostatics
@Silassilastic You can always grab a portion of the string also: Digest::SHA1.hexdigest([Time.now, rand].join)[0..10] – Picturesque
I use this to obfuscate IP addresses when sending the "client id" to Google Analytics' measurement protocol. It's supposed to be a UUID, but I just take the first 32 chars of the hexdigest for any given IP. – Pattypatulous
For a 32-bit IP address, it would be fairly easy to have a lookup table of all of any possible hexdigest generated by @thekingoftruth, so don't anyone go thinking that even a substring of the hash will be irreversible. – Judah
@Judah Absolutely. It's far from irreversible, and I would not use it for more important obfuscations. – Pattypatulous
is their a way to make it to 60 characters? – Decoupage
C
36

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
Candlefish answered 3/4, 2014 at 13:33 Comment(0)
D
30

There are some pretty slick ways of doing this demonstrated in this article:

https://web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/2/creating-small-unique-tokens-in-ruby

My favorite listed is this:

rand(36**8).to_s(36)
=> "uur0cj2h"
Dyal answered 16/5, 2011 at 18:28 Comment(9)
It looks like the first method is similar to what I'm doing, but I thought rand wasn't database agnostic? – Silassilastic
And I'm not sure I follow this: if self.new_record? and self.access_token.nil? ... is that what's checking to make sure the token isn't already stored? – Silassilastic
That code isn't in a SQL query, but Ruby β€” it doesn't matter which DB you are using. As far as the conditional, it just generates once per record when it is created, but you can do it however works best for your application. – Dyal
This could actually produce 2 identical tokens. You would need other checks against existing tokens to prevent an eventual issue. – Boldface
You will always need additional checks against existing tokens. I didn't realize that this wasn't obvious. Just add validates_uniqueness_of :token and add a unique index to the table with a migration. – Dyal
Also, it sounds like rand has been deprecated in the latest rails versions? rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/… – Silassilastic
rand is part of Ruby. Rails also defines Array#rand, but as you linked to, has been renamed/deprecated in favor of Array#random_element. That's unrelated to this code. – Dyal
author of the blog post here! Yes: I always add a db constraint or similar to assert the unicity in this case. – Exarate
For those looking for the post (which doesn't exist anymore) ... web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/… – Chiton
N
17

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)
Narva answered 16/5, 2011 at 18:18 Comment(6)
I'm more interested in ensuring that the value generated won't collide with the values already generated and stored, rather than methods for creating unique strings. – Silassilastic
generated value won't collide with values already generated - base64 is deterministic, so if you have unique ids, you will have unique tokens. – Narva
I went with random_string = Digest::MD5.hexdigest("#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}-#{id}")[1..6] where ID is the ID of the token. – Silassilastic
It seems to me that Base64::encode64(id.to_s) defeats the purpose of using a token. Most likely you're using a token to obscure the id and make the resource inaccessible to anyone who does not have the token. However, in this case, someone could just to run Base64::encode64(<insert_id_here>) and they would instantly have all the tokens for every resource on your site. – Bree
Needs to be changed to this to work string = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}") – Krasnoff
If anyone is looking for something similar but that can be salted, see hashids.org – Natasha
D
13

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.

Despond answered 12/10, 2011 at 9:29 Comment(3)
If you do not want weird characters like "+/=", you can just use SecureRandom.hex(10) instead of base64. – Brassie
SecureRandom.urlsafe_base64 achieves the same thing as well. – Rufford
Is this always unique? I need to save it in a db column unique. is there anything else needs to be done for that? – Membership
C
7

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

Collector answered 16/4, 2013 at 11:21 Comment(0)
A
6

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"
Awed answered 2/12, 2014 at 22:32 Comment(2)
I get undefined local variable 'has_secure_token'. Any ideas why? – Bernadinebernadotte
@AdrianMatteo I had this same issue. From what I have understood the has_secure_token comes with Rails 5, but I was using 4.x. I have followed the steps on this article and now it works for me. – Simplify
B
5

To create a proper, mysql, varchar 32 GUID

SecureRandom.uuid.gsub('-','').upcase
Bowlder answered 4/9, 2013 at 8:58 Comment(1)
Since we are trying to replacing a single character '-', you can use tr rather than gsub. SecureRandom.uuid.tr('-','').upcase. Check this link for comparison between tr and gsub. – Gesellschaft
S
3

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
Sailmaker answered 8/5, 2023 at 13:8 Comment(0)
Z
1
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
Zagreb answered 26/2, 2014 at 15:1 Comment(0)
N
-1

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
Newsdealer answered 30/1, 2019 at 15:27 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.