devise_challenge_questionable 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +19 -0
  5. data/README.md +136 -0
  6. data/Rakefile +1 -0
  7. data/app/controllers/devise/challenge_questions_controller.rb +106 -0
  8. data/app/views/devise/challenge_questions/edit.html.erb +64 -0
  9. data/app/views/devise/challenge_questions/max_challenge_question_attempts_reached.html.erb +11 -0
  10. data/app/views/devise/challenge_questions/new.html.erb +12 -0
  11. data/app/views/devise/challenge_questions/show.html.erb +20 -0
  12. data/app/views/devise/mailer/reset_challenge_questions_instructions.html.erb +8 -0
  13. data/config/locales/en.yml +13 -0
  14. data/devise_challenge_questionable.gemspec +29 -0
  15. data/lib/devise_challenge_questionable.rb +32 -0
  16. data/lib/devise_challenge_questionable/controllers/helpers.rb +39 -0
  17. data/lib/devise_challenge_questionable/controllers/url_helpers.rb +24 -0
  18. data/lib/devise_challenge_questionable/hooks/challenge_questions.rb +5 -0
  19. data/lib/devise_challenge_questionable/mailer.rb +8 -0
  20. data/lib/devise_challenge_questionable/model.rb +102 -0
  21. data/lib/devise_challenge_questionable/rails.rb +21 -0
  22. data/lib/devise_challenge_questionable/routes.rb +13 -0
  23. data/lib/devise_challenge_questionable/schema.rb +7 -0
  24. data/lib/devise_challenge_questionable/version.rb +3 -0
  25. data/lib/generators/active_record/devise_challenge_questionable_generator.rb +14 -0
  26. data/lib/generators/active_record/templates/migration.rb +21 -0
  27. data/lib/generators/devise_challenge_questionable/devise_challenge_questionable_generator.rb +95 -0
  28. data/lib/generators/devise_challenge_questionable/install_generator.rb +35 -0
  29. data/lib/generators/devise_challenge_questionable/templates/challenge_question_model.rb +14 -0
  30. data/lib/generators/devise_challenge_questionable/views_generator.rb +10 -0
  31. metadata +108 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: be8f8f0325e99887de470960d55a3fc9c1b37ec7
