two_factor_authentication 1.1.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +21 -0
  3. data/.rubocop.yml +295 -0
  4. data/.travis.yml +4 -5
  5. data/CHANGELOG.md +24 -14
  6. data/README.md +57 -65
  7. data/app/controllers/devise/two_factor_authentication_controller.rb +28 -12
  8. data/app/views/devise/two_factor_authentication/show.html.erb +10 -1
  9. data/config/locales/en.yml +1 -0
  10. data/config/locales/es.yml +8 -0
  11. data/config/locales/fr.yml +1 -0
  12. data/config/locales/ru.yml +1 -0
  13. data/lib/generators/active_record/templates/migration.rb +3 -0
  14. data/lib/two_factor_authentication.rb +9 -0
  15. data/lib/two_factor_authentication/controllers/helpers.rb +1 -1
  16. data/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +4 -23
  17. data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +68 -19
  18. data/lib/two_factor_authentication/routes.rb +3 -1
  19. data/lib/two_factor_authentication/schema.rb +12 -0
  20. data/lib/two_factor_authentication/version.rb +1 -1
  21. data/spec/controllers/two_factor_authentication_controller_spec.rb +2 -2
  22. data/spec/features/two_factor_authenticatable_spec.rb +36 -73
  23. data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +137 -80
  24. data/spec/rails_app/app/controllers/home_controller.rb +1 -1
  25. data/spec/rails_app/app/models/admin.rb +6 -0
  26. data/spec/rails_app/app/models/encrypted_user.rb +2 -1
  27. data/spec/rails_app/app/models/guest_user.rb +8 -1
  28. data/spec/rails_app/app/models/user.rb +2 -2
  29. data/spec/rails_app/config/initializers/devise.rb +2 -2
  30. data/spec/rails_app/config/routes.rb +1 -0
  31. data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +1 -1
  32. data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
  33. data/spec/rails_app/db/schema.rb +19 -1
  34. data/spec/support/authenticated_model_helper.rb +22 -15
  35. data/spec/support/controller_helper.rb +1 -1
  36. data/spec/support/totp_helper.rb +11 -0
  37. data/two_factor_authentication.gemspec +1 -1
  38. metadata +74 -7
@@ -1,6 +1,8 @@
1
+ require 'devise/version'
2
+
1
3
  class Devise::TwoFactorAuthenticationController < DeviseController
2
- prepend_before_filter :authenticate_scope!
3
- before_filter :prepare_and_validate, :handle_two_factor_authentication
4
+ prepend_before_action :authenticate_scope!
5
+ before_action :prepare_and_validate, :handle_two_factor_authentication
4
6
 
5
7
  def show
6
8
  end
@@ -15,24 +17,39 @@ class Devise::TwoFactorAuthenticationController < DeviseController
15
17
  end
16
18
  end
17
19
 
20
+ def resend_code
21
+ resource.send_new_otp
22
+ redirect_to send("#{resource_name}_two_factor_authentication_path"), notice: I18n.t('devise.two_factor_authentication.code_has_been_sent')
23
+ end
24
+
18
25
  private
19
26
 
20
27
  def after_two_factor_success_for(resource)
28
+ set_remember_two_factor_cookie(resource)
29
+
30
+ warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
31
+ # For compatability with devise versions below v4.2.0
32
+ # https://github.com/plataformatec/devise/commit/2044fffa25d781fcbaf090e7728b48b65c854ccb
33
+ if respond_to?(:bypass_sign_in)
34
+ bypass_sign_in(resource, scope: resource_name)
35
+ else
36
+ sign_in(resource_name, resource, bypass: true)
37
+ end
38
+ set_flash_message :notice, :success
39
+ resource.update_attribute(:second_factor_attempts_count, 0)
40
+
41
+ redirect_to after_two_factor_success_path_for(resource)
42
+ end
43
+
44
+ def set_remember_two_factor_cookie(resource)
21
45
  expires_seconds = resource.class.remember_otp_session_for_seconds
22
46
 
23
47
  if expires_seconds && expires_seconds > 0
24
48
  cookies.signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] = {
25
- value: "#{resource.class}-#{resource.id}",
49
+ value: "#{resource.class}-#{resource.public_send(Devise.second_factor_resource_id)}",
26
50
  expires: expires_seconds.from_now
27
51
  }
