How to create small, unique tokens in Ruby

A while back I was looking for a way to generate short unique tokens to be used as authorization tokens in urls. I did some research and asked on ruby-talk. Here’s a summary of what I found.

Note that most of these algorithms can be translated to C#, Java, IronRuby/JRuby or your language and platform of choice.

My choice: Dave Bass’s rand().to_s() trick

Dave Bass proposed this which I picked up for my implementation (here for an 8-chars token):

>> rand(36**8).to_s(36)
=> "uur0cj2h"

The result can be used as an url; pretty neat. It relies on the ability of Fixnum to translate itself to a string in a given base (here we use base 36, which I rarely use!).

This can be used in an ActiveRecord model for instance:

class Customer < ActiveRecord::Base
  validates_presence_of :access_token
  validates_uniqueness_of :access_token

protected
  def before_validation_on_create
    self.access_token = rand(36**8).to_s(36) if self.new_record? and self.access_token.nil?
  end 
end

Jamie Macey feedback

Jamie proposed several options. First, use a substring of SHA1, which is “small enough to be usable, but still pseudo-random enough for temporary tokens to not be guessable” :

>> require 'digest'
=> []
>> Digest::SHA1.hexdigest("some-random-string")[8..16]
=> "2ebe5597f"

Another technique is to rely on ActiveSupport SecureRandom, and tweak the results a bit to get a url-friendly token. Here’s my final bit of code with this method:

>> require 'active_support'
=> []
>> ActiveSupport::SecureRandom.base64(8).gsub("/","_").gsub(/=+$/,"")
=> "AEWQyovNFo0" 

Jamie’s last proposal is “not terribly robust, but functional” :

>> chars = ['A'..'Z', 'a'..'z', '0'..'9'].map{|r|r.to_a}.flatten
>> Array.new(6).map{chars[rand(chars.size)]}.join
=> "g64wdR"

Ryan Davis: let’s put more words in it

Ryan proposed something totally different:

>> words = File.read("/usr/share/dict/words").split; max = words.size
=> 234936
>> "#{words[rand(max)]}-#{words[rand(max)]}" 
=> "loquat-motorial"

The idea is interesting. You’ll need to ensure your dictionary doesn’t contain insults, if your user base cares about that :)

Another option Ryan got from Eric is to use the quite unknown bubble-babble to make hash values more readable:

>> require 'digest/bubblebabble'
=> true
Digest.bubblebabble(Digest::SHA1::hexdigest("random string")[8..12]) 
=> "xesik-fymak-gunax"

John Mettraux’s Rufus Mnemo

rufus-mnemo has the ability to translate an integer into easy-to-remember words, based on Japanese syllabes:

>> require 'rufus/mnemo'
>> s = Rufus::Mnemo::from_integer rand(8**5)
=> "bisoshi" 

Pretty neat! The generated words are “easy to the latin ears”. Take care of the meaning if your users are Japanese-speaking.

If you use UUID – be careful with Solaris zones!

If you deploy to Solaris zones, be careful about that: some other libraries I had a look at, like the very nice assaf’s uuid, are relying on macaddr, which doesn’t seem to work on Solaris Zone.

See here, here and here for solutions if you’re in trouble.

Have you got more ?

Please share using the comments system.

cheers!

The comments system is brand new - don't be afraid to comment!