/devise_challenge_questionable

A devise extension to add a challenge question as part of login process

Primary LanguageRuby

Challenge questions plugin for Devise

This plugin forces a two step login process. After entering username/email and password, you must then answer a challenge question. Unlike security questions which are typically used when doing things like changing passwords, the challenge question must be answered each time logging in. You can set which users or admins need to answer a challenge question. We will assume your resource is User for the remainder of the Readme.

Features

  • configure max challenge question attempts
  • configure number of challenge questions stored for each user
  • configure number of challenge questions asked for each user
  • per user level control if he really need challenge question authentication

Configuration

Initial Setup

In a Rails environment, require the gem in your Gemfile:

gem 'devise_challenge_questionable'

Once that's done, run:

bundle install

Automatic installation

In order to add challenge questions to a model, run the command:

bundle exec rails g devise_challenge_questionable MODEL

bundle exec rails g devise_challenge_questionable:install

bundle exec rails g devise_challenge_questionable:views users

Where MODEL is your model name (e.g. User or Admin). This generator will add :challenge_questionable to your model and create a migration in db/migrate/, which will add :reset_challenge_questions_token and :challenge_question_failed_attempts to your table. Finally, run the migration with:

bundle exec rake db:migrate

Manual installation

To manually enable challenge questions for the User model, you should add the following. Set up relationships. You should already have a devise line so you would just add :challenge_questionable to it. Also, you need to allow accessibility to :user_challenge_questions_attributes. Replace user with whatever resource you are using. Typically it would be user or admin.

  has_many :user_challenge_questions, :validate => true, :inverse_of => :user
  accepts_nested_attributes_for :user_challenge_questions, :allow_destroy => true

  devise :challenge_questionable

  attr_accessible :user_challenge_questions_attributes

You also need to add the user_challenge_question.rb Model.

  class UserChallengeQuestion < ActiveRecord::Base
    belongs_to :user

    validates :challenge_question, :challenge_answer, :presence => true
    validates :challenge_answer, :length => { :in => 4..56 }, :format => {:with => /^[\w\s:]*$/, :message => "can not contain special characters"}, :allow_blank => true

    # Must use custom validation since uniqueness scope will not work with has_many association
    validate :challenge_question_uniqueness
    validate :challenge_answer_uniqueness
    validate :challenge_answer_repeating

    before_save :digest_challenge_answer

    def digest_challenge_answer
      if ENV['PASSWORD_PEPPER']
        write_attribute(:challenge_answer, ::BCrypt::Password.create(self.challenge_answer.downcase + ENV['PASSWORD_PEPPER'], :cost => Devise.stretches)) unless self.challenge_answer.nil?
      else
        write_attribute(:challenge_answer, ::BCrypt::Password.create(self.challenge_answer.downcase, :cost => Devise.stretches)) unless self.challenge_answer.nil?
      end
    end

    private
    def challenge_question_uniqueness
      if self.challenge_question.present? && self.user.user_challenge_questions.select{|q| q.challenge_question == self.challenge_question}.count > 1
        errors.add(:challenge_question, 'can only be used once')
      end
    end

    def challenge_answer_uniqueness
      if  self.challenge_answer.present? && self.user.user_challenge_questions.select{|q| q.challenge_answer == self.challenge_answer}.count > 1
        errors.add(:challenge_answer, 'can only be used once')
      end
    end

    def challenge_answer_repeating
      if self.challenge_answer.present? && self.challenge_answer =~ /(.)\1{2,}/
        errors.add(:challenge_answer, 'can not have more then two repeating characters in a row')
      end
    end
  end

Locales:

  en:
    activerecord:
      attributes:
        user:
          user_challenge_questions:
            challenge_question: "Question"
            challenge_answer: "Answer"
    devise:
      challenge_questions:
        attempt_failed: "Attempt failed."
        updated_challenge_questions: "Successfully updated challenge questions."
        send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."

Configuration settings

  # ==> Configuration for :challenge_questionable
  # Max challenge question attempts
  config.max_challenge_question_attempts = 3

  # Number of challenge questions to store for each user
  config.number_of_challenge_questions = 3

  # Default challenge question options
  config.challenge_questions = ["What was your high school mascot?","In which city was your first elementary school?","In which city was your mother born?","What is the name of your favorite movie?","Who is your favorite athlete?","What was your most memorable gift as a child?","What is your favorite cartoon character?","What is the name of your favorite novel?","Name of favorite childhood pet?","What is the name of your elementary school?","What is your youngest child's middle name?","Last Name of your kindergarten teacher?","What is the first name of your grandmother (your father's mother)?","What is your spouse's nickname?","Name of the place where your wedding reception was held?","Name of a college you applied to but did not attend?","What is the first name of the youngest of your siblings?","What is the first name of the eldest of your siblings?","What is your favorite television show?","If you needed a new first name, what would it be?","What is the first name of your youngest child?","When is your mother's birthday (MM/DD)?","What is your eldest child's middle name?","What is the last name of the funniest friend you know?","Name the highest mountain you've been to the top of?","What is the first name of your grandmother (your mother's mother)?","What is the first name of your grandfather (your mother's father)?","What was the first name of your best man/maid of honor?","What was the last name of your first grade teacher?","What is the last name of your first boyfriend or girlfriend?","Which high school did you attend?","What was your major during college?","What was the name of your first pet?","What was your favorite place from your childhood?","What is your favorite song?","What is your favorite car?","What is your mother’s middle name?","What is the (MM/DD) of your employment?","What is the make/model of first car?","What is the name of the city or town where you were born?","What is the name of your favorite childhood teacher?","What is the name of your favorite childhood friend?","What are your oldest sibling’s (MM/YYYY) of birth?","What is your oldest sibling’s middle name?","What school did you attend for sixth grade?","On what street did you live in third grade?"]

Customization

By default challenge questions are enabled for each user, you can change it with this method in your User model:

  def login_challenge_questions?(request)
    request.ip != '127.0.0.1'
  end

  def set_challenge_questions?(request)
    request.ip != '127.0.0.1'
  end

this will disable challenge questions for local users