Skip to content

Commit

Permalink
Merge pull request #249 from foohey/master
Browse files Browse the repository at this point in the history
Added unlock token sent by email for bruteforce protection
  • Loading branch information
NoamB committed Apr 4, 2012
2 parents 3eb80bb + 97e10ce commit 244f4ec
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 9 deletions.
20 changes: 20 additions & 0 deletions lib/generators/sorcery/templates/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,26 @@
#
# user.login_lock_time_period =

# Unlock token attribute name
# Default: `:unlock_token`
#
# user.unlock_token_attribute_name =

# Unlock token mailer method
# Default: `:send_unlock_token_email`
#
# user.unlock_token_email_method_name =

# when true sorcery will not automatically
# send email with unlock token
# Default: `false`
#
# user.unlock_token_mailer_disabled = true

# Unlock token mailer class
# Default: `nil`
#
# user.unlock_token_mailer = UserMailer

# -- activity logging --
# Last login attribute name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ class SorceryBruteForceProtection < ActiveRecord::Migration
def self.up
add_column :<%= model_class_name.tableize %>, :failed_logins_count, :integer, :default => 0
add_column :<%= model_class_name.tableize %>, :lock_expires_at, :datetime, :default => nil
add_column :<%= model_class_name.tableize %>, :unlock_token, :string, :default => nil
end

def self.down
remove_column :<%= model_class_name.tableize %>, :lock_expires_at
remove_column :<%= model_class_name.tableize %>, :failed_logins_count
remove_column :<%= model_class_name.tableize %>, :unlock_token
end
end
47 changes: 38 additions & 9 deletions lib/sorcery/model/submodules/brute_force_protection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ def self.included(base)
:lock_expires_at_attribute_name, # this field indicates whether user
# is banned and when it will be active again.
:consecutive_login_retries_amount_limit, # how many failed logins allowed.
:login_lock_time_period # how long the user should be banned.
:login_lock_time_period, # how long the user should be banned.
# in seconds. 0 for permanent.

:unlock_token_attribute_name, # Unlock token attribute name
:unlock_token_email_method_name, # Mailer method name
:unlock_token_mailer_disabled, # When true, dont send unlock token via email
:unlock_token_mailer # Mailer class
end

base.sorcery_config.instance_eval do
@defaults.merge!(:@failed_logins_count_attribute_name => :failed_logins_count,
:@lock_expires_at_attribute_name => :lock_expires_at,
:@consecutive_login_retries_amount_limit => 50,
:@login_lock_time_period => 60 * 60)
:@login_lock_time_period => 60 * 60,

:@unlock_token_attribute_name => :unlock_token,
:@unlock_token_email_method_name => :send_unlock_token_email,
:@unlock_token_mailer_disabled => false,
:@unlock_token_mailer => nil)
reset!
end

Expand All @@ -31,9 +41,19 @@ def self.included(base)
end
base.extend(ClassMethods)
base.send(:include, InstanceMethods)

base.class_eval do
after_update :send_unlock_token_email!, :if => Proc.new { |user| user.send(sorcery_config.unlock_token_attribute_name).present? }
end
end

module ClassMethods
def load_from_unlock_token(token)
return nil if token.blank?
user = find_by_sorcery_token(sorcery_config.unlock_token_attribute_name,token)
user
end

protected

def define_brute_force_protection_mongoid_fields
Expand All @@ -58,25 +78,34 @@ def register_failed_login!
self.lock! if self.send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_limit
end

protected

def lock!
# /!\
# Moved out of protected for use like activate! in controller
# /!\
def unlock!
config = sorcery_config
self.send(:"#{config.lock_expires_at_attribute_name}=", Time.now.in_time_zone + config.login_lock_time_period)
self.send(:"#{config.lock_expires_at_attribute_name}=", nil)
self.send(:"#{config.failed_logins_count_attribute_name}=", 0)
self.send(:"#{config.unlock_token_attribute_name}=", nil) unless config.unlock_token_mailer_disabled or config.unlock_token_mailer.nil?
self.save!(:validate => false)
end

def unlock!
protected

def lock!
config = sorcery_config
self.send(:"#{config.lock_expires_at_attribute_name}=", nil)
self.send(:"#{config.failed_logins_count_attribute_name}=", 0)
self.send(:"#{config.lock_expires_at_attribute_name}=", Time.now.in_time_zone + config.login_lock_time_period)
self.send(:"#{config.unlock_token_attribute_name}=", TemporaryToken.generate_random_token) unless config.unlock_token_mailer_disabled or config.unlock_token_mailer.nil?
self.save!(:validate => false)
end

def unlocked?
config = sorcery_config
self.send(config.lock_expires_at_attribute_name).nil?
end

def send_unlock_token_email!
generic_send_email(:unlock_token_email_method_name, :unlock_token_mailer) unless sorcery_config.unlock_token_email_method_name.nil? or sorcery_config.unlock_token_mailer_disabled == true
end

# Prevents a locked user from logging in, and unlocks users that expired their lock time.
# Runs as a hook before authenticate.
Expand Down
7 changes: 7 additions & 0 deletions spec/rails3/app/mailers/sorcery_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@ def reset_password_email(user)
mail(:to => user.email,
:subject => "Your password has been reset")
end

def send_unlock_token_email(user)
@user = user
@url = "http://example.com/unlock/#{user.unlock_token}"
mail(:to => user.email,
:subject => "Your account has been locked due to many wrong logins")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Please visit <% @url %> for unlock your account.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class AddBruteForceProtectionToUsers < ActiveRecord::Migration
def self.up
add_column :users, :failed_logins_count, :integer, :default => 0
add_column :users, :lock_expires_at, :datetime, :default => nil
add_column :users, :unlock_token, :string, :default => nil
end

def self.down
Expand Down
21 changes: 21 additions & 0 deletions spec/rails3/spec/controller_brute_force_protection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@
sorcery_controller_property_set(:user_class, User)
Timecop.return
end

it "should generate unlock token after user locked" do
sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2)
sorcery_model_property_set(:login_lock_time_period, 0)
sorcery_model_property_set(:unlock_token_mailer, SorceryMailer)
3.times {get :test_login, :username => "gizmo", :password => "blabla"}
User.find_by_username('gizmo').unlock_token.should_not be_nil
end

it "should unlock after entering unlock token" do
sorcery_model_property_set(:consecutive_login_retries_amount_limit, 2)
sorcery_model_property_set(:login_lock_time_period, 0)
sorcery_model_property_set(:unlock_token_mailer, SorceryMailer)
3.times {get :test_login, :username => "gizmo", :password => "blabla"}
User.find_by_username('gizmo').unlock_token.should_not be_nil
token = User.find_by_username('gizmo').unlock_token
user = User.load_from_unlock_token(token)
user.should_not be_nil
user.unlock!
User.load_from_unlock_token(token).should be_nil
end

it "should count login retries" do
3.times {get :test_login, :username => 'gizmo', :password => 'blabla'}
Expand Down

0 comments on commit 244f4ec

Please sign in to comment.