aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel
diff options
context:
space:
mode:
authorUnathi Chonco <choncou@Unathis-MacBook-Pro.local>2016-10-12 01:01:01 +0800
committerUnathi Chonco <choncou@Unathis-MacBook-Pro.local>2016-10-12 01:02:27 +0800
commit86a48b4da3cbd925d30b0fbe472edbda7171ea9e (patch)
tree43bb5b6b618a1f7415353a6b6a497785ea481baf /activemodel
parent32617a2ca3aec1b2649eb86d2b698a0ee04dcb8a (diff)
downloadrails-86a48b4da3cbd925d30b0fbe472edbda7171ea9e.tar.gz
rails-86a48b4da3cbd925d30b0fbe472edbda7171ea9e.tar.bz2
rails-86a48b4da3cbd925d30b0fbe472edbda7171ea9e.zip
This addition will now allow configuring an attribute name for the
existing `#has_secure_password`. This can be useful when one would like to store some secure field as a digest, just like a password. The method still defaults to `password`. It now also allows using the same `#authenticate` method which now accepts a second argument for specifying the attribute to be authenticated, or will default to 'password`. A new method is also added for generating a new token for an attribute by calling `#regenerate_XXXX` where `XXXX` is the attribute name.
Diffstat (limited to 'activemodel')
-rw-r--r--activemodel/CHANGELOG.md21
-rw-r--r--activemodel/lib/active_model/secure_password.rb97
-rw-r--r--activemodel/test/cases/secure_password_test.rb13
-rw-r--r--activemodel/test/models/user.rb3
4 files changed, 93 insertions, 41 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 10f1de6706..0721655a9c 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,24 @@
+* Allows configurable attribute name for `#has_secure_password`. This
+ still defaults to an attribute named 'password', causing no breaking
+ change. Also includes a convenience method `#regenerate_XXX` where
+ +XXX+ is the name of the custom attribute name, eg:
+
+ class User < ActiveRecord::Base
+ has_secure_password :activation_token, validations: false
+ end
+
+ user = User.new()
+ user.regenerate_activation_token
+ user.activation_token # => "ME7abXFGvzZWJRVrD6Et0YqAS6Pg2eDo"
+ user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3Be..."
+
+ The existing `#authenticate` method now allows specifying the attribute
+ to be authenticated, but defaults to 'password', eg:
+
+ user.authenticate('ME7abXFGvzZWJRVrD6Et0YqAS6Pg2eDo', :activation_token) # => user
+
+ *Unathi Chonco*
+
* Removed deprecated `:tokenizer` in the length validator.
*Rafael Mendonça França*
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 1c0fe92bc0..5c7322698a 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -14,15 +14,16 @@ module ActiveModel
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
- # This mechanism requires you to have a +password_digest+ attribute.
+ # This mechanism requires you to have a +XXX_digest+ attribute.
+ # Where +XXX+ is the attribute name of your desired password/token or defaults to +password+
#
# The following validations are added automatically:
# * Password must be present on creation
# * Password length should be less than or equal to 72 characters
- # * Confirmation of password (using a +password_confirmation+ attribute)
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
#
- # If password confirmation validation is not needed, simply leave out the
- # value for +password_confirmation+ (i.e. don't provide a form field for
+ # If confirmation validation is not needed, simply leave out the
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
# it). When this attribute has a +nil+ value, the validation will not be
# triggered.
#
@@ -35,9 +36,10 @@ module ActiveModel
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
- # # Schema: User(name:string, password_digest:string)
+ # # Schema: User(name:string, password_digest:string, activation_token_digest:string)
# class User < ActiveRecord::Base
# has_secure_password
+ # has_secure_password :activation_token, validations: false
# end
#
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
@@ -46,11 +48,16 @@ module ActiveModel
# user.save # => false, confirmation doesn't match
# user.password_confirmation = 'mUc3m00RsqyRe'
# user.save # => true
+ # user.regenerate_activation_token
+ # user.activation_token # => "ME7abXFGvzZWJRVrD6Et0YqAS6Pg2eDo"
+ # user.activation_token_digest # => "$2a$10$0Budk0Fi/k2CDm2PEwa3BeXO5tPOA85b6xazE9rp8nF2MIJlsUik."
+ # user.save # => true
# user.authenticate('notright') # => false
# user.authenticate('mUc3m00RsqyRe') # => user
# User.find_by(name: 'david').try(:authenticate, 'notright') # => false
# User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
- def has_secure_password(options = {})
+ # user.authenticate('ME7abXFGvzZWJRVrD6Et0YqAS6Pg2eDo', :activation_token) # => user
+ def has_secure_password(attribute = :password, validations: true)
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
@@ -61,9 +68,41 @@ module ActiveModel
raise
end
+ attr_reader attribute
+
+ # Encrypts the password into the +password_digest+ attribute, only if the
+ # new password is not empty.
+ #
+ # class User < ActiveRecord::Base
+ # has_secure_password validations: false
+ # end
+ #
+ # user = User.new
+ # user.password = nil
+ # user.password_digest # => nil
+ # user.password = 'mUc3m00RsqyRe'
+ # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
+ define_method("#{attribute}=") do |unencrypted_password|
+ if unencrypted_password.nil?
+ self.send("#{attribute}_digest=", nil)
+ elsif !unencrypted_password.empty?
+ instance_variable_set("@#{attribute}", unencrypted_password)
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
+ end
+ end
+
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
+ end
+
+ define_method("regenerate_#{attribute}") do
+ self.send("#{attribute}=", self.class.generate_unique_secure_token)
+ end
+
include InstanceMethodsOnActivation
- if options.fetch(:validations, true)
+ if validations
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
@@ -71,13 +110,18 @@ module ActiveModel
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
- record.errors.add(:password, :blank) unless record.password_digest.present?
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
end
- validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
- validates_confirmation_of :password, allow_blank: true
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of attribute, allow_blank: true
end
end
+
+ # SecureRandom::base64 is used to generate a 24-character unique token, so collisions are highly unlikely.
+ def generate_unique_secure_token
+ SecureRandom.base64(24)
+ end
end
module InstanceMethodsOnActivation
@@ -91,36 +135,9 @@ module ActiveModel
# user.save
# user.authenticate('notright') # => false
# user.authenticate('mUc3m00RsqyRe') # => user
- def authenticate(unencrypted_password)
- BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
- end
-
- attr_reader :password
-
- # Encrypts the password into the +password_digest+ attribute, only if the
- # new password is not empty.
- #
- # class User < ActiveRecord::Base
- # has_secure_password validations: false
- # end
- #
- # user = User.new
- # user.password = nil
- # user.password_digest # => nil
- # user.password = 'mUc3m00RsqyRe'
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
- def password=(unencrypted_password)
- if unencrypted_password.nil?
- self.password_digest = nil
- elsif !unencrypted_password.empty?
- @password = unencrypted_password
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
- self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
- end
- end
-
- def password_confirmation=(unencrypted_password)
- @password_confirmation = unencrypted_password
+ def authenticate(unencrypted_password, attribute = :password)
+ attribute_digest = send("#{attribute}_digest")
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
end
end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 9423df2c09..abbd182f66 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -184,9 +184,13 @@ class SecurePasswordTest < ActiveModel::TestCase
test "authenticate" do
@user.password = "secret"
+ @user.activation_token = "new_token"
assert !@user.authenticate("wrong")
assert @user.authenticate("secret")
+
+ assert !@user.authenticate("wrong", :activation_token)
+ assert @user.authenticate("new_token", :activation_token)
end
test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
@@ -215,4 +219,13 @@ class SecurePasswordTest < ActiveModel::TestCase
@user.password = "secret"
assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost
end
+
+ test "regenerate attribute method" do
+ old_digest = @user.activation_token_digest
+ @user.regenerate_activation_token
+
+ assert_not_nil @user.activation_token
+ assert_not_nil @user.activation_token_digest
+ assert_not_equal old_digest, @user.activation_token_digest
+ end
end
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index 6556b1a7d9..ab424efb9b 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -5,6 +5,7 @@ class User
define_model_callbacks :create
has_secure_password
+ has_secure_password :activation_token, validations: false
- attr_accessor :password_digest
+ attr_accessor :password_digest, :activation_token_digest
end