two_factor_authentication 1.1.5 → 2.0.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 (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