28
52
  end
29
-
30
- warden.session(resource_name)[TwoFactorAuthentication::NEED_AUTHENTICATION] = false
31
- sign_in resource_name, resource, :bypass => true
32
- set_flash_message :notice, :success
33
- resource.update_attribute(:second_factor_attempts_count, 0)
34
-
35
- redirect_to after_two_factor_success_path_for(resource)
36
53
  end
37
54
 
38
55
  def after_two_factor_success_path_for(resource)
@@ -42,12 +59,11 @@ class Devise::TwoFactorAuthenticationController < DeviseController
42
59
  def after_two_factor_fail_for(resource)
43
60
  resource.second_factor_attempts_count += 1
44
61
  resource.save
45
- flash.now[:error] = find_message(:attempt_failed)
62
+ set_flash_message :alert, :attempt_failed, now: true
46
63
 
47
64
  if resource.max_login_attempts?
48
65
  sign_out(resource)
49
66
  render :max_login_attempts_reached
50
-
51
67
  else
52
68
  render :show
53
69
  end
@@ -1,4 +1,8 @@
1
- <h2>Enter your personal code</h2>
1
+ <% if resource.direct_otp %>
2
+ <h2>Enter the code that was sent to you</h2>
3
+ <% else %>
4
+ <h2>Enter the code from your authenticator app</h2>
5
+ <% end %>
2
6
 
3
7
  <p><%= flash[:notice] %></p>
4
8
 
@@ -7,4 +11,9 @@
7
11
  <%= submit_tag "Submit" %>
8
12
  <% end %>
9
13
 
14
+ <% if resource.direct_otp %>
15
+ <%= link_to "Resend Code", resend_code_user_two_factor_authentication_path, action: :get %>
16
+ <% else %>
17
+ <%= link_to "Send me a code instead", resend_code_user_two_factor_authentication_path, action: :get %>
18
+ <% end %>
10
19
  <%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
@@ -5,3 +5,4 @@ en:
5
5
  attempt_failed: "Attempt failed."
6
6
  max_login_attempts_reached: "Access completely denied as you have reached your attempts limit"
7
7
  contact_administrator: "Please contact your system administrator."
8
+ code_has_been_sent: "Your authentication code has been sent."
@@ -0,0 +1,8 @@
1
+ es:
2
+ devise:
3
+ two_factor_authentication:
4
+ success: "Autenticación multi-factor realizada exitosamente."
5
+ attempt_failed: "La autenticación ha fallado."
6
+ max_login_attempts_reached: "Has llegado al límite de intentos fallidos, acceso denegado."
7
+ contact_administrator: "Contacte a su administrador de sistema."
8
+ code_has_been_sent: "El código de autenticación ha sido enviado."
@@ -5,3 +5,4 @@ fr:
5
5
  attempt_failed: "La connexion a échoué."
6
6
  max_login_attempts_reached: "Limite de tentatives atteinte, accès refusé."
7
7
  contact_administrator: "Merci de contacter votre administrateur système."
8
+ code_has_been_sent: "Votre code de validation envoyé."
@@ -5,3 +5,4 @@ ru:
5
5
  attempt_failed: "Неверный код."
6
6
  max_login_attempts_reached: "Доступ заблокирован. Превышено число попыток авторизации"
7
7
  contact_administrator: "Пожалуйста, свяжитесь с системным администратором."
8
+ code_has_been_sent: "Ваш персональный код был отправлен."
@@ -4,6 +4,9 @@ class TwoFactorAuthenticationAddTo<%= table_name.camelize %> < ActiveRecord::Mig
4
4
  add_column :<%= table_name %>, :encrypted_otp_secret_key, :string
5
5
  add_column :<%= table_name %>, :encrypted_otp_secret_key_iv, :string
6
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
7
10
 
8
11
  add_index :<%= table_name %>, :encrypted_otp_secret_key, unique: true
9
12
  end
@@ -16,11 +16,20 @@ module Devise
16
16
  mattr_accessor :otp_length
17
17
  @@otp_length = 6
18
18
 
19
+ mattr_accessor :direct_otp_length
20
+ @@direct_otp_length = 6
21
+
22
+ mattr_accessor :direct_otp_valid_for
23
+ @@direct_otp_valid_for = 5.minutes
24
+
19
25
  mattr_accessor :remember_otp_session_for_seconds
