aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2010-12-18 13:38:05 -0800
committerDavid Heinemeier Hansson <david@loudthinking.com>2010-12-18 13:38:05 -0800
commitbcf4e4f2b02157cecc1f1727a95cdf5bfa471771 (patch)
treeb38232fb127a25817aa25d3b8976d218948119e3
parentd9b732fcadc001b6757e648761acf97833827b58 (diff)
downloadrails-bcf4e4f2b02157cecc1f1727a95cdf5bfa471771.tar.gz
rails-bcf4e4f2b02157cecc1f1727a95cdf5bfa471771.tar.bz2
rails-bcf4e4f2b02157cecc1f1727a95cdf5bfa471771.zip
Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with SHA2 encryption and salting
-rw-r--r--activemodel/CHANGELOG5
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/secure_password.rb73
-rw-r--r--activemodel/test/cases/secure_password_test.rb42
-rw-r--r--activemodel/test/models/user.rb8
-rw-r--r--activerecord/CHANGELOG19
-rw-r--r--activerecord/lib/active_record/base.rb1
7 files changed, 148 insertions, 1 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG
index 4e963c77b0..a19d029217 100644
--- a/activemodel/CHANGELOG
+++ b/activemodel/CHANGELOG
@@ -1,15 +1,18 @@
*Rails 3.1.0 (unreleased)*
-* No changes
+* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with SHA2 encryption and salting [DHH]
+
*Rails 3.0.2 (unreleased)*
* No changes
+
*Rails 3.0.1 (October 15, 2010)*
* No Changes, just a version bump.
+
*Rails 3.0.0 (August 29, 2010)*
* Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis]
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index be0f24ff92..dd6ee058cc 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -42,6 +42,7 @@ module ActiveModel
autoload :Naming
autoload :Observer, 'active_model/observing'
autoload :Observing
+ autoload :SecurePassword
autoload :Serialization
autoload :TestCase
autoload :Translation
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
new file mode 100644
index 0000000000..0599ce6865
--- /dev/null
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -0,0 +1,73 @@
+require 'digest/sha2'
+
+module ActiveModel
+ module SecurePassword
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Adds methods to set and authenticate against a SHA2-encrypted and salted password.
+ # This mechanism requires you to have password_digest and password_salt attributes.
+ #
+ # Validations for presence of password, confirmation of password (using a "password_confirmation" attribute),
+ # and strength of password (at least 6 chars, not "password") are automatically added.
+ # You can add more validations by hand if need be.
+ #
+ # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
+ #
+ # # Schema: User(name:string, password_digest:string, password_salt:string)
+ # class User < ActiveRecord::Base
+ # has_secure_password
+ # end
+ #
+ # user = User.new(:name => "david", :password => "secret", :password_confirmation => "nomatch")
+ # user.save # => false, password not long enough
+ # user.password = "mUc3m00RsqyRe"
+ # user.save # => false, confirmation doesn't match
+ # user.password_confirmation = "mUc3m00RsqyRe"
+ # user.save # => true
+ # user.authenticate("notright") # => false
+ # user.authenticate("mUc3m00RsqyRe") # => user
+ # User.find_by_name("david").try(:authenticate, "notright") # => nil
+ # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
+ def has_secure_password
+ attr_reader :password
+ attr_accessor :password_confirmation
+
+ attr_protected(:password_digest, :password_salt) if respond_to?(:attr_protected)
+
+ validates_confirmation_of :password
+ validates_presence_of :password_digest
+ validate :password_must_be_strong
+ end
+ end
+
+ module InstanceMethods
+ # Returns self if the password is correct, otherwise false.
+ def authenticate(unencrypted_password)
+ password_digest == encrypt_password(unencrypted_password) ? self : false
+ end
+
+ # Encrypts the password into the password_digest attribute.
+ def password=(unencrypted_password)
+ @password = unencrypted_password
+ self.password_digest = encrypt_password(unencrypted_password)
+ end
+
+ private
+ def salt_for_password
+ self.password_salt ||= self.object_id.to_s + rand.to_s
+ end
+
+ def encrypt_password(unencrypted_password)
+ Digest::SHA2.hexdigest(unencrypted_password + salt_for_password)
+ end
+
+ def password_must_be_strong
+ if @password.present?
+ errors.add(:password, "must be longer than 6 characters") unless @password.size > 6
+ errors.add(:password, "can't be 'password'") if @password == "password"
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
new file mode 100644
index 0000000000..7d7c51e6cb
--- /dev/null
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -0,0 +1,42 @@
+require 'cases/helper'
+require 'models/user'
+
+class SecurePasswordTest < ActiveModel::TestCase
+ setup do
+ @user = User.new
+ end
+
+ test "password must be present" do
+ assert !@user.valid?
+ assert_equal 1, @user.errors.size
+ end
+
+ test "password must match confirmation" do
+ @user.password = "thiswillberight"
+ @user.password_confirmation = "wrong"
+
+ assert !@user.valid?
+
+ @user.password_confirmation = "thiswillberight"
+
+ assert @user.valid?
+ end
+
+ test "password must pass validation rules" do
+ @user.password = "password"
+ assert !@user.valid?
+
+ @user.password = "short"
+ assert !@user.valid?
+
+ @user.password = "plentylongenough"
+ assert @user.valid?
+ end
+
+ test "authenticate" do
+ @user.password = "secret"
+
+ assert !@user.authenticate("wrong")
+ assert @user.authenticate("secret")
+ end
+end \ No newline at end of file
diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
new file mode 100644
index 0000000000..e221bb8091
--- /dev/null
+++ b/activemodel/test/models/user.rb
@@ -0,0 +1,8 @@
+class User
+ include ActiveModel::Validations
+ include ActiveModel::SecurePassword
+
+ has_secure_password
+
+ attr_accessor :password_digest, :password_salt
+end
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 056de18cde..0f7e65e8cd 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,24 @@
*Rails 3.1.0 (unreleased)*
+* Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with SHA2 encryption and salting [DHH]. Example:
+
+ # Schema: User(name:string, password_digest:string, password_salt:string)
+ class User < ActiveRecord::Base
+ has_secure_password
+ end
+
+ user = User.new(:name => "david", :password => "secret", :password_confirmation => "nomatch")
+ user.save # => false, password not long enough
+ user.password = "mUc3m00RsqyRe"
+ user.save # => false, confirmation doesn't match
+ user.password_confirmation = "mUc3m00RsqyRe"
+ user.save # => true
+ user.authenticate("notright") # => false
+ user.authenticate("mUc3m00RsqyRe") # => user
+ User.find_by_name("david").try(:authenticate, "notright") # => nil
+ User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
+
+
* When a model is generated add_index is added by default for belongs_to or references columns
rails g model post user:belongs_to will generate the following:
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index d0f33c1d18..858ccebbfa 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1851,6 +1851,7 @@ MSG
include ActiveModel::MassAssignmentSecurity
include Callbacks, ActiveModel::Observing, Timestamp
include Associations, AssociationPreload, NamedScope
+ include ActiveModel::SecurePassword
# AutosaveAssociation needs to be included before Transactions, because we want
# #save_with_autosave_associations to be wrapped inside a transaction.