From bcf4e4f2b02157cecc1f1727a95cdf5bfa471771 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 18 Dec 2010 13:38:05 -0800 Subject: Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with SHA2 encryption and salting --- activemodel/lib/active_model.rb | 1 + activemodel/lib/active_model/secure_password.rb | 73 +++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 activemodel/lib/active_model/secure_password.rb (limited to 'activemodel/lib') 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 -- cgit v1.2.3