20
26
  @@remember_otp_session_for_seconds = 0
21
27
 
22
28
  mattr_accessor :otp_secret_encryption_key
23
29
  @@otp_secret_encryption_key = ''
30
+
31
+ mattr_accessor :second_factor_resource_id
32
+ @@second_factor_resource_id = 'id'
24
33
  end
25
34
 
26
35
  module TwoFactorAuthentication
@@ -4,7 +4,7 @@ module TwoFactorAuthentication
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- before_filter :handle_two_factor_authentication
7
+ before_action :handle_two_factor_authentication
8
8
  end
9
9
 
10
10
  private
@@ -1,32 +1,13 @@
1
1
  Warden::Manager.after_authentication do |user, auth, options|
2
- reset_otp_state_for(user)
3
-
4
- expected_cookie_value = "#{user.class}-#{user.id}"
5
- actual_cookie_value = auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME]
6
- if actual_cookie_value.nil?
7
- bypass_by_cookie = false
8
- else
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]
9
5
  bypass_by_cookie = actual_cookie_value == expected_cookie_value
10
6
  end
11
7
 
12
8
  if user.respond_to?(:need_two_factor_authentication?) && !bypass_by_cookie
13
9
  if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request)
14
- user.send_two_factor_authentication_code
10
+ user.send_new_otp unless user.totp_enabled?
15
11
  end
16
12
  end
17
13
  end
18
-
19
- Warden::Manager.before_logout do |user, _auth, _options|
20
- reset_otp_state_for(user)
21
- end
22
-
23
- def reset_otp_state_for(user)
24
- klass_string = "#{user.class}OtpSender"
25
- return unless Object.const_defined?(klass_string)
26
-
27
- klass = Object.const_get(klass_string)
28
-
29
- otp_sender = klass.new(user)
30
-
31
- otp_sender.reset_otp_state if otp_sender.respond_to?(:reset_otp_state)
32
- end
@@ -11,42 +11,57 @@ module Devise
11
11
  def has_one_time_password(options = {})
12
12
  include InstanceMethodsOnActivation
13
13
  include EncryptionInstanceMethods if options[:encrypted] == true
14
-
15
- before_create { populate_otp_column }
16
14
  end
17
15
 
18
16
  ::Devise::Models.config(
19
17
  self, :max_login_attempts, :allowed_otp_drift_seconds, :otp_length,
20
- :remember_otp_session_for_seconds, :otp_secret_encryption_key)
18
+ :remember_otp_session_for_seconds, :otp_secret_encryption_key,
19
+ :direct_otp_length, :direct_otp_valid_for, :totp_timestamp)
21
20
  end
22
21
 
23
22
  module InstanceMethodsOnActivation
24
23
  def authenticate_otp(code, options = {})
25
- totp = ROTP::TOTP.new(
26
- otp_secret_key, digits: options[:otp_length] || self.class.otp_length
27
- )
28
- drift = options[:drift] || self.class.allowed_otp_drift_seconds
24
+ return true if direct_otp && authenticate_direct_otp(code)
25
+ return true if totp_enabled? && authenticate_totp(code, options)
26
+ false
27
+ end
29
28
 
30
- totp.verify_with_drift(code, drift)
29
+ def authenticate_direct_otp(code)
30
+ return false if direct_otp.nil? || direct_otp != code || direct_otp_expired?
31
+ clear_direct_otp
32
+ true
31
33
  end
32
34
 
33
- def otp_code(time = Time.now, options = {})
34
- ROTP::TOTP.new(
35
- otp_secret_key,
36
- digits: options[:otp_length] || self.class.otp_length
37
- ).at(time, true)
35
+ def authenticate_totp(code, options = {})
36
+ totp_secret = options[:otp_secret_key] || otp_secret_key
37
+ digits = options[:otp_length] || self.class.otp_length
38
+ drift = options[:drift] || self.class.allowed_otp_drift_seconds
39
+ raise "authenticate_totp called with no otp_secret_key set" if totp_secret.nil?
40
+ totp = ROTP::TOTP.new(totp_secret, digits: digits)
41
+ new_timestamp = totp.verify_with_drift_and_prior(code, drift, totp_timestamp)
42
+ return false unless new_timestamp
43
+ self.totp_timestamp = new_timestamp
44
+ true
38
45
  end
