26

I'm writing a rails app that, by its nature, CANNOT require users to register, and thus cannot use authentication as the usual means to protect records. (I know, I know...) User information here is limited to email addresses. So I do need a way to make my model IDs unpredictable so that other IDs cannot be easily guessed. (I know, I know...)

I have tried using plugins like uuidtools to randomize ids as records are created, like so:

require 'uuidtools'
class Post < ActiveRecord::Base 
 def before_create() 
  self.id = OpenSSL::Digest.SHA1.hexdigest(UUID.timestamp_create()) 
 end 
end 

...This looks good at first, but funny things happen. ActiveRecord sometimes tries to insert a 0 value into the id and I get errors such as 'can't find Post with id=0' etc...

I've run out of ideas. Can anyone help? Thanks.

4 Answers4

22

In your model, do this:

before_create :randomize_id

...

private
def randomize_id
  begin
    self.id = SecureRandom.random_number(1_000_000)
  end while Model.where(id: self.id).exists?
end
quaertym
  • 3,917
  • 2
  • 29
  • 41
s.krueger
  • 1,043
  • 8
  • 13
  • With this method, is it possible for multiple posts to have the same random_number?? – Justin Feb 10 '14 at 14:06
  • @Justin No it is not, because the while loop checks if an object with that id already exists (you have to replace *Model* with Post in your case) – s.krueger Mar 20 '14 at 15:38
  • 3
    as long as you have less than a million users :P – Saravanabalagi Ramachandran Sep 30 '16 at 14:19
  • 1
    Instead of model, you can say `self.class` and put it into a base ActiveRecord model that your models extend from. Also increase the random number to maximum allowed by the database. – hakunin Nov 16 '16 at 09:29
  • 8
    This isn't thread safe-- I've seen a solution like this create duplicate entries in a production system. –  Dec 01 '16 at 03:03
17

Best way is to use SecureRandom.uuid that generates a V4 UUID (Universally Unique IDentifier).

It is virtually completely random and unique (collision probability is something like one over tens of trillions) : https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29

This should do the job :

class Post < ActiveRecord::Base
 before_create :generate_random_id

 private 
 def generate_random_id
   self.id = SecureRandom.uuid
 end 
end

Or if you are using Rails >= 4 and PostgreSQL, you can have it generating them for you :

create_table :posts, id: :uuid do |t|
  ...
end
ccyrille
  • 873
  • 7
  • 18
3

The thing that is going wrong here is that self.id requires an int and OpenSSL::Digest.SHA1.hexdigest(UUID.timestamp_create()) returns a string with non-numeric characters which would lead to the value '0' being actually stored in the database

  • Not necessarily, you can define your database to have a string for the ID column. It may not be ideal, but it is possible. – Bill Leeper Nov 30 '15 at 22:02
2

An alternative is to generate a token or checksum or whatever in a second column during record creation, and in all cases your controllers query for an object, use Model.find_by_id_and_token.

You'll then always generate URLs that contain and require both the id and token.

kch
  • 77,385
  • 46
  • 136
  • 148
  • The other reason to do it this way (adding a token/checksum) is that ActiveRecord is simply easier when the primary keys are integers -- as far as associations are concerned. – Philip Hallstrom May 07 '09 at 18:43