163

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.

Sachin Gevariya
  • 1,167
  • 11
  • 25
Slick23
  • 5,827
  • 10
  • 41
  • 72

12 Answers12

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
    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}")
    token = SecureRandom.urlsafe_base64(16, false)
    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
Krule
  • 6,468
  • 3
  • 34
  • 56
  • @Krule shouldn't it be `break unless` instead of `break if`? also the `token` var is quite shadowed I think – kain Mar 11 '13 at 13:05
  • 7
    this exact code won't work since random_token is scoped within the loop. – Jonathan Mui Mar 13 '13 at 17:13
  • 1
    @Krule 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? – paracycle Aug 20 '13 at 08:29
  • @Krule, why are we not using [SecureRandom.uuid](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/securerandom/rdoc/SecureRandom.html#uuid-method) here ? – Jashwant Nov 27 '13 at 18:04
  • @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. – Krule Nov 28 '13 at 14:25
  • @Krule, thanks for quick reply. I am new to Rails and thus had curiosity. +1 to great answer – Jashwant Nov 28 '13 at 14:32
  • @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 – Krule Nov 28 '13 at 14:50
  • @Krule One small question... Will it make things slow in a big database while trying to go through all ids? – THpubs Nov 25 '14 at 12:58
  • @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. – Krule Dec 01 '14 at 16:00
  • 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. – peterept Mar 11 '15 at 02:07
  • 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`. – Krule Mar 11 '15 at 10:10
  • 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? – Kocur4d Mar 13 '15 at 12:15
  • ```self.token = loop do ``` simple doesn't work. loop doesn't return anything for me. – João Paulo Motta Mar 27 '15 at 02:28
  • @JoãoPauloMotta `loop` does not return any value or it does not break? – Krule Mar 31 '15 at 11:13
  • @Krule loop returned nil. I had to declare the variable outside the loop and assign its value inside. – João Paulo Motta Apr 02 '15 at 20:16
  • The new approach with has_secure_token, is available on which rails version? – Adrian Matteo Apr 23 '15 at 13:20
  • @AdrianMatteo: It is currently available in master and scheduled for Rails 5. – Krule Apr 24 '15 at 09:11
  • Thanks @Krule, wanted to use it on a rails 4 app. – Adrian Matteo Apr 24 '15 at 13:33
  • @AdrianMatteo well, you can copy the module from GitHub and use it now in Rails 4 app :) It's only another concern. – Krule Apr 29 '15 at 08:43
  • 1
    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) – Aleks Jan 11 '17 at 09:55
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)
Nate Bird
  • 5,243
  • 2
  • 27
  • 37
  • 3
    Yeah, that's not bad. I'm usually looking for much shorter strings, to use as part of an URL. – Slick23 Jan 03 '12 at 16:46
  • 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. – Nate Bird Jan 03 '12 at 21:20
  • 12
    @Slick23 You can always grab a portion of the string also: `Digest::SHA1.hexdigest([Time.now, rand].join)[0..10]` – Bijan Sep 26 '13 at 04:55
  • 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. – thekingoftruth Jan 16 '15 at 19:22
  • 1
    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. – mwfearnley Mar 24 '16 at 17:24
  • @mwfearnley Absolutely. It's far from irreversible, and I would not use it for more important obfuscations. – thekingoftruth Mar 28 '16 at 20:53
  • is their a way to make it to 60 characters? – Jenuel Ganawed Jan 22 '20 at 05:39
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
Marius Pop
  • 1,431
  • 2
  • 19
  • 32
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"
Jordan Arnesen
  • 413
  • 5
  • 10
coreyward
  • 77,547
  • 20
  • 137
  • 166
  • It looks like the first method is similar to what I'm doing, but I thought rand wasn't database agnostic? – Slick23 May 16 '11 at 18:36
  • 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? – Slick23 May 16 '11 at 18:39
  • 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. – coreyward May 16 '11 at 19:29
  • This could actually produce 2 identical tokens. You would need other checks against existing tokens to prevent an eventual issue. – Unixmonkey May 16 '11 at 22:14
  • 4
    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. – coreyward May 16 '11 at 22:25
  • Also, it sounds like `rand` has been deprecated in the latest rails versions? https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4555 – Slick23 May 17 '11 at 13:12
  • `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. – coreyward May 17 '11 at 14:47
  • 7
    author of the blog post here! Yes: I always add a db constraint or similar to assert the unicity in this case. – Thibaut Barrère Apr 04 '12 at 08:41
  • 1
    For those looking for the post (which doesn't exist anymore) ... https://web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/2/creating-small-unique-tokens-in-ruby – King'ori Maina Feb 16 '15 at 14:27
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)
Esse
  • 3,278
  • 2
  • 21
  • 25
  • 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. – Slick23 May 16 '11 at 18:34
  • generated value won't collide with values already generated - base64 is deterministic, so if you have unique ids, you will have unique tokens. – Esse May 16 '11 at 20:28
  • 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. – Slick23 May 16 '11 at 21:42
  • 12
    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()` and they would instantly have all the tokens for every resource on your site. – Sky Sep 27 '12 at 07:52
  • Needs to be changed to this to work `string = (Digest::MD5.hexdigest "#{SecureRandom.hex(10)}-#{DateTime.now.to_s}")` – Qasim Dec 23 '16 at 11:47
  • If anyone is looking for something similar but that can be salted, see http://hashids.org – Felipe Zavan Sep 09 '21 at 14:12
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.

Vik
  • 5,931
  • 3
  • 31
  • 38
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

Community
  • 1
  • 1
Nickolay Kondratenko
  • 1,871
  • 2
  • 21
  • 25
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"
user2627938
  • 207
  • 2
  • 3
  • nicely wrapped! Thanks :D – mswiszcz Jan 05 '15 at 12:04
  • 1
    I get undefined local variable 'has_secure_token'. Any ideas why? – Adrian Matteo Apr 23 '15 at 13:08
  • 3
    @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](https://coderwall.com/p/kb97gg/secure-tokens-from-rails-5-to-rails-4-x-and-3-x) and now it works for me. – Tamara Bernad Jul 10 '15 at 17:36
5

To create a proper, mysql, varchar 32 GUID

SecureRandom.uuid.gsub('-','').upcase
Aaron Henderson
  • 1,840
  • 21
  • 20
  • Since we are trying to replacing a single character '-', you can use tr rather than gsub. `SecureRandom.uuid.tr('-','').upcase`. Check this [link](http://stackoverflow.com/a/26750460/811517) for comparison between tr and gsub. – Sree Raj Aug 04 '16 at 04:51
1
def generate_token
    self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
miosser
  • 135
  • 1
  • 7
1

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
Andrei Erdoss
  • 1,573
  • 1
  • 13
  • 14
-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
cappie013
  • 2,354
  • 1
  • 22
  • 30