39
46
 
40
47
  def provisioning_uri(account = nil, options = {})
41
- account ||= self.email if self.respond_to?(:email)
42
- ROTP::TOTP.new(otp_secret_key, options).provisioning_uri(account)
48
+ totp_secret = options[:otp_secret_key] || otp_secret_key
49
+ options[:digits] ||= options[:otp_length] || self.class.otp_length
50
+ raise "provisioning_uri called with no otp_secret_key set" if totp_secret.nil?
51
+ account ||= email if respond_to?(:email)
52
+ ROTP::TOTP.new(totp_secret, options).provisioning_uri(account)
43
53
  end
44
54
 
45
55
  def need_two_factor_authentication?(request)
46
56
  true
47
57
  end
48
58
 
49
- def send_two_factor_authentication_code
59
+ def send_new_otp(options = {})
60
+ create_direct_otp options
61
+ send_two_factor_authentication_code(direct_otp)
62
+ end
63
+
64
+ def send_two_factor_authentication_code(code)
50
65
  raise NotImplementedError.new("No default implementation - please define in your class.")
51
66
  end
52
67
 
@@ -58,8 +73,41 @@ module Devise
58
73
  self.class.max_login_attempts
59
74
  end
60
75
 
61
- def populate_otp_column
62
- self.otp_secret_key = ROTP::Base32.random_base32
76
+ def totp_enabled?
77
+ respond_to?(:otp_secret_key) && !otp_secret_key.nil?
78
+ end
79
+
80
+ def confirm_totp_secret(secret, code, options = {})
81
+ return false unless authenticate_totp(code, {otp_secret_key: secret})
82
+ self.otp_secret_key = secret
83
+ true
84
+ end
85
+
86
+ def generate_totp_secret
87
+ ROTP::Base32.random_base32
88
+ end
89
+
90
+ def create_direct_otp(options = {})
91
+ # Create a new random OTP and store it in the database
92
+ digits = options[:length] || self.class.direct_otp_length || 6
93
+ update_attributes(
94
+ direct_otp: random_base10(digits),
95
+ direct_otp_sent_at: Time.now.utc
96
+ )
97
+ end
98
+
99
+ private
100
+
101
+ def random_base10(digits)
102
+ SecureRandom.random_number(10**digits).to_s.rjust(digits, '0')
103
+ end
104
+
105
+ def direct_otp_expired?
106
+ Time.now.utc > direct_otp_sent_at + self.class.direct_otp_valid_for
107
+ end
108
+
109
+ def clear_direct_otp
110
+ update_attributes(direct_otp: nil, direct_otp_sent_at: nil)
63
111
  end
64
112
  end
65
113
 
@@ -105,7 +153,8 @@ module Devise
105
153
  value: value,
106
154
  key: Devise.otp_secret_encryption_key,
107
155
  iv: iv_for_attribute,
108
- salt: salt_for_attribute
156
+ salt: salt_for_attribute,
157
+ algorithm: 'aes-256-cbc'
109
158
  }
110
159
  end
111
160
 
@@ -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
@@ -15,5 +15,17 @@ module TwoFactorAuthentication
15
15
  def encrypted_otp_secret_key_salt
16
16
  apply_devise_schema :encrypted_otp_secret_key_salt, String
17
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
18
30
  end
19
31
  end
@@ -1,3 +1,3 @@
1
1
  module TwoFactorAuthentication
2
- VERSION = "1.1.5".freeze
2
+ VERSION = "2.0.0".freeze
3
3
  end
@@ -8,8 +8,8 @@ describe Devise::TwoFactorAuthenticationController, type: :controller do
8
8
 
9
9
  context 'after user enters valid OTP code' do
10
10
  it 'returns true' do
11
- post :update, code: controller.current_user.otp_code
12
-
11
+ controller.current_user.send_new_otp
12
+ post :update, code: controller.current_user.direct_otp
13
13
  expect(subject.is_fully_authenticated?).to eq true
14
14
  end
15
15
  end
@@ -5,6 +5,7 @@ feature "User of two factor authentication" do
5
5
  context 'sending two factor authentication code via SMS' do
