Skip to content
Browse files

Added ActiveRecord::Base#has_secure_password (via ActiveModel::Secure…

…Password) to encapsulate dead-simple password usage with SHA2 encryption and salting
  • Loading branch information...
1 parent d9b732f commit bcf4e4f2b02157cecc1f1727a95cdf5bfa471771 @dhh dhh committed Dec 18, 2010
View
5 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]
View
1 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
View
73 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
@trevorturk
trevorturk added a note Dec 19, 2010

Any chance of making this minimum size configurable like weak_passwords?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ errors.add(:password, "can't be 'password'") if @password == "password"
@joshsusser
joshsusser added a note Dec 19, 2010

It's really nice to be able to use "password" as the password in development and tests. Can this check be enforced only in production? Also, why prevent "password" but not "123456" (the most common) or other top-10 common passwords?

@tenderlove
Ruby on Rails member
tenderlove added a note Dec 19, 2010

I don't think we should have this at all. If someone wants to prevent certain passwords, it's easy enough to write their own validation. I frequently use password as my development password. This code would cause a hassle, as well as make tests fail.

@parndt
parndt added a note Dec 19, 2010

Would rather not have a hardcoded fail on one particular string. Infact, could this instead be some sort of config based array so that one could provide invalid passwords for their own application? This has the added bonus of allowing developers to specify none, too.

e.g.

invalid_passwords.each do |password|
  errors.add(:password, "can't be '#{password}'") if @password == password
end

That way, there is not just one default "bad", English, word that fails.

@fxn
Ruby on Rails member
fxn added a note Dec 19, 2010

I agree with Aaron, don't see this control at all in core.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+ end
+ end
+ end
+end
View
42 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
View
8 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
View
19 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:
@mitchellh
mitchellh added a note Dec 19, 2010

I think the end of this line is meant to say BCrypt and not SHA2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ # 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:
View
1 activerecord/lib/active_record/base.rb
@@ -1851,6 +1851,7 @@ def clear_timestamp_attributes
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.

5 comments on commit bcf4e4f

@dmathieu

Interesting. Is that a first step on including an authentication system in rails core ? (I hope not).

@dhh
Ruby on Rails member
dhh commented on bcf4e4f Dec 18, 2010

dmathieu, absolutely not. This is wrapping up the low-level task of proper hashing. Nothing is coming to core that'll encapsulate how to send reset emails or whatever else the whole-hog authentication solutions do.

@dhh
Ruby on Rails member
dhh commented on bcf4e4f Dec 18, 2010

Also, we switched to BCrypt for added security: 39b5ea6

@dmathieu

Another thing : is there a reason why you didn't allow to manually specify the password field name ? Something like this :

secure :password
@radar
Ruby on Rails member
radar commented on bcf4e4f Dec 19, 2010

What is the backstory on this commit? Why does this functionality need to be in Rails rather than inside the libraries that implement authentication?

Please sign in to comment.
Something went wrong with that request. Please try again.