4
+ data.tar.gz: fdaa87f16cfc9d30b8b20406525e3e5b193cbb9e
5
+ SHA512:
6
+ metadata.gz: d81bc9b249d222646417c525eaad2c68f87d404a0c49d777a02dc33bcf677c1bf0e40b6d8e65307c590dd5b793c35caf914d8a366a6fc548e43e8d01f7091066
7
+ data.tar.gz: dfefac2714845f6aff75bd17c11b0e515a821bf47bb2e9956033d9386bbadc43febe77289038f5d35c566c8344609ee5b797e9f70d808589abb5688715676c8d
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+
6
+ # Temporary files of every sort
7
+ .DS_Store
8
+ .idea
9
+ .rvmrc
10
+ .stgit*
11
+ *.swap
12
+ *.swo
13
+ *.swp
14
+ *~
15
+ bin/*
16
+ nbproject
17
+ patches-*
18
+ capybara-*.html
19
+ dump.rdb
20
+ *.ids
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in devise_ip_filter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2012 Dmitrii Golub
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ ## Challenge questions plugin for Devise
2
+
3
+ This plugin forces a two step login process. After entering username 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 preferred resource) need to answer a challenge question
4
+ ## Features
5
+
6
+ * configure max challenge question attempts
7
+ * configure number of challenge questions stored for each {resource}
8
+ * per {resource} level control if he really need challenge question authentication
9
+
10
+ ## Configuration
11
+
12
+ ### Initial Setup
13
+
14
+ In a Rails environment, require the gem in your Gemfile:
15
+
16
+ gem 'devise_challenge_questionable'
17
+
18
+ Once that's done, run:
19
+
20
+ bundle install
21
+
22
+
23
+ ### Automatic installation
24
+
25
+ In order to add challenge questions to a model, run the command:
26
+
27
+ bundle exec rails g devise_challenge_questionable MODEL
28
+
29
+ bundle exec rails g devise_challenge_questionable:install
30
+
31
+ bundle exec rails g devise_challenge_questionable:views {resource}s
32
+
33
+ Where MODEL is your model name (e.g. User or Admin). This generator will add `:challenge_questionable` to your model
34
+ and create a migration in `db/migrate/`, which will add `:reset_challenge_questions_token` and `:challenge_question_failed_attempts` to your table.
35
+ Finally, run the migration with:
36
+
37
+ bundle exec rake db:migrate
38
+
39
+
40
+ ### Manual installation
41
+
42
+ To manually enable challenge questions for the {Resource} 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 `:{resource}_challenge_questions_attributes`. Replace {resource} with whatever resource you are using. Typically it would be {resource} or admin.
43
+
44
+ ```ruby
45
+ has_many :{resource}_challenge_questions, :validate => true, :inverse_of => :{resource}
46
+ accepts_nested_attributes_for :{resource}_challenge_questions, :allow_destroy => true
47
+
48
+ devise :challenge_questionable
49
+
50
+ attr_accessible :{resource}_challenge_questions_attributes
51
+ ```
52
+
53
+ You also need to add the `{resource}_challenge_question.rb` Model.
54
+
55
+ ```ruby
56
+ class {Resource}ChallengeQuestion < ActiveRecord::Base
57
+ belongs_to :{resource}
58
+
59
+ validates :challenge_question, :challenge_answer, :presence => true
60
+ validates :challenge_answer, :length => { :in => 4..56 }, :format => {:with => /^[\w\s:]*$/, :message => "can not contain special characters"}, :allow_blank => true
61
+
62
+ # Must use custom validation since uniqueness scope will not work with has_many association
63
+ validate :challenge_question_uniqueness
64
+ validate :challenge_answer_uniqueness
65
+ validate :challenge_answer_repeating
66
+
67
+ before_save :digest_challenge_answer
68
+
69
+ def digest_challenge_answer
70
+ write_attribute(:challenge_answer, Digest::MD5.hexdigest(self.challenge_answer.downcase)) unless self.challenge_answer.nil?
71
+ end
72
+
73
+ private
74
+ def challenge_question_uniqueness
75
+ if self.challenge_question.present? && self.{resource}.{resource}_challenge_questions.select{|q| q.challenge_question == self.challenge_question}.count > 1
76
+ errors.add(:challenge_question, 'can only be used once')
77
+ end
78
+ end
79
+
80
+ def challenge_answer_uniqueness
81
+ if self.challenge_answer.present? && self.{resource}.{resource}_challenge_questions.select{|q| q.challenge_answer == self.challenge_answer}.count > 1
82
+ errors.add(:challenge_answer, 'can only be used once')
83
+ end
84
+ end
85
+
86
+ def challenge_answer_repeating
87
+ if self.challenge_answer.present? && self.challenge_answer =~ /(.)\1{2,}/
88
+ errors.add(:challenge_answer, 'can not have more then two repeating characters in a row')
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ Locales:
95
+
96
+ ```yaml
97
+ en:
98
+ activerecord:
99
+ attributes:
100
+ user:
101
+ user_challenge_questions:
102
+ challenge_question: "Question"
103
+ challenge_answer: "Answer"
104
+ devise:
105
+ challenge_questions:
106
+ attempt_failed: "Attempt failed."
107
+ updated_challenge_questions: "Successfully updated challenge questions."
108
+ send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."
109
+ ```
110
+
111
+ Configuration settings
112
+
113
+ ```ruby
114
+ # ==> Configuration for :challenge_questionable
115
+ # Max challenge question attempts
116
+ config.max_challenge_question_attempts = 3
117
+
118
+ # Number of challenge questions to store for each {resource}
119
+ config.number_of_challenge_questions = 3
120
+
121
+ # Default challenge question options
122
+ 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?"]
123
+
124
+ ```
125
+
126
+ ### Customization
127
+
128
+ By default challenge questions are enabled for each {resource}, you can change it with this method in your User model:
129
+
130
+ ```ruby
131
+ def need_challenge_questions?(request)
132
+ request.ip != '127.0.0.1'
133
+ end
134
+ ```
135
+
136
+ this will disable challenge questions for local {resource}s
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,106 @@
1
+ class Devise::ChallengeQuestionsController < ApplicationController
2
+ include Devise::Controllers::InternalHelpers
3
+ prepend_before_filter :require_no_authentication, :only => [ :new, :create, :edit, :update ]
4
+ prepend_before_filter :authenticate_scope!, :only => [:show, :authenticate, :manage, :forgot]
5
+ before_filter :prepare_and_validate, :handle_challenge_questions, :only => [:show, :authenticate]
6
+
7
+ # GET /resource/challenge_question/new
8
+ def new
9
+ build_resource
10
+ render_with_scope :new
11
+ end
12
+
13
+ # POST /resource/challenge_question
14
+ def create
15
+ self.resource = resource_class.send_reset_challenge_questions_instructions(params[resource_name])
16
+
17
+ if resource.errors.empty?
18
+ set_flash_message :notice, :send_instructions
19
+ redirect_to new_session_path(resource_name)
20
+ else
21
+ render_with_scope :new
22
+ end
23
+ end
24
+
25
+ # GET /resource/challenge_question/edit?reset_challenge_questions_token=abcdef
26
+ def edit
27
+ self.resource = resource_class.new
28
+ resource.reset_challenge_questions_token = params[:reset_challenge_questions_token]
29
+ Devise.number_of_challenge_questions.times { resource.send("#{resource_name}_challenge_questions").build }
30
+ render_with_scope :edit
31
+ end
32
+
33
+ # PUT /resource/challenge_question
34
+ def update
35
+ self.resource = resource_class.reset_challenge_questions_by_token(params[resource_name])
36
+
37
+ if resource.errors.empty?
38
+ set_flash_message :notice, :updated_challenge_questions
39
+ redirect_to :root
40
+ else
41
+ render_with_scope :edit
42
+ end
43
+ end
44
+
45
+ # GET /resource/challenge_question
46
+ def show
47
+ @challenge_question = resource.send("#{resource_name}_challenge_questions").sample
48
+ if @challenge_question.nil?
49
+ resource.set_reset_challenge_questions_token
50
+ sign_out(resource)
51
+ redirect_to edit_challenge_question_path(resource, :reset_challenge_questions_token => resource.reset_challenge_questions_token)
52
+ else
53
+ render_with_scope :show
54
+ end
55
+ end
56
+
57
+ def authenticate
58
+ render_with_scope :show and return if params[:challenge_answer].nil?
59
+ @challenge_question = resource.send("#{resource_name}_challenge_questions").find(params[:challenge_question_id])
60
+ md5 = Digest::MD5.hexdigest(params[:challenge_answer].downcase)
61
+ if md5.eql?(@challenge_question.challenge_answer)
62
+ warden.session(resource_name)[:need_challenge_questions] = false
63
+ sign_in resource_name, resource
64
+ redirect_to stored_location_for(resource_name) || :root
65
+ resource.update_attribute(:challenge_question_failed_attempts, 0)
66
+ else
67
+ resource.challenge_question_failed_attempts += 1
68
+ resource.save
69
+ set_flash_message :notice, :attempt_failed
70
+ if resource.max_challenge_question_attempts?
71
+ resource.max_challenge_question_lock_account
72
+ sign_out(resource)
73
+ render_with_scope :max_challenge_question_attempts_reached and return
74
+ else
75
+ redirect_to send("#{resource_name}_challenge_question_path")
76
+ end
77
+ end
78
+ end
79
+
80
+ # Build token and redirect
81
+ def manage
82
+ resource.set_reset_challenge_questions_token
83
+ sign_out(resource)
84
+ redirect_to edit_challenge_question_path(resource, :reset_challenge_questions_token => resource.reset_challenge_questions_token)
85
+ end
86
+
87
+ def forgot
88
+ sign_out(resource)
89
+ redirect_to new_challenge_question_path(resource_name)
90
+ end
91
+
92
+ private
93
+
94
+ def authenticate_scope!
95
+ self.resource = send("current_#{resource_name}")
96
+ end
97
+
98
+ def prepare_and_validate
99
+ redirect_to :root and return if resource.nil?
100
+ @limit = resource.class.max_challenge_question_attempts
101
+ if resource.max_challenge_question_attempts?
102
+ sign_out(resource)
103
+ render_with_scope :max_challenge_question_attempts_reached and return
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,64 @@
1
+ <h3>Change Your Challenge Questions</h3>
2
+ <p>Change your current challenge questions by filling out the form below. The questions you set up previously will be replaced with these new questions. All fields are required.</p>
3
+
4
+ <fieldset>
5
+ <h4>Please select <%= Devise.number_of_challenge_questions %> questions below, 1 from each drop down. Then type your answers in the field provided.</h4>
6
+ <%= form_for(resource, :as => resource_name, :url => challenge_question_path(resource_name), :html => { :method => :put }) do |f| %>
7
+ <%= devise_error_messages! %>
8
+ <%= f.hidden_field :id %>
9
+ <%= f.hidden_field :reset_challenge_questions_token %>
10
+
11
+ <%= f.fields_for :"#{resource_name}_challenge_questions", resource.send("#{resource_name}_challenge_questions") do |cq| %>
12
+ <% if cq.object.new_record? %>
13
+ <p>
14
+ Question: <%= cq.select :challenge_question, Devise.challenge_questions.shuffle, :prompt => 'Please select a question' %>
15
+ <br />
16
+ Answer:&nbsp;&nbsp;&nbsp; <%= cq.text_field :challenge_answer, :placeholder => "Type your answer here", :autocomplete => "off" %>
17
+ <hr>
18
+ </p>
19
+ <% end %>
20
+ <% end %>
21
+ <p>Remember, your current Challenge Questions will be overwritten.</p>
22
+ <p><%= f.submit "Change my challenge questions" %></p>
23
+ <% end %>
24
+ </fieldset>
25
+
26
+ <%- content_for :sidebar do %>
27
+ <div>
28
+ <h3>Tips for Creating Security Questions</h3>
29
+ <ul>
30
+ <li>1. Please choose questions with clear answers that you can remember easily.</li>
31
+ <li>2. All questions must be answered.</li>
32
+ <li>3. You cannot use the same answer for more than 1 question.</li>
33
+ <li>4. Answers must have between 3 and 56 characters.</li>
34
+ <li>5. Answers cannot have more than 2 repeating characters in a row.</li>
35
+ <li>6. Special characters, such as & and () are not allowed.</li>
36
+ <li>7. Answers are not case sensitive.</li>
37
+ </ul>
38
+ </div>
39
+ <% end %>
40
+
41
+ <script type="text/javascript" language="javascript">
42
+ if (window.jQuery) {
43
+ jQuery(document).ready(function() {
44
+ $("select").change(function () {
45
+ var $this = $(this);
46
+ var prevVal = $this.data("prev");
47
+ var otherSelects = $("select").not(this);
48
+ otherSelects.find('option:contains('+ $(this).val() +')').attr('disabled', 'disabled');
49
+ if (prevVal) {
50
+ otherSelects.find('option:contains('+ prevVal +')').removeAttr('disabled');
51
+ }
52
+ $this.data("prev", $this.val());
53
+ });
54
+
55
+ $("select").each(function () {
56
+ var $this = $(this);
57
+ var prevVal = $this.data("prev");
58
+ var otherSelects = $("select").not(this);
59
+ otherSelects.find('option:contains('+ $(this).val() +')').attr('disabled', 'disabled');
60
+ $this.data("prev", $this.val());
61
+ });
62
+ });
63
+ }
64
+ </script>
@@ -0,0 +1,11 @@
1
+ <h3>Please contact your system administrator.</h3>
2
+
3
+ <fieldset>
4
+ <p>
5
+ Access denied as you have reached your max challenge question attempts.
6
+ </p>
7
+ <p>
8
+ <%= link_to "Forgot your challenge questions?", new_challenge_question_path(resource_name) %>
9
+ </p>
10
+ </fieldset>
11
+
@@ -0,0 +1,12 @@
1
+ <h3>Forgot your challenge questions?</h3>
2
+
3
+ <%= form_for(resource, :as => resource_name, :url => challenge_question_path(resource_name), :html => { :method => :post }) do |f| %>
4
+ <fieldset>
5
+ <%= devise_error_messages! %>
6
+
7
+ <p><%= f.label :email %><br />
8
+ <%= f.text_field :email, :autocomplete => "off", :placeholder => "example@email.com" %></p>
9
+
10
+ <p><%= f.submit "Send me reset challenge question instructions" %></p>
11
+ </fieldset>
12
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <h3>Answer the challenge question below</h3>
2
+
3
+ <%= form_tag([:authenticate, resource_name, :challenge_question], :method => :put) do %>
4
+ <fieldset>
5
+ <p>
6
+ <%= hidden_field_tag :challenge_question_id, @challenge_question.id%>
7
+ <%= label_tag :challege_question, @challenge_question.challenge_question %>
8
+ <br>
9
+ <%= password_field_tag :challenge_answer, nil, :placeholder => 'Answer', :class => 'wide', :style => 'font-size: 1.8em;padding:5px', :autofocus => 'true', :autocomplete => "off" %>
10
+ </p>
11
+ <p>
12
+ <%= submit_tag "Submit", :style => 'font-size:1.8em;'%>
13
+ or
14
+ <%= link_to "Cancel", destroy_user_session_path %>
15
+ </p>
16
+ <p>
17
+ <%= link_to "Forgot your challenge questions?", forgot_challenge_question_path(resource_name) %>
18
+ </p>
19
+ </fieldset>
20
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>Someone has requested a link to change your challenge questions, and you can do this through the link below.</p>
4
+
5
+ <p><%= link_to 'Change my challenge questions', edit_challenge_question_url(@resource, :reset_challenge_questions_token => @resource.reset_challenge_questions_token) %></p>
6
+
7
+ <p>If you didn't request this, please ignore this email.</p>
8
+ <p>Your challenge questions won't change until you access the link above and create a new ones.</p>
@@ -0,0 +1,13 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ user:
5
+ user_challenge_questions:
6
+ challenge_question: "Question"
7
+ challenge_answer: "Answer"
8
+ devise:
9
+ challenge_questions:
10
+ attempt_failed: "Attempt failed."
11
+ updated_challenge_questions: "Successfully updated challenge questions."
12
+ send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."
13
+
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "devise_challenge_questionable/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "devise_challenge_questionable"
7
+ s.version = DeviseChallengeQuestionable::VERSION.dup
8
+ s.authors = ["Andrew Kennedy"]
9
+ s.email = ["andrew@akennedy.io"]
10
+ s.homepage = "https://github.com/akennedy/devise_challenge_questionable"
11
+ s.summary = %q{Challenge question plugin for devise}
12
+ s.description = <<-EOF
13
+ ### Features ###
14
+ * configure max challenge question attempts
15
+ * per user level control if he really need challenge questions
16
+ EOF
17
+
18
+ s.rubyforge_project = "devise_challenge_questionable"
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+
25
+ s.add_runtime_dependency 'rails', '3.0.20'
26
+ s.add_runtime_dependency 'devise', '1.1.3'
27
+
28
+ s.add_development_dependency 'bundler'
29
+ end
@@ -0,0 +1,32 @@
1
+ require 'devise_challenge_questionable/version'
2
+ require 'devise'
3
+ require 'digest'
4
+ require 'active_support/concern'
5
+
6
+ module Devise
7
+ mattr_accessor :max_challenge_question_attempts
8
+ @@max_challenge_question_attempts = 3
9
+
10
+ mattr_accessor :challenge_questions
11
+ @@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?"]
12
+
13
+
14
+ mattr_accessor :number_of_challenge_questions
15
+ @@number_of_challenge_questions = 3
16
+ end
17
+
18
+ module DeviseChallengeQuestionable
19
+ autoload :Schema, 'devise_challenge_questionable/schema'
20
+ module Controllers
21
+ autoload :Helpers, 'devise_challenge_questionable/controllers/helpers'
22
+ autoload :UrlHelpers, 'devise_challenge_questionable/controllers/url_helpers'
23
+ end
24
+ end
25
+
26
+ Devise.add_module :challenge_questionable, :model => 'devise_challenge_questionable/model', :controller => :challenge_questions, :route => :challenge_question
27
+
28
+ require 'devise_challenge_questionable/mailer'
29
+ require 'devise_challenge_questionable/routes'
30
+ require 'devise_challenge_questionable/model'
31
+ require 'devise_challenge_questionable/rails'
32
+
@@ -0,0 +1,39 @@
1
+ module DeviseChallengeQuestionable
2
+ module Controllers
3
+ module Helpers
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_filter :handle_challenge_questions
8
+ end
9
+
10
+ private
11
+
12
+ def handle_challenge_questions
13
+ unless devise_controller? && (controller_name != 'registrations' && ![:edit, :update, :destroy].include?(action_name))
14
+ Devise.mappings.keys.flatten.any? do |scope|
15
+ if signed_in?(scope) and warden.session(scope)[:need_challenge_questions]
16
+ failed_challenge_question(scope)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def failed_challenge_question(scope)
23
+ if request.format.present? and request.format.html?
24
+ session["#{scope}_return_to"] = request.path if request.get?
25
+ redirect_to challenge_questions_path_for(scope)
26
+ else
27
+ render :nothing => true, :status => :unauthorized
28
+ end
29
+ end
30
+
31
+ def challenge_questions_path_for(resource_or_scope = nil)
32
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
33
+ change_path = "#{scope}_challenge_question_path"
34
+ send(change_path)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ module DeviseChallengeQuestionable
2
+ module Controllers
3
+ module UrlHelpers
4
+ [:path, :url].each do |path_or_url|
5
+ [nil, :edit_, :authenticate_, :manage_, :forgot_].each do |action|
6
+ class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
7
+ def #{action}challenge_question_#{path_or_url}(resource, *args)
8
+ resource = case resource
9
+ when Symbol, String
10
+ resource
11
+ when Class
12
+ resource.name.underscore
13
+ else
14
+ resource.class.name.underscore
15
+ end
16
+
17
+ send("#{action}\#{resource}_challenge_question_#{path_or_url}", *args)
18
+ end
19
+ URL_HELPERS
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ Warden::Manager.after_authentication do |user, auth, options|
2
+ if user.respond_to?(:need_challenge_questions?)
3
+ auth.session(options[:scope])[:need_challenge_questions] = user.need_challenge_questions?(auth.request)
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ module DeviseChallengeQuestionable
2
+ module Mailer
3
+ # Deliver reset challenge question instructions when is requested
4
+ def reset_challenge_questions_instructions(record)
5
+ setup_mail(record, :reset_challenge_questions_instructions)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,102 @@
1
+ require 'devise_challenge_questionable/hooks/challenge_questions'
2
+ module Devise
3
+ module Models
4
+ module ChallengeQuestionable
5
+ extend ActiveSupport::Concern
6
+
7
+ # Update challenge questions saving the record and clearing token. Returns true if
8
+ # the challenge questions are valid and the record was saved, false otherwise.
9
+ def reset_challenge_questions!(attributes)
10
+ self.send("#{self.class.name.underscore}_challenge_questions").destroy_all
11
+ self.attributes = {"#{self.class.name.underscore}_challenge_questions_attributes" => attributes["#{self.class.name.underscore}_challenge_questions_attributes"]}
12
+ clear_reset_challenge_questions_token if self.valid?
13
+ self.save
14
+ end
15
+
16
+ def need_challenge_questions?(request)
17
+ true
18
+ end
19
+
20
+ def max_challenge_question_lock_account
21
+ self.lock_access! if self.class.lock_strategy_enabled?(:failed_attempts)
22
+ end
23
+
24
+ def max_challenge_question_attempts?
25
+ challenge_question_failed_attempts >= self.class.max_challenge_question_attempts
26
+ end
27
+
28
+ # Resets reset challenge question token and send reset challenge question instructions by email
29
+ def send_reset_challenge_questions_instructions
30
+ generate_reset_challenge_questions_token!
31
+ ::Devise.mailer.reset_challenge_questions_instructions(self).deliver
32
+ end
33
+
34
+ # Generates a new random token for reset challenge_question
35
+ def set_reset_challenge_questions_token
36
+ generate_reset_challenge_questions_token!
37
+ end
38
+
39
+ def unlock_access_including_challenge_questions!
40
+ if_access_locked do
41
+ self.locked_at = nil
42
+ self.failed_attempts = 0 if respond_to?(:failed_attempts=)
43
+ self.challenge_question_failed_attempts = 0 if respond_to?(:challenge_question_failed_attempts=)
44
+ self.unlock_token = nil if respond_to?(:unlock_token=)
45
+ save(:validate => false)
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ # Generates a new random token for reset challenge_question
52
+ def generate_reset_challenge_questions_token
53
+ self.reset_challenge_questions_token = self.class.reset_challenge_questions_token
54
+ end
55
+
56
+ # Resets the reset challenge_question token with and save the record without
57
+ # validating
58
+ def generate_reset_challenge_questions_token!
59
+ generate_reset_challenge_questions_token && save(:validate => false)
60
+ end
61
+
62
+ # Removes reset_challenge_question token
63
+ def clear_reset_challenge_questions_token
64
+ self.reset_challenge_questions_token = nil
65
+ end
66
+
67
+ def clear_challenge_question_failed_attempts
68
+ self.challenge_question_failed_attempts = 0
69
+ end
70
+
71
+ module ClassMethods
72
+ ::Devise::Models.config(self, :max_challenge_question_attempts)
73
+
74
+ # Attempt to find a user by it's email. If a record is found, send new
75
+ # challenge_question instructions to it. If not user is found, returns a new user
76
+ # with an email not found error.
77
+ # Attributes must contain the user email
78
+ def send_reset_challenge_questions_instructions(attributes={})
79
+ challenge_questionable = find_or_initialize_with_error_by(:email, attributes[:email], :not_found)
80
+ challenge_questionable.send_reset_challenge_questions_instructions if challenge_questionable.persisted?
81
+ challenge_questionable
82
+ end
83
+
84
+ # Generate a token checking if one does not already exist in the database.
85
+ def reset_challenge_questions_token
86
+ generate_token(:reset_challenge_questions_token)
87
+ end
88
+
89
+ # Attempt to find a user by it's reset_challenge_questions_token to reset it's
90
+ # challenge_question. If a user is found, reset it's challenge_question and automatically
91
+ # try saving the record. If not user is found, returns a new user
92
+ # containing an error in reset_challenge_questions_token attribute.
93
+ # Attributes must contain reset_challenge_questions_token, challenge_question and confirmation
94
+ def reset_challenge_questions_by_token(attributes={})
95
+ challenge_questionable = find_or_initialize_with_error_by(:reset_challenge_questions_token, attributes[:reset_challenge_questions_token])
96
+ challenge_questionable.reset_challenge_questions!(attributes) if challenge_questionable.persisted?
97
+ challenge_questionable
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,21 @@
1
+ module DeviseChallengeQuestionable
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) { include DeviseChallengeQuestionable::Controllers::Helpers }
4
+ ActiveSupport.on_load(:action_controller) { include DeviseChallengeQuestionable::Controllers::UrlHelpers }
5
+ ActiveSupport.on_load(:action_view) { include DeviseChallengeQuestionable::Controllers::UrlHelpers }
6
+
7
+ # We use to_prepare instead of after_initialize here because Devise is a Rails engine; its
8
+ # mailer is reloaded like the rest of the user's app. Got to make sure that our mailer methods
9
+ # are included each time Devise::Mailer is (re)loaded.
10
+ config.to_prepare do
11
+ require 'devise/mailer'
12
+ Devise::Mailer.send :include, DeviseChallengeQuestionable::Mailer
13
+ end
14
+
15
+
16
+ initializer "devise.add_filters" do |app|
17
+ app.config.filter_parameters += [:challenge_answer, :reset_challenge_questions_token]
18
+ app.config.filter_parameters.uniq
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+ protected
4
+
5
+ def devise_challenge_question(mapping, controllers)
6
+ resource :challenge_question, :only => [:show, :new, :create, :edit, :update], :path => mapping.path_names[:challenge_question], :controller => controllers[:challenge_questions] do
7
+ put :authenticate, :path => mapping.path_names[:authenticate]
8
+ get :manage, :path => mapping.path_names[:manage]
9
+ get :forgot, :path => mapping.path_names[:forgot]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module DeviseChallengeQuestionable
2
+ module Schema
3
+ # def second_factor_attempts_count
4
+ # apply_devise_schema :second_factor_attempts_count, Integer, :default => 0
5
+ # end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module DeviseChallengeQuestionable
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ActiveRecord
4
+ module Generators
5
+ class DeviseChallengeQuestionableGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ def copy_migration
9
+ migration_template "migration.rb", "db/migrate/devise_challenge_questionable_add_to_#{table_name}"
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ class DeviseChallengeQuestionableAddTo<%= table_name.camelize %> < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :<%= table_name %>, :reset_challenge_questions_token, :string
4
+ add_column :<%= table_name %>, :challenge_question_failed_attempts, :integer, :default => 0
5
+
6
+ create_table :<%= class_name.underscore %>_challenge_questions do |t|
7
+ t.integer :<%= table_name.singularize %>_id
8
+ t.string :challenge_question
9
+ t.string :challenge_answer
10
+ t.timestamps
11
+ end
12
+
13
+ end
14
+
15
+ def self.down
16
+ drop_table :<%= class_name.underscore %>_challenge_questions
17
+ remove_column :<%= table_name %>, :reset_challenge_questions_token
18
+ remove_column :<%= table_name %>, :challenge_question_failed_attempts
19
+ end
20
+
21
+ end
@@ -0,0 +1,95 @@
1
+ module DeviseChallengeQuestionable
2
+ module Generators
3
+ class DeviseChallengeQuestionableGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("../templates", __FILE__)
5
+ namespace "devise_challenge_questionable"
6
+
7
+ desc "Adds :challenge_questionable directive in the given model. It also generates an active record migration."
8
+
9
+ def inject_devise_challenge_questionable_content
10
+ path = File.join("app", "models", "#{file_path}.rb")
11
+ inject_into_file(path, "challenge_questionable, :", :after => "devise :") if File.exists?(path)
12
+ inject_into_file(path, "user_challenge_questions_attributes, :", :after => "attr_accessible :") if File.exists?(path)
13
+ inject_into_file path, :after => "class #{class_name} < ActiveRecord::Base\n" do
14
+ <<-CONTENT
15
+ has_many :#{class_name.underscore}_challenge_questions, :validate => true, :inverse_of => :user
16
+ accepts_nested_attributes_for :#{class_name.underscore}_challenge_questions, :allow_destroy => true
17
+ CONTENT
18
+ end if File.exists?(path)
19
+ end
20
+
21
+ def generate_model
22
+ invoke "active_record:model", ["#{file_path}_challenge_question"], :migration => false
23
+ end
24
+
25
+ def inject_scope_challenge_question_content
26
+ path = File.join("app", "models", "#{file_path}_challenge_question.rb")
27
+ inject_into_file path, :before => /^end/ do
28
+ <<-CONTENT
29
+ belongs_to :#{class_name.underscore}
30
+
31
+ validates :challenge_question, :challenge_answer, :presence => true
32
+ validates :challenge_answer, :length => { :in => 3..56 }, :format => {:with => /^[\\w\\s:]*$/, :message => "can not contain special characters"}, :allow_blank => true
33
+
34
+ # Must use custom validation since uniqueness scope will not work with has_many association
35
+ validate :challenge_question_uniqueness
36
+ validate :challenge_answer_uniqueness
37
+ validate :challenge_answer_repeating
38
+
39
+ before_save :digest_challenge_answer
40
+
41
+ def digest_challenge_answer
42
+ write_attribute(:challenge_answer, Digest::MD5.hexdigest(self.challenge_answer.downcase)) unless self.challenge_answer.nil?
43
+ end
44
+
45
+ private
46
+ def challenge_question_uniqueness
47
+ if self.challenge_question.present? && self.#{class_name.underscore}.#{class_name.underscore}_challenge_questions.select{|q| q.challenge_question == self.challenge_question}.count > 1
48
+ errors.add(:challenge_question, 'can only be used once')
49
+ end
50
+ end
51
+
52
+ def challenge_answer_uniqueness
53
+ if self.challenge_answer.present? && self.#{class_name.underscore}.#{class_name.underscore}_challenge_questions.select{|q| q.challenge_answer == self.challenge_answer}.count > 1
54
+ errors.add(:challenge_answer, 'can only be used once')
55
+ end
56
+ end
57
+
58
+ def challenge_answer_repeating
59
+ if self.challenge_answer.present? && self.challenge_answer =~ /(.)\1{2,}/
60
+ errors.add(:challenge_answer, 'can not have more then two repeating characters in a row')
61
+ end
62
+ end
63
+ CONTENT
64
+ end if File.exists?(path)
65
+ end
66
+
67
+ def inject_localization_model_content
68
+ path = File.join("config", "locales", "devise.en.yml")
69
+ inject_into_file path, :after => "en:\n" do
70
+ <<-CONTENT
71
+ activerecord:
72
+ attributes:
73
+ #{class_name.underscore}:
74
+ #{class_name.underscore}_challenge_questions:
75
+ challenge_question: "Question"
76
+ challenge_answer: "Answer"
77
+ CONTENT
78
+ end if File.exists?(path)
79
+ inject_into_file path, :after => "devise:\n" do
80
+ <<-CONTENT
81
+ challenge_questions:
82
+ attempt_failed: "Attempt failed."
83
+ updated_challenge_questions: "Successfully updated challenge questions."
84
+ send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."
85
+ CONTENT
86
+ end if File.exists?(path)
87
+ end
88
+
89
+ hook_for :orm
90
+
91
+ end
92
+ end
93
+ end
94
+
95
+
@@ -0,0 +1,35 @@
1
+ module DeviseChallengeQuestionable
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+ desc "Add DeviseChallengeQuestionable config variables to the Devise initializer and copy DeviseChallengeQuestionable locale files to your application."
6
+
7
+ def add_config_options_to_initializer
8
+ devise_initializer_path = "config/initializers/devise.rb"
9
+ if File.exist?(devise_initializer_path)
10
+ old_content = File.read(devise_initializer_path)
11
+
12
+ if old_content.match(Regexp.new(/^\s# ==> Configuration for :challenge_questionable\n/))
13
+ false
14
+ else
15
+ inject_into_file(devise_initializer_path, :before => " # ==> Configuration for :confirmable\n") do
16
+ <<-CONTENT
17
+ # ==> Configuration for :challenge_questionable
18
+ # Max challenge question attempts
19
+ config.max_challenge_question_attempts = 3
20
+
21
+ # Number of challenge questions to store for each user
22
+ config.number_of_challenge_questions = 3
23
+
24
+ # Default challenge question options
25
+ 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?"]
26
+
27
+ CONTENT
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,14 @@
1
+ class <%= class_name %>ChallengeQuestion < ActiveRecord::Base
2
+
3
+ belongs_to :<%= class_name.underscore %>
4
+
5
+ validates :challenge_question, :uniqueness => {:scope => :<%= class_name.underscore %>_id}
6
+ validates :challenge_answer, :presence => true
7
+
8
+ before_save :digest_challenge_answer
9
+
10
+ def digest_challenge_answer
11
+ write_attribute(:challenge_answer, Digest::MD5.hexdigest(self.challenge_answer)) unless self.challenge_answer.nil?
12
+ end
13
+
14
+ end
@@ -0,0 +1,10 @@
1
+ require 'generators/devise/views_generator'
2
+
3
+ module DeviseChallengeQuestionable
4
+ module Generators
5
+ class ViewsGenerator < Devise::Generators::ViewsGenerator
6
+ source_root File.expand_path("../../../../app/views", __FILE__)
7
+ desc 'Copies all DeviseChallengeQuestionable views to your application.'
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devise_challenge_questionable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Kennedy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2014-01-23 00:00:00 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ prerelease: false
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "="
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.20
22
+ type: :runtime
23
+ version_requirements: *id001
24
+ - !ruby/object:Gem::Dependency
25
+ name: devise
26
+ prerelease: false
27
+ requirement: &id002 !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - "="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.1.3
32
+ type: :runtime
33
+ version_requirements: *id002
34
+ - !ruby/object:Gem::Dependency
35
+ name: bundler
36
+ prerelease: false
37
+ requirement: &id003 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - &id004
40
+ - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ type: :development
44
+ version_requirements: *id003
45
+ description: " ### Features ###\n * configure max challenge question attempts\n * per user level control if he really need challenge questions\n"
46
+ email:
47
+ - andrew@akennedy.io
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files: []
53
+
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - app/controllers/devise/challenge_questions_controller.rb
61
+ - app/views/devise/challenge_questions/edit.html.erb
62
+ - app/views/devise/challenge_questions/max_challenge_question_attempts_reached.html.erb
63
+ - app/views/devise/challenge_questions/new.html.erb
64
+ - app/views/devise/challenge_questions/show.html.erb
65
+ - app/views/devise/mailer/reset_challenge_questions_instructions.html.erb
66
+ - config/locales/en.yml
67
+ - devise_challenge_questionable.gemspec
68
+ - lib/devise_challenge_questionable.rb
69
+ - lib/devise_challenge_questionable/controllers/helpers.rb
70
+ - lib/devise_challenge_questionable/controllers/url_helpers.rb
71
+ - lib/devise_challenge_questionable/hooks/challenge_questions.rb
72
+ - lib/devise_challenge_questionable/mailer.rb
73
+ - lib/devise_challenge_questionable/model.rb
74
+ - lib/devise_challenge_questionable/rails.rb
75
+ - lib/devise_challenge_questionable/routes.rb
76
+ - lib/devise_challenge_questionable/schema.rb
77
+ - lib/devise_challenge_questionable/version.rb
78
+ - lib/generators/active_record/devise_challenge_questionable_generator.rb
79
+ - lib/generators/active_record/templates/migration.rb
80
+ - lib/generators/devise_challenge_questionable/devise_challenge_questionable_generator.rb
81
+ - lib/generators/devise_challenge_questionable/install_generator.rb
82
+ - lib/generators/devise_challenge_questionable/templates/challenge_question_model.rb
83
+ - lib/generators/devise_challenge_questionable/views_generator.rb
84
+ homepage: https://github.com/akennedy/devise_challenge_questionable
85
+ licenses: []
86
+
87
+ metadata: {}
88
+
89
+ post_install_message:
90
+ rdoc_options: []
91
+
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - *id004
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - *id004
100
+ requirements: []
101
+
102
+ rubyforge_project: devise_challenge_questionable
103
+ rubygems_version: 2.0.13
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Challenge question plugin for devise
107
+ test_files: []
108
+