Skip to content

Commit

Permalink
adding password_reset hammering protection
Browse files Browse the repository at this point in the history
  • Loading branch information
NoamB committed Feb 12, 2011
1 parent a01719c commit 61f28e7
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.rdoc
Expand Up @@ -25,6 +25,7 @@ Password Reset (see lib/sorcery/model/submodules/password_reset.rb):
* Reset password with email verification.
* configurable mailer, method name, and attribute name.
* configurable expiration.
* configurable time between emails (hammering protection).

Remember Me (see lib/sorcery/model/submodules/remember_me.rb):
* Remember me with configurable expiration.
Expand Down Expand Up @@ -55,9 +56,9 @@ Other:
== Next Planned Features:

I've got many plans which include:
* Hammering reset password protection
* Configurable Auto login on registration/activation
* Other reset password strategies (security questions?)
* Other brute force protection strategies (captcha, lock account)
* Sinatra support
* Mongoid support
* OmniAuth integration
Expand Down
12 changes: 10 additions & 2 deletions lib/sorcery/model/submodules/password_reset.rb
Expand Up @@ -7,18 +7,22 @@ def self.included(base)
base.sorcery_config.class_eval do
attr_accessor :reset_password_code_attribute_name, # reset password code attribute name.
:reset_password_code_expires_at_attribute_name, # expires at attribute name.
:reset_password_email_sent_at_attribute_name, # when was email sent, used for hammering protection.
:reset_password_mailer, # mailer class. Needed.
:reset_password_email_method_name, # reset password email method on your mailer class.
:reset_password_expiration_period # how many seconds before the reset request expires. nil for never expires.
:reset_password_expiration_period, # how many seconds before the reset request expires. nil for never expires.
:reset_password_time_between_emails # hammering protection, how long to wait before allowing another email to be sent.

end

base.sorcery_config.instance_eval do
@defaults.merge!(:@reset_password_code_attribute_name => :reset_password_code,
:@reset_password_code_expires_at_attribute_name => :reset_password_code_expires_at,
:@reset_password_email_sent_at_attribute_name => :reset_password_email_sent_at,
:@reset_password_mailer => nil,
:@reset_password_email_method_name => :reset_password_email,
:@reset_password_expiration_period => nil )
:@reset_password_expiration_period => nil,
:@reset_password_time_between_emails => 5.minutes )

reset!
end
Expand All @@ -43,8 +47,12 @@ def validate_mailer_defined
module InstanceMethods
def reset_password!
config = sorcery_config
# hammering protection
return if self.send(config.reset_password_email_sent_at_attribute_name) && self.send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.ago

self.send(:"#{config.reset_password_code_attribute_name}=", generate_random_code)
self.send(:"#{config.reset_password_code_expires_at_attribute_name}=", Time.now.utc+config.reset_password_expiration_period) if config.reset_password_expiration_period
self.send(:"#{config.reset_password_email_sent_at_attribute_name}=", Time.now.utc)
self.class.transaction do
self.save!(:validate => false)
generic_send_email(:reset_password_email_method_name, :reset_password_mailer)
Expand Down
Expand Up @@ -2,9 +2,11 @@ class AddPasswordResetToUsers < ActiveRecord::Migration
def self.up
add_column :users, :reset_password_code, :string, :default => nil
add_column :users, :reset_password_code_expires_at, :datetime, :default => nil
add_column :users, :reset_password_email_sent_at, :datetime, :default => nil
end

def self.down
remove_column :users, :reset_password_email_sent_at
remove_column :users, :reset_password_code_expires_at
remove_column :users, :reset_password_code
end
Expand Down
32 changes: 32 additions & 0 deletions spec/rails3/user_password_reset_spec.rb
Expand Up @@ -49,6 +49,16 @@
plugin_set_model_config_property(:reset_password_expiration_period, 16)
User.sorcery_config.reset_password_expiration_period.should equal(16)
end

it "should allow configuration option 'reset_password_email_sent_at_attribute_name'" do
plugin_set_model_config_property(:reset_password_email_sent_at_attribute_name, :blabla)
User.sorcery_config.reset_password_email_sent_at_attribute_name.should equal(:blabla)
end

it "should allow configuration option 'reset_password_time_between_emails'" do
plugin_set_model_config_property(:reset_password_time_between_emails, 16)
User.sorcery_config.reset_password_time_between_emails.should equal(16)
end
end

# ----------------- PLUGIN ACTIVATED -----------------------
Expand All @@ -71,6 +81,7 @@

it "the reset_password_code should be random" do
create_new_user
plugin_set_model_config_property(:reset_password_time_between_emails, 0)
@user.reset_password!
old_password_code = @user.reset_password_code
@user.reset_password!
Expand Down Expand Up @@ -121,6 +132,27 @@
@user.reset_password_code_valid?("asdadagfdgdf").should == false
end

it "should not send an email if time between emails has not passed since last email" do
create_new_user
plugin_set_model_config_property(:reset_password_time_between_emails, 10000)
old_size = ActionMailer::Base.deliveries.size
@user.reset_password!
ActionMailer::Base.deliveries.size.should == old_size + 1
@user.reset_password!
ActionMailer::Base.deliveries.size.should == old_size + 1
end

it "should send an email if time between emails has passed since last email" do
create_new_user
plugin_set_model_config_property(:reset_password_time_between_emails, 0.5)
old_size = ActionMailer::Base.deliveries.size
@user.reset_password!
ActionMailer::Base.deliveries.size.should == old_size + 1
sleep 0.5
@user.reset_password!
ActionMailer::Base.deliveries.size.should == old_size + 2
end

it "if mailer is nil on activation, throw exception!" do
expect{plugin_model_configure([:password_reset])}.to raise_error(ArgumentError)
end
Expand Down

0 comments on commit 61f28e7

Please sign in to comment.