Skip to content

Brute force protection

Shinichi Maeshima edited this page Apr 24, 2024 · 25 revisions

In this tutorial we will build upon the app created at Simple Password Authentication so make sure you understand it.

First Add some db fields:

rails g sorcery:install brute_force_protection --only-submodules

Which will create:

class SorceryBruteForceProtection < 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
    remove_column :users, :lock_expires_at
    remove_column :users, :failed_logins_count
    remove_column :users, :unlock_token
  end
end
rake db:migrate

Then add the brute_force_protection submodule:

# config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:brute_force_protection, blabla, blablu, ...]

Refer to your config/initializers/sorcery.rb file to customize specifics that are required - it will not work properly without customization:

# config/initializers/sorcery.rb

user.consecutive_login_retries_amount_limit = 50 
user.login_lock_time_period = (60 * 5) # in seconds

user.unlock_token_mailer_disabled = true # I want to have full control over when and how emails are sent

# You'll also need to specify a mailer, a mailer action and a view so that password unlock instructions are sent.
user.unlock_token_mailer = UserMailer

# default mailer action is: send_unlock_token_email - but is configurable

You will also need to configure your sessions_controller.rb or equivalent file to register that an incorrect login has occurred which will increase the failed_logins_count on the relevant user model:

# sessions_controller
def create
  login(params[:email], params[:password], params[:remember]) do |user, failure|      
    # if the login fails, we need to handle it
    # based on the different failure cases
    if failure
      if failure == :locked
        flash.now[:alert] = "oh no, you're locked out! Please check your email"
      else
        flash.now[:alert] = 'Login failed'
      end
      render action: 'new'
    # since there are no login failures, we can redirect the user
    # back to wherever they were trying to go, or the root page
    else
      redirect_back_or_to(:root, notice: 'Login successful')
    end
  end
end

Now please configure your Mailer

# UserMailer.rb
def send_unlock_token_email(user_id)
    @user = User.find(user_id)
    @url  = unlock_accounts_url(@user.unlock_token)
    mail(:to => @user.email,
         :subject => "Please unlock your account"
         )
  end


# mailers/users/send_unlock_token_email.html.erb 
<h1>Hello, <%= @user.email %></h1>
<p>
	You've been locked out of your account, because of too many incorrect password attempts.
</p>
<p>
	To unlock, just follow this link: <%= link_to("unlock account.", @url) %>
</p>
<p>Have a great day!</p>

# mailers/users/send_unlock_token_email.text.erb 
Hello, <%= @user.email %>
===============================================

You've been locked out of your account, because of too many incorrect password attempts.

To unlock, just follow this link: <%= link_to("unlock account.", @url) %>

Have a great day!

We need to create the unlock url and controllers:

  # routes.rb
  scope module: :users do
    get "unlock_accounts/:token", to: "unlock_accounts#show", as: "unlock_accounts"
  end

  # users/unlock_accounts_controller.rb
  module Users
  class UnlockAccountsController < ApplicationController
    skip_before_action :require_login, :only => [:show]
    def show
      if @user = User.load_from_unlock_token(params[:token])
        @user.login_unlock!
        redirect_to login_path, notice: "Please reset your password if you have forgotten it, or otherwise log in: #{view_context.link_to "here ", login_path}."
      else
        not_authenticated
      end
    end
  end
end