Skip to content

Commit

Permalink
changing brute_force_protection submodule into something useful, like…
Browse files Browse the repository at this point in the history
… Authlogic's one
  • Loading branch information
NoamB committed Feb 19, 2011
1 parent 6508aa8 commit 06e29d7
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 146 deletions.
3 changes: 1 addition & 2 deletions Gemfile
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
@@ -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
Expand Down
12 changes: 0 additions & 12 deletions Rakefile
Expand Up @@ -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

Expand Down
13 changes: 0 additions & 13 deletions features/support/env.rb

This file was deleted.

1 change: 1 addition & 0 deletions lib/sorcery.rb
Expand Up @@ -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'
Expand Down
6 changes: 3 additions & 3 deletions lib/sorcery/controller.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
77 changes: 8 additions & 69 deletions lib/sorcery/controller/submodules/brute_force_protection.rb
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions lib/sorcery/crypto_providers/bcrypt.rb
@@ -1,8 +1,4 @@
begin
require "bcrypt"
rescue LoadError
"sudo gem install bcrypt-ruby"
end
require 'bcrypt'

module Sorcery
module CryptoProviders
Expand Down
72 changes: 72 additions & 0 deletions lib/sorcery/model/submodules/brute_force_protection.rb
@@ -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
65 changes: 24 additions & 41 deletions spec/rails3/controller_brute_force_protection_spec.rb
@@ -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
Expand All @@ -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

0 comments on commit 06e29d7

Please sign in to comment.