diff options
-rw-r--r-- | activerecord/CHANGELOG.md | 5 | ||||
-rw-r--r-- | activerecord/lib/active_record.rb | 1 | ||||
-rw-r--r-- | activerecord/lib/active_record/base.rb | 1 | ||||
-rw-r--r-- | activerecord/lib/active_record/secure_token.rb | 49 | ||||
-rw-r--r-- | activerecord/test/cases/secure_token_test.rb | 39 | ||||
-rw-r--r-- | activerecord/test/models/user.rb | 4 | ||||
-rw-r--r-- | activerecord/test/schema/schema.rb | 5 |
7 files changed, 104 insertions, 0 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d1eee8e73e..5588b24851 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,8 @@ +* Added ActiveRecord::SecureToken in order to encapsulate generation of + unique tokens for attributes in a model using SecureRandom + + *Roberto Miranda* + * Change the behavior of boolean columns to be closer to Ruby's semantics. Before this change we had a small set of "truthy", and all others are "falsy". diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 2eec62846b..d9d47c3d99 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -67,6 +67,7 @@ module ActiveRecord autoload :Transactions autoload :Translation autoload :Validations + autoload :SecureToken eager_autoload do autoload :ActiveRecordError, 'active_record/errors' diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index bb01231bca..100d3780f6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -312,6 +312,7 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store + include SecureToken end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb new file mode 100644 index 0000000000..23d4292cbb --- /dev/null +++ b/activerecord/lib/active_record/secure_token.rb @@ -0,0 +1,49 @@ +module ActiveRecord + module SecureToken + extend ActiveSupport::Concern + + module ClassMethods + # Example using has_secure_token + # + # # Schema: User(toke:string, auth_token:string) + # class User < ActiveRecord::Base + # has_secure_token + # has_secure_token :auth_token + # end + # + # user = User.new + # user.save + # user.token # => "44539a6a59835a4ee9d7b112" + # user.auth_token # => "e2426a93718d1817a43abbaa" + # user.regenerate_token # => true + # user.regenerate_auth_token # => true + # + # SecureRandom is used to generate the 24-character unique token, so collisions are highly unlikely. + # We'll check to see if the generated token has been used already using #exists?, and retry up to 10 + # times to find another unused token. After that a RuntimeError is raised if the problem persists. + # + # Note that it's still possible to generate a race condition in the database in the same way that + # validates_presence_of can. You're encouraged to add a unique index in the database to deal with + # this even more unlikely scenario. + def has_secure_token(attribute = :token) + # Load securerandom only when has_secure_key is used. + require 'securerandom' + define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(attribute) } + before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token(attribute)) } + end + + def generate_unique_secure_token(attribute) + 10.times do |i| + SecureRandom.hex(12).tap do |token| + if exists?(attribute => token) + raise "Couldn't generate a unique token in 10 attempts!" if i == 9 + else + return token + end + end + end + end + end + end +end + diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb new file mode 100644 index 0000000000..400fce8c6e --- /dev/null +++ b/activerecord/test/cases/secure_token_test.rb @@ -0,0 +1,39 @@ +require 'cases/helper' +require 'models/user' + +class SecureTokenTest < ActiveRecord::TestCase + setup do + @user = User.new + end + + test "assing unique token values" do + @user.save + assert_not_nil @user.token + assert_not_nil @user.auth_token + end + + test "regenerate the secure key for the attribute" do + @user.save + old_token = @user.token + old_auth_token = @user.auth_token + @user.regenerate_token + @user.regenerate_auth_token + + assert_not_equal @user.token, old_token + assert_not_equal @user.auth_token, old_auth_token + end + + test "raise and exception when with 10 attemps is reached" do + User.stubs(:exists?).returns(*Array.new(10, true)) + assert_raises(RuntimeError) do + @user.save + end + end + + test "assing unique token after 9 attemps reached" do + User.stubs(:exists?).returns(*Array.new(10){ |i| i == 9 ? false : true}) + @user.save + assert_not_nil @user.token + assert_not_nil @user.auth_token + end +end diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb new file mode 100644 index 0000000000..23cd2e0e1c --- /dev/null +++ b/activerecord/test/models/user.rb @@ -0,0 +1,4 @@ +class User < ActiveRecord::Base + has_secure_token + has_secure_token :auth_token +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5907d6ef97..e283f7a9cc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -892,6 +892,11 @@ ActiveRecord::Schema.define do t.string :overloaded_string_with_limit, limit: 255 t.string :string_with_default, default: 'the original default' end + + create_table :users, force: true do |t| + t.string :token + t.string :auth_token + end end Course.connection.create_table :courses, force: true do |t| |