devise_xfactor_authentication 2.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +21 -0
  3. data/.gitignore +23 -0
  4. data/.rubocop.yml +295 -0
  5. data/.travis.yml +28 -0
  6. data/CHANGELOG.md +119 -0
  7. data/Gemfile +31 -0
  8. data/LICENSE +19 -0
  9. data/README.md +405 -0
  10. data/Rakefile +14 -0
  11. data/app/controllers/devise/devise_xfactor_authentication_controller.rb +84 -0
  12. data/app/views/devise/devise_xfactor_authentication/max_login_attempts_reached.html.erb +3 -0
  13. data/app/views/devise/devise_xfactor_authentication/show.html.erb +19 -0
  14. data/config/locales/de.yml +8 -0
  15. data/config/locales/en.yml +8 -0
  16. data/config/locales/es.yml +8 -0
  17. data/config/locales/fr.yml +8 -0
  18. data/config/locales/ru.yml +8 -0
  19. data/devise_xfactor_authentication.gemspec +39 -0
  20. data/lib/devise_xfactor_authentication/controllers/helpers.rb +49 -0
  21. data/lib/devise_xfactor_authentication/hooks/devise_xfactor_authenticatable.rb +17 -0
  22. data/lib/devise_xfactor_authentication/models/devise_xfactor_authenticatable.rb +203 -0
  23. data/lib/devise_xfactor_authentication/orm/active_record.rb +14 -0
  24. data/lib/devise_xfactor_authentication/rails.rb +7 -0
  25. data/lib/devise_xfactor_authentication/routes.rb +11 -0
  26. data/lib/devise_xfactor_authentication/schema.rb +31 -0
  27. data/lib/devise_xfactor_authentication/version.rb +3 -0
  28. data/lib/devise_xfactor_authentication.rb +52 -0
  29. data/lib/generators/active_record/devise_xfactor_authentication_generator.rb +14 -0
  30. data/lib/generators/active_record/templates/migration.rb +14 -0
  31. data/lib/generators/devise_xfactor_authentication/devise_xfactor_authentication_generator.rb +17 -0
  32. data/spec/controllers/devise_xfactor_authentication_controller_spec.rb +41 -0
  33. data/spec/features/devise_xfactor_authenticatable_spec.rb +237 -0
  34. data/spec/generators/active_record/devise_xfactor_authentication_generator_spec.rb +36 -0
  35. data/spec/lib/devise_xfactor_authentication/models/devise_xfactor_authenticatable_spec.rb +326 -0
  36. data/spec/rails_app/.gitignore +3 -0
  37. data/spec/rails_app/README.md +3 -0
  38. data/spec/rails_app/Rakefile +7 -0
  39. data/spec/rails_app/app/assets/javascripts/application.js +1 -0
  40. data/spec/rails_app/app/assets/stylesheets/application.css +4 -0
  41. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  42. data/spec/rails_app/app/controllers/home_controller.rb +10 -0
  43. data/spec/rails_app/app/helpers/application_helper.rb +8 -0
  44. data/spec/rails_app/app/mailers/.gitkeep +0 -0
  45. data/spec/rails_app/app/models/.gitkeep +0 -0
  46. data/spec/rails_app/app/models/admin.rb +6 -0
  47. data/spec/rails_app/app/models/encrypted_user.rb +15 -0
  48. data/spec/rails_app/app/models/guest_user.rb +17 -0
  49. data/spec/rails_app/app/models/user.rb +14 -0
  50. data/spec/rails_app/app/views/home/dashboard.html.erb +11 -0
  51. data/spec/rails_app/app/views/home/index.html.erb +3 -0
  52. data/spec/rails_app/app/views/layouts/application.html.erb +20 -0
  53. data/spec/rails_app/config/application.rb +63 -0
  54. data/spec/rails_app/config/boot.rb +10 -0
  55. data/spec/rails_app/config/database.yml +19 -0
  56. data/spec/rails_app/config/environment.rb +5 -0
  57. data/spec/rails_app/config/environments/development.rb +28 -0
  58. data/spec/rails_app/config/environments/production.rb +68 -0
  59. data/spec/rails_app/config/environments/test.rb +41 -0
  60. data/spec/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  61. data/spec/rails_app/config/initializers/cookies_serializer.rb +3 -0
  62. data/spec/rails_app/config/initializers/devise.rb +258 -0
  63. data/spec/rails_app/config/initializers/inflections.rb +15 -0
  64. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  65. data/spec/rails_app/config/initializers/secret_token.rb +7 -0
  66. data/spec/rails_app/config/initializers/session_store.rb +8 -0
  67. data/spec/rails_app/config/initializers/wrap_parameters.rb +14 -0
  68. data/spec/rails_app/config/locales/devise.en.yml +59 -0
  69. data/spec/rails_app/config/locales/en.yml +5 -0
  70. data/spec/rails_app/config/routes.rb +65 -0
  71. data/spec/rails_app/config.ru +4 -0
  72. data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +42 -0
  73. data/spec/rails_app/db/migrate/20140407172619_devise_xfactor_authentication_add_to_users.rb +15 -0
  74. data/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +7 -0
  75. data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +9 -0
  76. data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +19 -0
  77. data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +5 -0
  78. data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
  79. data/spec/rails_app/db/schema.rb +55 -0
  80. data/spec/rails_app/lib/assets/.gitkeep +0 -0
  81. data/spec/rails_app/lib/sms_provider.rb +17 -0
  82. data/spec/rails_app/public/404.html +26 -0
  83. data/spec/rails_app/public/422.html +26 -0
  84. data/spec/rails_app/public/500.html +25 -0
  85. data/spec/rails_app/public/favicon.ico +0 -0
  86. data/spec/rails_app/script/rails +6 -0
  87. data/spec/spec_helper.rb +26 -0
  88. data/spec/support/authenticated_model_helper.rb +59 -0
  89. data/spec/support/capybara.rb +3 -0
  90. data/spec/support/controller_helper.rb +16 -0
  91. data/spec/support/features_spec_helper.rb +42 -0
  92. data/spec/support/sms_provider.rb +5 -0
  93. data/spec/support/totp_helper.rb +11 -0
  94. metadata +293 -0