6
6
  shared_examples 'sends and authenticates code' do |user, type|
7
7
  before do
8
+ user.reload
8
9
  if type == 'encrypted'
9
10
  allow(User).to receive(:has_one_time_password).with(encrypted: true)
10
11
  end
@@ -18,12 +19,12 @@ feature "User of two factor authentication" do
18
19
  visit new_user_session_path
19
20
  complete_sign_in_form_for(user)
20
21
 
21
- expect(page).to have_content 'Enter your personal code'
22
+ expect(page).to have_content 'Enter the code that was sent to you'
22
23
 
23
24
  expect(SMSProvider.messages.size).to eq(1)
24
25
  message = SMSProvider.last_message
25
26
  expect(message.to).to eq(user.phone_number)
26
- expect(message.body).to eq(user.otp_code)
27
+ expect(message.body).to eq(user.reload.direct_otp)
27
28
  end
28
29
 
29
30
  it 'authenticates a valid OTP code' do
@@ -32,7 +33,7 @@ feature "User of two factor authentication" do
32
33
 
33
34
  expect(page).to have_content('You are signed in as Marissa')
34
35
 
35
- fill_in 'code', with: user.otp_code
36
+ fill_in 'code', with: SMSProvider.last_message.body
36
37
  click_button 'Submit'
37
38
 
38
39
  within('.flash.notice') do
@@ -66,7 +67,7 @@ feature "User of two factor authentication" do
66
67
 
67
68
  expect(page).to_not have_content("Your Personal Dashboard")
68
69
 
69
- fill_in "code", with: user.otp_code
70
+ fill_in "code", with: SMSProvider.last_message.body
70
71
  click_button "Submit"
71
72
 
72
73
  expect(page).to have_content("Your Personal Dashboard")
@@ -84,7 +85,7 @@ feature "User of two factor authentication" do
84
85
  fill_in "code", with: "incorrect#{rand(100)}"
85
86
  click_button "Submit"
86
87
 
87
- within(".flash.error") do
88
+ within(".flash.alert") do
88
89
  expect(page).to have_content("Attempt failed")
89
90
  end
90
91
  end
@@ -113,9 +114,7 @@ feature "User of two factor authentication" do
113
114
  end
114
115
 
115
116
  scenario "doesn't require TFA code again within 30 days" do
116
- visit user_two_factor_authentication_path
117
- fill_in "code", with: user.otp_code
118
- click_button "Submit"
117
+ sms_sign_in
119
118
 
120
119
  logout
121
120
 
@@ -126,9 +125,7 @@ feature "User of two factor authentication" do
126
125
  end
127
126
 
128
127
  scenario "requires TFA code again after 30 days" do
129
- visit user_two_factor_authentication_path
130
- fill_in "code", with: user.otp_code
131
- click_button "Submit"
128
+ sms_sign_in
132
129
 
133
130
  logout
134
131
 
@@ -136,13 +133,11 @@ feature "User of two factor authentication" do
136
133
  login_as user
137
134
  visit dashboard_path
138
135
  expect(page).to have_content("You are signed in as Marissa")
139
- expect(page).to have_content("Enter your personal code")
136
+ expect(page).to have_content("Enter the code that was sent to you")
140
137
  end
141
138
 
142
139
  scenario 'TFA should be different for different users' do
143
- visit user_two_factor_authentication_path
144
- fill_in 'code', with: user.otp_code
145
- click_button 'Submit'
140
+ sms_sign_in
146
141
 
147
142
  tfa_cookie1 = get_tfa_cookie()
148
143
 
@@ -151,19 +146,22 @@ feature "User of two factor authentication" do
151
146
 
152
147
  user2 = create_user()
153
148
  login_as(user2)
154
- visit user_two_factor_authentication_path
155
- fill_in 'code', with: user2.otp_code
156
- click_button 'Submit'
149
+ sms_sign_in
157
150
 
158
151
  tfa_cookie2 = get_tfa_cookie()
159
152
 
160
153
  expect(tfa_cookie1).not_to eq tfa_cookie2
161
154
  end
162
155
 
163
- scenario 'TFA should be unique for specific user' do
156
+ def sms_sign_in
157
+ SMSProvider.messages.clear()
164
158
  visit user_two_factor_authentication_path
