two_factor_authentication 1.1.3 → 2.2.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +21 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +295 -0
  5. data/.travis.yml +14 -7
  6. data/CHANGELOG.md +119 -0
  7. data/Gemfile +12 -3
  8. data/README.md +320 -58
  9. data/app/controllers/devise/two_factor_authentication_controller.rb +65 -25
  10. data/app/views/devise/two_factor_authentication/show.html.erb +11 -2
  11. data/config/locales/en.yml +1 -0
  12. data/config/locales/es.yml +8 -0
  13. data/config/locales/fr.yml +8 -0
  14. data/config/locales/ru.yml +1 -0
  15. data/lib/generators/active_record/templates/migration.rb +9 -11
  16. data/lib/two_factor_authentication/controllers/helpers.rb +3 -3
  17. data/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +12 -2
  18. data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +158 -29
  19. data/lib/two_factor_authentication/orm/active_record.rb +2 -0
  20. data/lib/two_factor_authentication/routes.rb +3 -1
  21. data/lib/two_factor_authentication/schema.rb +24 -4
  22. data/lib/two_factor_authentication/version.rb +1 -1
  23. data/lib/two_factor_authentication.rb +20 -3
  24. data/spec/controllers/two_factor_authentication_controller_spec.rb +41 -0
  25. data/spec/features/two_factor_authenticatable_spec.rb +179 -30
  26. data/spec/generators/active_record/two_factor_authentication_generator_spec.rb +36 -0
  27. data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +272 -114
  28. data/spec/rails_app/app/controllers/home_controller.rb +1 -1
  29. data/spec/rails_app/app/models/admin.rb +6 -0
  30. data/spec/rails_app/app/models/encrypted_user.rb +15 -0
  31. data/spec/rails_app/app/models/guest_user.rb +8 -1
  32. data/spec/rails_app/app/models/user.rb +3 -4
  33. data/spec/rails_app/config/environments/test.rb +10 -1
  34. data/spec/rails_app/config/initializers/devise.rb +5 -3
  35. data/spec/rails_app/config/routes.rb +1 -0
  36. data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +2 -2
  37. data/spec/rails_app/db/migrate/20140407172619_two_factor_authentication_add_to_users.rb +1 -1
  38. data/spec/rails_app/db/migrate/20140407215513_add_nickanme_to_users.rb +1 -1
  39. data/spec/rails_app/db/migrate/20151224171231_add_encrypted_columns_to_user.rb +9 -0
  40. data/spec/rails_app/db/migrate/20151224180310_populate_otp_column.rb +19 -0
  41. data/spec/rails_app/db/migrate/20151228230340_remove_otp_secret_key_from_user.rb +5 -0
  42. data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
  43. data/spec/rails_app/db/schema.rb +35 -18
  44. data/spec/spec_helper.rb +4 -0
  45. data/spec/support/authenticated_model_helper.rb +33 -2
  46. data/spec/support/controller_helper.rb +16 -0
  47. data/spec/support/features_spec_helper.rb +24 -1
  48. data/spec/support/totp_helper.rb +11 -0
  49. data/two_factor_authentication.gemspec +4 -2
  50. metadata +133 -30
  51. data/spec/controllers/two_factor_auth_spec.rb +0 -18
@@ -1,7 +1,17 @@
1
1
  Warden::Manager.after_authentication do |user, auth, options|
2
- if user.respond_to?(:need_two_factor_authentication?)
2
+ if auth.env["action_dispatch.cookies"]
3
+ expected_cookie_value = "#{user.class}-#{user.public_send(Devise.second_factor_resource_id)}"
4
+ actual_cookie_value = auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
5
+ bypass_by_cookie = actual_cookie_value == expected_cookie_value
6
+ end
7
+
8
+ if user.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie
3
9
  if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
4
- user.send_two_factor_authentication_code
10
+ user.send_new_otp if user.send_new_otp_after_login?
5
11
  end
6
12
  end
7
13
  end
14
+
15
+ Warden::Manager.before_logout do |user, auth, _options|
16
+ auth.cookies.delete TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME if Devise.delete_cookie_on_logout
17
+ end
@@ -1,4 +1,7 @@
1
1
  require 'two_factor_authentication/hooks/two_factor_authenticatable'
2
+ require 'rotp'
3
+ require 'encryptor'
4
+
2
5
  module Devise
3
6
  module Models
4
7
  module TwoFactorAuthenticatable
@@ -6,53 +9,67 @@ module Devise
6
9
 
7
10
  module ClassMethods
8
11
  def has_one_time_password(options = {})
9
-
10
- cattr_accessor :otp_column_name
11
- self.otp_column_name = "otp_secret_key"
12
-
13
12
  include InstanceMethodsOnActivation
