Permalink
Browse files

changing brute_force_protection submodule into something useful, like…

… Authlogic's one
  • Loading branch information...
NoamB committed Feb 19, 2011
1 parent 6508aa8 commit 06e29d73a12cb4325e688a43dc7a3c23a600fb39
View
@@ -6,13 +6,12 @@ source "http://rubygems.org"
# Add dependencies to develop your gem here.
# Include everything needed to run rake, tests, features, etc.
group :development do
- gem "rails", "3.0.3"
+ gem "rails", ">= 3.0.0"
gem "rspec", "~> 2.3.0"
gem 'rspec-rails'
gem 'ruby-debug19'
gem 'sqlite3-ruby', :require => 'sqlite3'
gem "yard", "~> 0.6.0"
- gem "cucumber", ">= 0"
gem "bundler", "~> 1.0.0"
gem "jeweler", "~> 1.5.2"
gem 'simplecov', '>= 0.3.8', :require => false # Will install simplecov-html as a dependency
View
@@ -1,4 +1,4 @@
-Copyright (c) 2010 Noam Ben-Ari
+Copyright (c) 2010 Noam Ben-Ari <mailto:nbenari@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
View
@@ -33,21 +33,9 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList['spec/**/*_spec.rb']
end
-RSpec::Core::RakeTask.new(:rcov) do |spec|
- spec.pattern = 'spec/**/*_spec.rb'
- spec.rcov = true
-end
-
-require 'cucumber/rake/task'
-Cucumber::Rake::Task.new(:features)
-
-
-
require 'yard'
YARD::Rake::YardocTask.new
-
-#task :default => :spec
desc 'Default: Run all specs.'
task :default => :all_specs
View
@@ -1,13 +0,0 @@
-require 'bundler'
-begin
- Bundler.setup(:default, :development)
-rescue Bundler::BundlerError => e
- $stderr.puts e.message
- $stderr.puts "Run `bundle install` to install missing gems"
- exit e.status_code
-end
-
-$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
-require 'sorcery'
-
-require 'rspec/expectations'
View
@@ -6,6 +6,7 @@ module Submodules
autoload :ResetPassword, 'sorcery/model/submodules/reset_password'
autoload :RememberMe, 'sorcery/model/submodules/remember_me'
autoload :ActivityLogging, 'sorcery/model/submodules/activity_logging'
+ autoload :BruteForceProtection, 'sorcery/model/submodules/brute_force_protection'
end
end
autoload :Controller, 'sorcery/controller'
@@ -45,7 +45,7 @@ def login(*credentials)
after_login!(user, credentials)
logged_in_user
else
- after_failed_login!(user, credentials)
+ after_failed_login!(credentials)
nil
end
end
@@ -94,8 +94,8 @@ def after_login!(user, credentials)
Config.after_login.each {|c| self.send(c, user, credentials)}
end
- def after_failed_login!(user, credentials)
- Config.after_failed_login.each {|c| self.send(c, user, credentials)}
+ def after_failed_login!(credentials)
+ Config.after_failed_login.each {|c| self.send(c, credentials)}
end
def before_logout!(user)
@@ -4,83 +4,22 @@ module Submodules
module BruteForceProtection
def self.included(base)
base.send(:include, InstanceMethods)
- Config.module_eval do
- class << self
- attr_accessor :login_retries_amount_allowed, # how many failed logins allowed.
- :login_retries_time_period, # the time after which the failed logins counter is reset.
- :login_ban_time_period, # how long the user should be banned. in seconds.
- :banned_action # what controller action should be called when a banned user tries.
-
- def merge_brute_force_protection_defaults!
- @defaults.merge!(:@login_retries_amount_allowed => 50,
- :@login_retries_time_period => 30,
- :@login_ban_time_period => 3600,
- :@banned_action => :default_banned_action)
- end
- end
- merge_brute_force_protection_defaults!
- end
- Config.after_failed_login << :check_failed_logins_limit_reached
- base.prepend_before_filter :deny_banned_user
+
+ Config.after_login << :reset_failed_logins_count!
+ Config.after_failed_login << :update_failed_logins_count!
end
module InstanceMethods
- def check_failed_logins_limit_reached(user, credentials)
- now = Time.now.utc
-
- # not banned
- if session[:first_failed_login_time]
- reset_failed_logins_if_time_passed(now)
- else
- session[:first_failed_login_time] = now
- end
- increment_failed_logins
- # ban
- ban_if_above_limit(now)
- end
protected
- def release_ban_if_time_passed(now)
- if now - session[:ban_start_time] > Config.login_ban_time_period
- session[:banned] = nil
- session[:failed_logins] = 0
- return true
- end
- false
- end
-
- def increment_failed_logins
- session[:failed_logins] ||= 0
- session[:failed_logins] += 1
- end
-
- def reset_failed_logins_if_time_passed(now)
- if now - session[:first_failed_login_time] > Config.login_retries_time_period
- session[:failed_logins] = 0
- session[:first_failed_login_time] = now
- end
- end
-
- def ban_if_above_limit(now)
- if session[:failed_logins] > Config.login_retries_amount_allowed
- session[:banned] = true
- session[:ban_start_time] = now
- end
- end
-
- def deny_banned_user
- if session[:banned]
- now = Time.now.utc
- release_ban_if_time_passed(now)
- end
-
- # if still banned
- send(Config.banned_action) if session[:banned]
+ def update_failed_logins_count!(credentials)
+ user = User.where("#{User.sorcery_config.username_attribute_name} = ?", credentials[0]).first
+ user.register_failed_login! if user
end
- def default_banned_action
- render :nothing => true
+ def reset_failed_logins_count!(user, credentials)
+ user.update_attributes!(User.sorcery_config.failed_logins_count_attribute_name => 0)
end
end
end
@@ -1,8 +1,4 @@
-begin
- require "bcrypt"
-rescue LoadError
- "sudo gem install bcrypt-ruby"
-end
+require 'bcrypt'
module Sorcery
module CryptoProviders
@@ -0,0 +1,72 @@
+module Sorcery
+ module Model
+ module Submodules
+ module BruteForceProtection
+ def self.included(base)
+ base.sorcery_config.class_eval do
+ attr_accessor :failed_logins_count_attribute_name, # failed logins attribute name.
+ :lock_expires_at_attribute_name, # this field indicates whether user is banned and when it will be active again.
+ :consecutive_login_retries_amount_allowed, # how many failed logins allowed.
+ :login_lock_time_period # how long the user should be banned. in seconds. 0 for permanent.
+ 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_allowed => 50,
+ :@login_lock_time_period => 3600)
+ reset!
+ end
+
+ base.class_eval do
+
+ end
+
+ base.sorcery_config.before_authenticate << :prevent_locked_user_login
+ base.extend(ClassMethods)
+ base.send(:include, InstanceMethods)
+ end
+
+ module ClassMethods
+ protected
+
+ end
+
+ module InstanceMethods
+ def register_failed_login!
+ config = sorcery_config
+ self.increment(config.failed_logins_count_attribute_name)
+ save!
+ self.lock! if self.send(config.failed_logins_count_attribute_name) >= config.consecutive_login_retries_amount_allowed
+ end
+
+ protected
+
+ def lock!
+ config = sorcery_config
+ self.update_attributes!(config.lock_expires_at_attribute_name => Time.now.utc + config.login_lock_time_period)
+ end
+
+ def unlock!
+ config = sorcery_config
+ self.update_attributes!(config.lock_expires_at_attribute_name => nil,
+ config.failed_logins_count_attribute_name => 0)
+ end
+
+ def unlocked?
+ config = sorcery_config
+ self.send(config.lock_expires_at_attribute_name).nil?
+ end
+
+ def prevent_locked_user_login
+ config = sorcery_config
+ if !self.unlocked?
+ self.unlock! if self.send(config.lock_expires_at_attribute_name) <= Time.now.utc
+ end
+ unlocked?
+ end
+ end
+ end
+ end
+ end
+end
@@ -0,0 +1,11 @@
+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
+ end
+
+ def self.down
+ remove_column :users, :lock_expires_at
+ remove_column :users, :failed_logins_count
+ end
+end
@@ -1,6 +1,13 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe ApplicationController do
+ before(:all) do
+ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate/brute_force_protection")
+ end
+
+ after(:all) do
+ ActiveRecord::Migrator.rollback("#{Rails.root}/db/migrate/brute_force_protection")
+ end
# ----------------- SESSION TIMEOUT -----------------------
describe ApplicationController, "with brute force protection features" do
@@ -14,59 +21,35 @@
plugin_set_controller_config_property(:user_class, User)
end
- it "should have configuration for 'login_retries_amount_allowed' per session" do
- plugin_set_controller_config_property(:login_retries_amount_allowed, 32)
- Sorcery::Controller::Config.login_retries_amount_allowed.should equal(32)
- end
-
- it "should have configuration for 'login_retries_counter_reset_time'" do
- plugin_set_controller_config_property(:login_retries_time_period, 32)
- Sorcery::Controller::Config.login_retries_time_period.should equal(32)
- end
-
- it "should count login retries per session" do
+ it "should count login retries" do
3.times {get :test_login, :username => 'gizmo', :password => 'blabla'}
- session[:failed_logins].should == 3
+ User.find_by_username('gizmo').failed_logins_count.should == 3
end
- it "should reset the counter if enough time has passed" do
- plugin_set_controller_config_property(:login_retries_amount_allowed, 5)
- plugin_set_controller_config_property(:login_retries_time_period, 0.2)
- get :test_login, :username => 'gizmo', :password => 'blabla'
- sleep 0.4
- get :test_login, :username => 'gizmo', :password => 'blabla'
- session[:failed_logins].should == 1
+ it "should reset the counter on a good login" do
+ plugin_set_model_config_property(:consecutive_login_retries_amount_allowed, 5)
+ 3.times {get :test_login, :username => 'gizmo', :password => 'blabla'}
+ get :test_login, :username => 'gizmo', :password => 'secret'
+ User.find_by_username('gizmo').failed_logins_count.should == 0
end
- it "should ban session when number of retries reached within an amount of time" do
- plugin_set_controller_config_property(:login_retries_amount_allowed, 1)
- plugin_set_controller_config_property(:login_retries_time_period, 50)
- get :test_login, :username => 'gizmo', :password => 'blabla'
+ it "should lock user when number of retries reached the limit" do
+ User.find_by_username('gizmo').lock_expires_at.should be_nil
+ plugin_set_model_config_property(:consecutive_login_retries_amount_allowed, 1)
get :test_login, :username => 'gizmo', :password => 'blabla'
- session[:banned].should == true
+ User.find_by_username('gizmo').lock_expires_at.should_not be_nil
end
- it "should clear ban after ban time limit passes" do
- plugin_set_controller_config_property(:login_retries_amount_allowed, 1)
- plugin_set_controller_config_property(:login_retries_time_period, 50)
- plugin_set_controller_config_property(:login_ban_time_period, 0.2)
+ it "should unlock after lock time period passes" do
+ plugin_set_model_config_property(:consecutive_login_retries_amount_allowed, 2)
+ plugin_set_model_config_property(:login_lock_time_period, 0.2)
get :test_login, :username => 'gizmo', :password => 'blabla'
get :test_login, :username => 'gizmo', :password => 'blabla'
- session[:banned].should == true
+ User.find_by_username('gizmo').lock_expires_at.should_not be_nil
sleep 0.3
get :test_login, :username => 'gizmo', :password => 'blabla'
- session[:banned].should == nil
- end
-
- it "banned session calls the configured banned action" do
- plugin_set_controller_config_property(:login_retries_amount_allowed, 1)
- plugin_set_controller_config_property(:login_retries_time_period, 50)
- plugin_set_controller_config_property(:login_ban_time_period, 50)
- get :test_login, :username => 'gizmo', :password => 'blabla'
- get :test_login, :username => 'gizmo', :password => 'blabla'
- get :test_login, :username => 'gizmo', :password => 'blabla'
- session[:banned].should == true
- response.body.should == " "
+ User.find_by_username('gizmo').lock_expires_at.should be_nil
end
+
end
end
Oops, something went wrong.

0 comments on commit 06e29d7

Please sign in to comment.