@@ -0,0 +1,203 @@
1
+ require 'devise_xfactor_authentication/hooks/devise_xfactor_authenticatable'
2
+ require 'rotp'
3
+ require 'encryptor'
4
+
5
+ module Devise
6
+ module Models
7
+ module DeviseXfactorAuthenticatable
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ def has_one_time_password(options = {})
12
+ include InstanceMethodsOnActivation
13
+ include EncryptionInstanceMethods if options[:encrypted] == true
14
+ end
15
+
16
+ ::Devise::Models.config(
17
+ self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length,
18
+ :remember_otp_session_for_seconds, :otp_secret_encryption_key,
19
+ :direct_otp_length, :direct_otp_valid_for, :totp_timestamp, :delete_cookie_on_logout
20
+ )
21
+ end
22
+
23
+ module InstanceMethodsOnActivation
24
+ def authenticate_otp(code, options = {})
25
+ return true if direct_otp && authenticate_direct_otp(code)
26
+ return true if totp_enabled? && authenticate_totp(code, options)
27
+ false
28
+ end
29
+
30
+ def authenticate_direct_otp(code)
31
+ return false if direct_otp.nil? || direct_otp != code || direct_otp_expired?
32
+ clear_direct_otp
33
+ true
34
+ end
35
+
36
+ def authenticate_totp(code, options = {})
37
+ totp_secret = options[:otp_secret_key] || otp_secret_key
38
+ digits = options[:otp_length] || self.class.otp_length
39
+ drift = options[:drift] || self.class.allowed_otp_drift_seconds
40
+ raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
41
+ totp = ROTP::TOTP.new(totp_secret, digits: digits)
42
+ new_timestamp = totp.verify(
43
+ without_spaces(code),
44
+ drift_ahead: drift, drift_behind: drift, after: totp_timestamp
45
+ )
46
+ return false unless new_timestamp
47
+ self.totp_timestamp = new_timestamp
48
+ true
49
+ end
50
+
51
+ def provisioning_uri(account = nil, options = {})
52
+ totp_secret = options[:otp_secret_key] || otp_secret_key
53
+ options[:digits] ||= options[:otp_length] || self.class.otp_length
54
+ raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil?
55
+ account ||= email if respond_to?(:email)
56
+ ROTP::TOTP.new(totp_secret, options).provisioning_uri(account)
57
+ end
58
+
59
+ def need_devise_xfactor_authentication?(request)
60
+ true
61
+ end
62
+
63
+ def send_new_otp(options = {})
64
+ create_direct_otp options
65
+ send_devise_xfactor_authentication_code(direct_otp)
66
+ end
67
+
68
+ def send_new_otp_after_login?
69
+ !totp_enabled?
70
+ end
71
+
72
+ def send_devise_xfactor_authentication_code(code)
73
+ raise NotImplementedError.new("No default implementation - please define in your class.")
74
+ end
75
+
76
+ def max_login_attempts?
77
+ second_factor_attempts_count.to_i >= max_login_attempts.to_i
78
+ end
79
+
80
+ def max_login_attempts
81
+ self.class.max_login_attempts
82
+ end
83
+
84
+ def totp_enabled?
85
+ respond_to?(:otp_secret_key) && !otp_secret_key.nil?
86
+ end
87
+
88
+ def confirm_totp_secret(secret, code, options = {})
89
+ return false unless authenticate_totp(code, {otp_secret_key: secret})
90
+ self.otp_secret_key = secret
91
+ true
92
+ end
93
+
94
+ def generate_totp_secret
95
+ ROTP::Base32.random_base32
96
+ end
97
+
98
+ def create_direct_otp(options = {})
99
+ # Create a new random OTP and store it in the database
100
+ digits = options[:length] || self.class.direct_otp_length || 6
101
+ update(
102
+ direct_otp: random_base10(digits),
103
+ direct_otp_sent_at: Time.now.utc
104
+ )
105
+ end
106
+
107
+ private
108
+
109
+ def without_spaces(code)
110
+ code.gsub(/\s/, '')
111
+ end
112
+
113
+ def random_base10(digits)
114
+ SecureRandom.random_number(10**digits).to_s.rjust(digits, '0')
115
+ end
116
+
117
+ def direct_otp_expired?
118
+ Time.now.utc > direct_otp_sent_at + self.class.direct_otp_valid_for
119
+ end
120
+
121
+ def clear_direct_otp
122
+ update(direct_otp: nil, direct_otp_sent_at: nil)
123
+ end
124
+ end
125
+
126
+ module EncryptionInstanceMethods
127
+ def otp_secret_key
128
+ otp_decrypt(encrypted_otp_secret_key)
129
+ end
130
+
131
+ def otp_secret_key=(value)
132
+ self.encrypted_otp_secret_key = otp_encrypt(value)
133
+ end
134
+
135
+ private
136
+
137
+ def otp_decrypt(encrypted_value)
138
+ return encrypted_value if encrypted_value.blank?
139
+
140
+ encrypted_value = encrypted_value.unpack('m').first
141
+
142
+ value = ::Encryptor.decrypt(encryption_options_for(encrypted_value))
143
+
144
+ if defined?(Encoding)
145
+ encoding = Encoding.default_internal || Encoding.default_external
146
+ value = value.force_encoding(encoding.name)
147
+ end
148
+
149
+ value
150
+ end
151
+
152
+ def otp_encrypt(value)
153
+ return value if value.blank?
154
+
155
+ value = value.to_s
156
+ encrypted_value = ::Encryptor.encrypt(encryption_options_for(value))
157
+
158
+ encrypted_value = [encrypted_value].pack('m')
159
+
160
+ encrypted_value
161
+ end
162
+
163
+ def encryption_options_for(value)
164
+ {
165
+ value: value,
166
+ key: Devise.otp_secret_encryption_key,
167
+ iv: iv_for_attribute,
168
+ salt: salt_for_attribute,
169
+ algorithm: 'aes-256-cbc'
170
+ }
171
+ end
172
+
173
+ def iv_for_attribute(algorithm = 'aes-256-cbc')
174
+ iv = encrypted_otp_secret_key_iv
175
+
176
+ if iv.nil?
177
+ algo = OpenSSL::Cipher.new(algorithm)
178
+ iv = [algo.random_iv].pack('m')
179
+ self.encrypted_otp_secret_key_iv = iv
180
+ end
181
+
182
+ iv.unpack('m').first if iv.present?
183
+ end
184
+
185
+ def salt_for_attribute
186
+ salt = encrypted_otp_secret_key_salt ||
187
+ self.encrypted_otp_secret_key_salt = generate_random_base64_encoded_salt
188
+
189
+ decode_salt_if_encoded(salt)
190
+ end
191
+
192
+ def generate_random_base64_encoded_salt
193
+ prefix = '_'
194
+ prefix + [SecureRandom.random_bytes].pack('m')
195
+ end
196
+
197
+ def decode_salt_if_encoded(salt)
198
+ salt.slice(0).eql?('_') ? salt.slice(1..-1).unpack('m').first : salt
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,14 @@
1
+ require "active_record"
2
+
3
+ module DeviseXfactorAuthentication
4
+ module Orm
5
+ module ActiveRecord
6
+ module Schema
7
+ include DeviseXfactorAuthentication::Schema
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ ActiveRecord::ConnectionAdapters::Table.send :include, DeviseXfactorAuthentication::Orm::ActiveRecord::Schema
14
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseXfactorAuthentication::Orm::ActiveRecord::Schema
@@ -0,0 +1,7 @@
1
+ module DeviseXfactorAuthentication
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include DeviseXfactorAuthentication::Controllers::Helpers
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+ protected
4
+
5
+ def devise_devise_xfactor_authentication(mapping, controllers)
6
+ resource :devise_xfactor_authentication, :only => [:show, :update, :resend_code], :path => mapping.path_names[:devise_xfactor_authentication], :controller => controllers[:devise_xfactor_authentication] do
7
+ collection { get "resend_code" }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module DeviseXfactorAuthentication
2
+ module Schema
3
+ def second_factor_attempts_count
4
+ apply_devise_schema :second_factor_attempts_count, Integer, :default => 0
5
+ end
6
+
7
+ def encrypted_otp_secret_key
8
+ apply_devise_schema :encrypted_otp_secret_key, String
9
+ end
10
+
11
+ def encrypted_otp_secret_key_iv
12
+ apply_devise_schema :encrypted_otp_secret_key_iv, String
13
+ end
14
+
15
+ def encrypted_otp_secret_key_salt
16
+ apply_devise_schema :encrypted_otp_secret_key_salt, String
17
+ end
18
+
19
+ def direct_otp
20
+ apply_devise_schema :direct_otp, String
21
+ end
22
+
23
+ def direct_otp_sent_at
24
+ apply_devise_schema :direct_otp_sent_at, DateTime
25
+ end
26
+
27
+ def totp_timestamp
28
+ apply_devise_schema :totp_timestamp, Timestamp
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module DeviseXfactorAuthentication
2
+ VERSION = "2.2.18".freeze
3
+ end
@@ -0,0 +1,52 @@
1
+ require 'devise_xfactor_authentication/version'
2
+ require 'devise'
3
+ require 'active_support/concern'
4
+ require "active_model"
5
+ require "active_support/core_ext/class/attribute_accessors"
6
+ require "cgi"
7
+
8
+ module Devise
9
+ mattr_accessor :max_login_attempts
10
+ @@max_login_attempts = 3
11
+
12
+ mattr_accessor :allowed_otp_drift_seconds
13
+ @@allowed_otp_drift_seconds = 30
14
+
15
+ mattr_accessor :otp_length
16
+ @@otp_length = 6
17
+
18
+ mattr_accessor :direct_otp_length
19
+ @@direct_otp_length = 6
20
+
21
+ mattr_accessor :direct_otp_valid_for
22
+ @@direct_otp_valid_for = 5.minutes
23
+
24
+ mattr_accessor :remember_otp_session_for_seconds
25
+ @@remember_otp_session_for_seconds = 0
26
+
27
+ mattr_accessor :otp_secret_encryption_key
28
+ @@otp_secret_encryption_key = ''
29
+
30
+ mattr_accessor :second_factor_resource_id
31
+ @@second_factor_resource_id = 'id'
32
+
33
+ mattr_accessor :delete_cookie_on_logout
34
+ @@delete_cookie_on_logout = false
35
+ end
36
+
37
+ module DeviseXfactorAuthentication
38
+ NEED_AUTHENTICATION = 'need_devise_xfactor_authentication'
39
+ REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
40
+
41
+ autoload :Schema, 'devise_xfactor_authentication/schema'
42
+ module Controllers
43
+ autoload :Helpers, 'devise_xfactor_authentication/controllers/helpers'
44
+ end
45
+ end
46
+
47
+ Devise.add_module :devise_xfactor_authenticatable, :model => 'devise_xfactor_authentication/models/devise_xfactor_authenticatable', :controller => :devise_xfactor_authentication, :route => :devise_xfactor_authentication
48
+
49
+ require 'devise_xfactor_authentication/orm/active_record' if defined?(ActiveRecord::Base)
50
+ require 'devise_xfactor_authentication/routes'
51
+ require 'devise_xfactor_authentication/models/devise_xfactor_authenticatable'
52
+ require 'devise_xfactor_authentication/rails'
@@ -0,0 +1,14 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ActiveRecord
4
+ module Generators
5
+ class DeviseXfactorAuthenticationGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+ def copy_devise_xfactor_authentication_migration
9
+ migration_template "migration.rb", "db/migrate/devise_xfactor_authentication_add_to_#{table_name}.rb"
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ class DeviseXfactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :<%= table_name %>, :second_factor_attempts_count, :integer, default: 0
4
+ add_column :<%= table_name %>, :encrypted_otp_secret_key, :string
5
+ add_column :<%= table_name %>, :encrypted_otp_secret_key_iv, :string
6
+ add_column :<%= table_name %>, :encrypted_otp_secret_key_salt, :string
7
+ add_column :<%= table_name %>, :direct_otp, :string
8
+ add_column :<%= table_name %>, :direct_otp_sent_at, :datetime
9
+ add_column :<%= table_name %>, :totp_timestamp, :timestamp
10
+ add_column :<%= table_name %>, :otp_secret_key, :string
11
+
12
+ add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module DeviseXfactorAuthenticatable
2
+ module Generators
3
+ class DeviseXfactorAuthenticationGenerator < Rails::Generators::NamedBase
4
+ namespace "devise_xfactor_authentication"
5
+
6
+ desc "Adds :devise_xfactor_authenticable directive in the given model. It also generates an active record migration."
7
+
8
+ def inject_devise_xfactor_authentication_content
9
+ path = File.join("app", "models", "#{file_path}.rb")
10
+ inject_into_file(path, "devise_xfactor_authenticatable, :", :after => "devise :") if File.exists?(path)
11
+ end
12
+
13
+ hook_for :orm
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Devise::DeviseXfactorAuthenticationController, type: :controller do
4
+ describe 'is_fully_authenticated? helper' do
5
+ def post_code(code)
6
+ if Rails::VERSION::MAJOR >= 5
7
+ post :update, params: { code: code }
8
+ else
9
+ post :update, code: code
10
+ end
11
+ end
12
+
13
+ before do
14
+ sign_in
15
+ end
16
+
17
+ context 'after user enters valid OTP code' do
18
+ it 'returns true' do
19
+ controller.current_user.send_new_otp
20
+ post_code controller.current_user.direct_otp
21
+ expect(subject.is_fully_authenticated?).to eq true
22
+ end
23
+ end
24
+
25
+ context 'when user has not entered any OTP yet' do
26
+ it 'returns false' do
27
+ get :show
28
+
29
+ expect(subject.is_fully_authenticated?).to eq false
30
+ end
31
+ end
32
+
33
+ context 'when user enters an invalid OTP' do
34
+ it 'returns false' do
35
+ post_code '12345'
36
+
37
+ expect(subject.is_fully_authenticated?).to eq false
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,237 @@
1
+ require 'spec_helper'
2
+ include AuthenticatedModelHelper
3
+
4
+ feature "User of two factor authentication" do
5
+ context 'sending two factor authentication code via SMS' do
6
+ shared_examples 'sends and authenticates code' do |user, type|
7
+ before do
8
+ user.reload
9
+ if type == 'encrypted'
10
+ allow(User).to receive(:has_one_time_password).with(encrypted: true)
11
+ end
12
+ end
13
+
14
+ it 'does not send an SMS before the user has signed in' do
15
+ expect(SMSProvider.messages).to be_empty
16
+ end
17
+
18
+ it 'sends code via SMS after sign in' do
19
+ visit new_user_session_path
20
+ complete_sign_in_form_for(user)
21
+
22
+ expect(page).to have_content 'Enter the code that was sent to you'
23
+
24
+ expect(SMSProvider.messages.size).to eq(1)
25
+ message = SMSProvider.last_message
26
+ expect(message.to).to eq(user.phone_number)
27
+ expect(message.body).to eq(user.reload.direct_otp)
28
+ end
29
+
30
+ it 'authenticates a valid OTP code' do
31
+ visit new_user_session_path
32
+ complete_sign_in_form_for(user)
33
+
34
+ expect(page).to have_content('You are signed in as Marissa')
35
+
36
+ fill_in 'code', with: SMSProvider.last_message.body
37
+ click_button 'Submit'
38
+
39
+ within('.flash.notice') do
40
+ expect(page).to have_content('Two factor authentication successful.')
41
+ end
42
+
43
+ expect(current_path).to eq root_path
44
+ end
45
+ end
46
+
47
+ it_behaves_like 'sends and authenticates code', create_user('not_encrypted')
48
+ it_behaves_like 'sends and authenticates code', create_user, 'encrypted'
49
+ end
50
+
51
+ scenario "must be logged in" do
52
+ visit user_devise_xfactor_authentication_path
53
+
54
+ expect(page).to have_content("Welcome Home")
55
+ expect(page).to have_content("You are signed out")
56
+ end
57
+
58
+ context "when logged in" do
59
+ let(:user) { create_user }
60
+
61
+ background do
62
+ login_as user
63
+ end
64
+
65
+ scenario "is redirected to TFA when path requires authentication" do
66
+ visit dashboard_path + "?A=param%20a&B=param%20b"
67
+
68
+ expect(page).to_not have_content("Your Personal Dashboard")
69
+
70
+ fill_in "code", with: SMSProvider.last_message.body
71
+ click_button "Submit"
72
+
73
+ expect(page).to have_content("Your Personal Dashboard")
74
+ expect(page).to have_content("You are signed in as Marissa")
75
+ expect(page).to have_content("Param A is param a")
76
+ expect(page).to have_content("Param B is param b")
77
+ end
78
+
79
+ scenario "is locked out after max failed attempts" do
80
+ visit user_devise_xfactor_authentication_path
81
+
82
+ max_attempts = User.max_login_attempts
83
+
84
+ max_attempts.times do
85
+ fill_in "code", with: "incorrect#{rand(100)}"
86
+ click_button "Submit"
87
+
88
+ within(".flash.alert") do
89
+ expect(page).to have_content("Attempt failed")
90
+ end
91
+ end
92
+
93
+ expect(page).to have_content("Access completely denied")
94
+ expect(page).to have_content("You are signed out")
95
+ end
96
+
97
+ scenario "cannot retry authentication after max attempts" do
98
+ user.update(:second_factor_attempts_count, User.max_login_attempts)
99
+
100
+ visit user_devise_xfactor_authentication_path
101
+
102
+ expect(page).to have_content("Access completely denied")
103
+ expect(page).to have_content("You are signed out")
104
+ end
105
+
106
+ describe "rememberable TFA" do
107
+ before do
108
+ @original_remember_otp_session_for_seconds = User.remember_otp_session_for_seconds
109
+ User.remember_otp_session_for_seconds = 30.days
110
+ end
111
+
112
+ after do
113
+ User.remember_otp_session_for_seconds = @original_remember_otp_session_for_seconds
114
+ end
115
+
116
+ scenario "doesn't require TFA code again within 30 days" do
117
+ sms_sign_in
118
+
119
+ logout
120
+
121
+ login_as user
122
+ visit dashboard_path
123
+ expect(page).to have_content("Your Personal Dashboard")
124
+ expect(page).to have_content("You are signed in as Marissa")
125
+ end
126
+
127
+ scenario "requires TFA code again after 30 days" do
128
+ sms_sign_in
129
+
130
+ logout
131
+
132
+ Timecop.travel(30.days.from_now)
133
+ login_as user
134
+ visit dashboard_path
135
+ expect(page).to have_content("You are signed in as Marissa")
136
+ expect(page).to have_content("Enter the code that was sent to you")
137
+ end
138
+
139
+ scenario 'TFA should be different for different users' do
140
+ sms_sign_in
141
+
142
+ tfa_cookie1 = get_tfa_cookie()
143
+
144
+ logout
145
+ reset_session!
146
+
147
+ user2 = create_user()
148
+ login_as(user2)
149
+ sms_sign_in
150
+
151
+ tfa_cookie2 = get_tfa_cookie()
152
+
153
+ expect(tfa_cookie1).not_to eq tfa_cookie2
154
+ end
155
+
156
+ def sms_sign_in
157
+ SMSProvider.messages.clear()
158
+ visit user_devise_xfactor_authentication_path
159
+ fill_in 'code', with: SMSProvider.last_message.body
160
+ click_button 'Submit'
161
+ end
162
+
163
+ scenario 'TFA should be unique for specific user' do
164
+ sms_sign_in
165
+
166
+ tfa_cookie1 = get_tfa_cookie()
167
+
168
+ logout
169
+ reset_session!
170
+
171
+ user2 = create_user()
172
+ set_tfa_cookie(tfa_cookie1)
173
+ login_as(user2)
174
+ visit dashboard_path
175
+ expect(page).to have_content("Enter the code that was sent to you")
176
+ end
177
+
178
+ scenario 'Delete cookie when user logs out if enabled' do
179
+ user.class.delete_cookie_on_logout = true
180
+
181
+ login_as user
182
+ logout
183
+
184
+ login_as user
185
+
186
+ visit dashboard_path
187
+ expect(page).to have_content("Enter the code that was sent to you")
188
+ end
189
+ end
190
+
191
+ it 'sets the warden session need_devise_xfactor_authentication key to true' do
192
+ session_hash = { 'need_devise_xfactor_authentication' => true }
193
+
194
+ expect(page.get_rack_session_key('warden.user.user.session')).to eq session_hash
195
+ end
196
+ end
197
+
198
+ describe 'signing in' do
199
+ let(:user) { create_user }
200
+ let(:admin) { create_admin }
201
+
202
+ scenario 'user signs is' do
203
+ visit new_user_session_path
204
+ complete_sign_in_form_for(user)
205
+
206
+ expect(page).to have_content('Signed in successfully.')
207
+ end
208
+
209
+ scenario 'admin signs in' do
210
+ visit new_admin_session_path
211
+ complete_sign_in_form_for(admin)
212
+
213
+ expect(page).to have_content('Signed in successfully.')
214
+ end
215
+ end
216
+
217
+ describe 'signing out' do
218
+ let(:user) { create_user }
219
+ let(:admin) { create_admin }
220
+
221
+ scenario 'user signs out' do
222
+ visit new_user_session_path
223
+ complete_sign_in_form_for(user)
224
+ visit destroy_user_session_path
225
+
226
+ expect(page).to have_content('Signed out successfully.')
227
+ end
228
+
229
+ scenario 'admin signs out' do
230
+ visit new_admin_session_path
231
+ complete_sign_in_form_for(admin)
232
+ visit destroy_admin_session_path
233
+
234
+ expect(page).to have_content('Signed out successfully.')
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ require 'generators/active_record/devise_xfactor_authentication_generator'
4
+
5
+ describe ActiveRecord::Generators::DeviseXfactorAuthenticationGenerator, type: :generator do
6
+ destination File.expand_path('../../../../../tmp', __FILE__)
7
+
8
+ before do
9
+ prepare_destination
10
+ end
11
+
12
+ it 'runs all methods in the generator' do
13
+ gen = generator %w(users)
14
+ expect(gen).to receive(:copy_devise_xfactor_authentication_migration)
15
+ gen.invoke_all
16
+ end
17
+
18
+ describe 'the generated files' do
19
+ before do
20
+ run_generator %w(users)
21
+ end
22
+
23
+ describe 'the migration' do
24
+ subject { migration_file('db/migrate/devise_xfactor_authentication_add_to_users.rb') }
25
+
26
+ it { is_expected.to exist }
27
+ it { is_expected.to be_a_migration }
28
+ it { is_expected.to contain /def change/ }
29
+ it { is_expected.to contain /add_column :users, :second_factor_attempts_count, :integer, default: 0/ }
30
+ it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key, :string/ }
31
+ it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_iv, :string/ }
32
+ it { is_expected.to contain /add_column :users, :encrypted_otp_secret_key_salt, :string/ }
33
+ it { is_expected.to contain /add_index :users, :encrypted_otp_secret_key, unique: true/ }
34
+ end
35
+ end
36
+ end