14
-
15
- before_create { populate_otp_column }
16
-
17
- if respond_to?(:attributes_protected_by_default)
18
- def self.attributes_protected_by_default #:nodoc:
19
- super + [self.otp_column_name]
20
- end
21
- end
13
+ include EncryptionInstanceMethods if options[:encrypted] == true
22
14
  end
23
- ::Devise::Models.config(self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length)
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
+ )
24
21
  end
25
22
 
26
23
  module InstanceMethodsOnActivation
27
24
  def authenticate_otp(code, options = {})
28
- totp = ROTP::TOTP.new(self.otp_column, { digits: options[:otp_length] || self.class.otp_length })
29
- drift = options[:drift] || self.class.allowed_otp_drift_seconds
25
+ return true if direct_otp && authenticate_direct_otp(code)
26
+ return true if totp_enabled? && authenticate_totp(code, options)
27
+ false
28
+ end
30
29
 
31
- totp.verify_with_drift(code, drift)
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
32
34
  end
33
35
 
34
- def otp_code(time = Time.now, options = {})
35
- ROTP::TOTP.new(self.otp_column, { digits: options[:otp_length] || self.class.otp_length }).at(time, true)
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
36
49
  end
37
50
 
38
51
  def provisioning_uri(account = nil, options = {})
39
- account ||= self.email if self.respond_to?(:email)
40
- ROTP::TOTP.new(self.otp_column, options).provisioning_uri(account)
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)
41
57
  end
42
58
 
43
- def otp_column
44
- self.send(self.class.otp_column_name)
59
+ def need_two_factor_authentication?(request)
60
+ true
45
61
  end
46
62
 
47
- def otp_column=(attr)
48
- self.send("#{self.class.otp_column_name}=", attr)
63
+ def send_new_otp(options = {})
64
+ create_direct_otp options
65
+ send_two_factor_authentication_code(direct_otp)
49
66
  end
50
67
 
51
- def need_two_factor_authentication?(request)
52
- true
68
+ def send_new_otp_after_login?
69
+ !totp_enabled?
53
70
  end
54
71
 
55
- def send_two_factor_authentication_code
72
+ def send_two_factor_authentication_code(code)
56
73
  raise NotImplementedError.new("No default implementation - please define in your class.")
57
74
  end
58
75
 
@@ -64,10 +81,122 @@ module Devise
64
81
  self.class.max_login_attempts
65
82
  end
66
83
 
67
- def populate_otp_column
68
- self.otp_column = ROTP::Base32.random_base32
84
+ def totp_enabled?
85
+ respond_to?(:otp_secret_key) && !otp_secret_key.nil?
69
86
  end
70
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_attributes(
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_attributes(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
71
200
  end
72
201
  end
73
202
  end
@@ -1,3 +1,5 @@
1
+ require "active_record"
2
+
1
3
  module TwoFactorAuthentication
2
4
  module Orm
3
5
  module ActiveRecord
@@ -3,7 +3,9 @@ module ActionDispatch::Routing
3
3
  protected
4
4
 
5
5
  def devise_two_factor_authentication(mapping, controllers)
6
- resource :two_factor_authentication, :only => [:show, :update], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication]
6
+ resource :two_factor_authentication, :only => [:show, :update, :resend_code], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] do
7
+ collection { get "resend_code" }
8
+ end
7
9
  end
8
10
  end
9
11
  end
@@ -1,11 +1,31 @@
1
1
  module TwoFactorAuthentication
2
2
  module Schema
3
- def otp_secret_key
4
- apply_devise_schema :otp_secret_key, String
5
- end
6
-
7
3
  def second_factor_attempts_count
8
4
  apply_devise_schema :second_factor_attempts_count, Integer, :default => 0
9
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
10
30
  end
11
31
  end
@@ -1,3 +1,3 @@
1
1
  module TwoFactorAuthentication
2
- VERSION = "1.1.3".freeze
2
+ VERSION = "2.2.0".freeze
3
3
  end
@@ -2,10 +2,8 @@ require 'two_factor_authentication/version'
2
2
  require 'devise'
3
3
  require 'active_support/concern'
4
4
  require "active_model"
5
- require "active_record"
6
5
  require "active_support/core_ext/class/attribute_accessors"
7
6
  require "cgi"
8
- require "rotp"
9
7
 
10
8
  module Devise
11
9
  mattr_accessor :max_login_attempts
@@ -16,10 +14,29 @@ module Devise
16
14
 
17
15
  mattr_accessor :otp_length
18
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
19
35
  end
20
36
 
21
37
  module TwoFactorAuthentication
22
38
  NEED_AUTHENTICATION = 'need_two_factor_authentication'
39
+ REMEMBER_TFA_COOKIE_NAME = "remember_tfa"
23
40
 
