diff options
Diffstat (limited to 'activemodel')
-rw-r--r-- | activemodel/CHANGELOG | 5 | ||||
-rw-r--r-- | activemodel/lib/active_model.rb | 1 | ||||
-rw-r--r-- | activemodel/lib/active_model/secure_password.rb | 73 | ||||
-rw-r--r-- | activemodel/test/cases/secure_password_test.rb | 42 | ||||
-rw-r--r-- | activemodel/test/models/user.rb | 8 |
5 files changed, 128 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 |