165
- fill_in 'code', with: user.otp_code
159
+ fill_in 'code', with: SMSProvider.last_message.body
166
160
  click_button 'Submit'
161
+ end
162
+
163
+ scenario 'TFA should be unique for specific user' do
164
+ sms_sign_in
167
165
 
168
166
  tfa_cookie1 = get_tfa_cookie()
169
167
 
@@ -174,7 +172,7 @@ feature "User of two factor authentication" do
174
172
  set_tfa_cookie(tfa_cookie1)
175
173
  login_as(user2)
176
174
  visit dashboard_path
177
- expect(page).to have_content('Enter your personal code')
175
+ expect(page).to have_content("Enter the code that was sent to you")
178
176
  end
179
177
  end
180
178
 
@@ -187,76 +185,41 @@ feature "User of two factor authentication" do
187
185
 
188
186
  describe 'signing in' do
189
187
  let(:user) { create_user }
188
+ let(:admin) { create_admin }
190
189
 
191
- scenario 'when UserOtpSender#reset_otp_state is defined' do
192
- klass = stub_const 'UserOtpSender', Class.new
193
-
194
- klass.class_eval do
195
- def reset_otp_state; end
196
- end
197
-
198
- otp_sender = instance_double(UserOtpSender)
199
- expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender)
200
- expect(otp_sender).to receive(:reset_otp_state)
201
-
190
+ scenario 'user signs is' do
202
191
  visit new_user_session_path
203
192
  complete_sign_in_form_for(user)
204
- end
205
-
206
- scenario 'when UserOtpSender#reset_otp_state is not defined' do
207
- klass = stub_const 'UserOtpSender', Class.new
208
193
 
209
- klass.class_eval do
210
- def reset_otp_state; end
211
- end
212
-
213
- otp_sender = instance_double(UserOtpSender)
214
- allow(otp_sender).to receive(:respond_to?).with(:reset_otp_state).and_return(false)
194
+ expect(page).to have_content('Signed in successfully.')
195
+ end
215
196
 
216
- expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender)
217
- expect(otp_sender).to_not receive(:reset_otp_state)
197
+ scenario 'admin signs in' do
198
+ visit new_admin_session_path
199
+ complete_sign_in_form_for(admin)
218
200
 
219
- visit new_user_session_path
220
- complete_sign_in_form_for(user)
201
+ expect(page).to have_content('Signed in successfully.')
221
202
  end
222
203
  end
223
204
 
224
205
  describe 'signing out' do
225
206
  let(:user) { create_user }
207
+ let(:admin) { create_admin }
226
208
 
227
- scenario 'when UserOtpSender#reset_otp_state is defined' do
209
+ scenario 'user signs out' do
228
210
  visit new_user_session_path
229
211
  complete_sign_in_form_for(user)
230
-
231
- klass = stub_const 'UserOtpSender', Class.new
232
- klass.class_eval do
233
- def reset_otp_state; end
234
- end
235
-
236
- otp_sender = instance_double(UserOtpSender)
237
-
238
- expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender)
239
- expect(otp_sender).to receive(:reset_otp_state)
240
-
241
212
  visit destroy_user_session_path
242
- end
243
-
244
- scenario 'when UserOtpSender#reset_otp_state is not defined' do
245
- visit new_user_session_path
246
- complete_sign_in_form_for(user)
247
213
 
248
- klass = stub_const 'UserOtpSender', Class.new
249
- klass.class_eval do
250
- def reset_otp_state; end
251
- end
252
-
253
- otp_sender = instance_double(UserOtpSender)
254
- allow(otp_sender).to receive(:respond_to?).with(:reset_otp_state).and_return(false)
214
+ expect(page).to have_content('Signed out successfully.')
215
+ end
255
216
 
256
- expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender)
257
- expect(otp_sender).to_not receive(:reset_otp_state)
217
+ scenario 'admin signs out' do
218
+ visit new_admin_session_path
219
+ complete_sign_in_form_for(admin)
220
+ visit destroy_admin_session_path
258
221
 
259
- visit destroy_user_session_path
222
+ expect(page).to have_content('Signed out successfully.')
260
223
  end
261
224
  end
262
225
  end