24
41
  autoload :Schema, 'two_factor_authentication/schema'
25
42
  module Controllers
@@ -29,7 +46,7 @@ end
29
46
 
30
47
  Devise.add_module :two_factor_authenticatable, :model => 'two_factor_authentication/models/two_factor_authenticatable', :controller => :two_factor_authentication, :route => :two_factor_authentication
31
48
 
32
- require 'two_factor_authentication/orm/active_record'
49
+ require 'two_factor_authentication/orm/active_record' if defined?(ActiveRecord::Base)
33
50
  require 'two_factor_authentication/routes'
34
51
  require 'two_factor_authentication/models/two_factor_authenticatable'
35
52
  require 'two_factor_authentication/rails'
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Devise::TwoFactorAuthenticationController, 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
@@ -1,47 +1,65 @@
1
1
  require 'spec_helper'
2
+ include AuthenticatedModelHelper
2
3
 
3
4
  feature "User of two factor authentication" do
4
- let(:user) { create_user }
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
5
13
 
6
- scenario "must be logged in" do
7
- visit user_two_factor_authentication_path
14
+ it 'does not send an SMS before the user has signed in' do
15
+ expect(SMSProvider.messages).to be_empty
16
+ end
8
17
 
9
- expect(page).to have_content("Welcome Home")
10
- expect(page).to have_content("You are signed out")
11
- end
18
+ it 'sends code via SMS after sign in' do
19
+ visit new_user_session_path
20
+ complete_sign_in_form_for(user)
12
21
 
13
- scenario "sends two factor authentication code after sign in" do
14
- expect(SMSProvider.messages).to be_empty
22
+ expect(page).to have_content 'Enter the code that was sent to you'
15
23
 
16
- visit new_user_session_path
17
- complete_sign_in_form_for(user)
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
18
29
 
19
- expect(page).to have_content "Enter your personal code"
30
+ it 'authenticates a valid OTP code' do
31
+ visit new_user_session_path
32
+ complete_sign_in_form_for(user)
20
33
 
21
- expect(SMSProvider.messages.size).to eq(1)
22
- message = SMSProvider.last_message
23
- expect(message.to).to eq(user.phone_number)
24
- expect(message.body).to eq(user.otp_code)
25
- end
34
+ expect(page).to have_content('You are signed in as Marissa')
26
35
 
27
- context "when logged in" do
36
+ fill_in 'code', with: SMSProvider.last_message.body
37
+ click_button 'Submit'
28
38
 
29
- background do
30
- login_as user
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
31
45
  end
32
46
 
33
- scenario "can fill in TFA code" do
34
- visit user_two_factor_authentication_path
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
35
50
 
36
- expect(page).to have_content("You are signed in as Marissa")
37
- expect(page).to have_content("Enter your personal code")
51
+ scenario "must be logged in" do
52
+ visit user_two_factor_authentication_path
38
53
 
39
- fill_in "code", with: user.otp_code
40
- click_button "Submit"
54
+ expect(page).to have_content("Welcome Home")
55
+ expect(page).to have_content("You are signed out")
56
+ end
41
57
 
42
- within(".flash.notice") do
43
- expect(page).to have_content("Two factor authentication successful.")
44
- end
58
+ context "when logged in" do
59
+ let(:user) { create_user }
60
+
61
+ background do
62
+ login_as user
45
63
  end
46
64
 
47
65
  scenario "is redirected to TFA when path requires authentication" do
@@ -49,7 +67,7 @@ feature "User of two factor authentication" do
49
67
 
50
68
  expect(page).to_not have_content("Your Personal Dashboard")
51
69
 
52
- fill_in "code", with: user.otp_code
70
+ fill_in "code", with: SMSProvider.last_message.body
53
71
  click_button "Submit"
54
72
 
55
73
  expect(page).to have_content("Your Personal Dashboard")
@@ -67,7 +85,7 @@ feature "User of two factor authentication" do
67
85
  fill_in "code", with: "incorrect#{rand(100)}"
68
86
  click_button "Submit"
69
87
 
70
- within(".flash.error") do
88
+ within(".flash.alert") do
71
89
  expect(page).to have_content("Attempt failed")
72
90
  end
73
91
  end
@@ -84,5 +102,136 @@ feature "User of two factor authentication" do
84
102
  expect(page).to have_content("Access completely denied")
85
103
  expect(page).to have_content("You are signed out")
86
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_two_factor_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_two_factor_authentication key to true' do
192
+ session_hash = { 'need_two_factor_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
87
236
  end
88
237
  end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ require 'generators/active_record/two_factor_authentication_generator'
4
+
5
+ describe ActiveRecord::Generators::TwoFactorAuthenticationGenerator, 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_two_factor_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/two_factor_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