diff --git a/README.md b/README.md index 1596828..3574b29 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,39 @@ task :update_users_with_otp_secret_key => :environment do end ``` +#### Executing some code after the user signs in and before they sign out + +In some cases, you might want to perform some action right after the user signs +in, but before the OTP is sent, and also right before the user signs out. One +scenario where you would need this is if you are requiring users to confirm +their phone number first before they can receive an OTP. If they enter a wrong +number, then sign out or close the browser before they confirm, they won't be +able to confirm their real number. To solve this problem, we need to be able to +reset their unconfirmed number before they sign out or sign in, and before the +OTP code is sent. + +To define this action, create a `#{user.class}OtpSender` class that takes the +current user as its parameter, and defines a `#reset_otp_state` instance method. +For example, if your user's class is `User`, you would create a `UserOtpSender` +class, like this: +```ruby +class UserOtpSender + def initialize(user) + @user = user + end + + def reset_otp_state + if @user.unconfirmed_mobile.present? + @user.update(unconfirmed_mobile: nil) + end + end +end +``` +If you have different types of users in your app (for example, User and Admin), +and you need different logic for each type of user, create a second class for +your admin user, such as `AdminOtpSender`, with its own logic for +`#reset_otp_state`. + ### Example [TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample) diff --git a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb index 5b479c4..3f89bdb 100644 --- a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +++ b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb @@ -1,4 +1,6 @@ Warden::Manager.after_authentication do |user, auth, options| + reset_otp_state_for(user) + if user.respond_to?(:need_two_factor_authentication?) && !auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] if auth.session(options[:scope])[TwoFactorAuthentication::NEED_AUTHENTICATION] = user.need_two_factor_authentication?(auth.request) @@ -6,3 +8,18 @@ Warden::Manager.after_authentication do |user, auth, options| end end end + +Warden::Manager.before_logout do |user, _auth, _options| + reset_otp_state_for(user) +end + +def reset_otp_state_for(user) + klass_string = "#{user.class}OtpSender" + return unless Object.const_defined?(klass_string) + + klass = Object.const_get(klass_string) + + otp_sender = klass.new(user) + + otp_sender.reset_otp_state if otp_sender.respond_to?(:reset_otp_state) +end diff --git a/spec/features/two_factor_authenticatable_spec.rb b/spec/features/two_factor_authenticatable_spec.rb index 466010b..ef93e14 100644 --- a/spec/features/two_factor_authenticatable_spec.rb +++ b/spec/features/two_factor_authenticatable_spec.rb @@ -123,4 +123,59 @@ feature "User of two factor authentication" do end end end + + describe 'signing in' do + scenario 'when UserOtpSender#reset_otp_state is defined' do + stub_const 'UserOtpSender', Class.new + + otp_sender = instance_double(UserOtpSender) + expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender) + expect(otp_sender).to receive(:reset_otp_state) + + visit new_user_session_path + complete_sign_in_form_for(user) + end + + scenario 'when UserOtpSender#reset_otp_state is not defined' do + stub_const 'UserOtpSender', Class.new + + otp_sender = instance_double(UserOtpSender) + allow(otp_sender).to receive(:respond_to?).with(:reset_otp_state).and_return(false) + + expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender) + expect(otp_sender).to_not receive(:reset_otp_state) + + visit new_user_session_path + complete_sign_in_form_for(user) + end + end + + describe 'signing out' do + scenario 'when UserOtpSender#reset_otp_state is defined' do + visit new_user_session_path + complete_sign_in_form_for(user) + + stub_const 'UserOtpSender', Class.new + otp_sender = instance_double(UserOtpSender) + + expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender) + expect(otp_sender).to receive(:reset_otp_state) + + visit destroy_user_session_path + end + + scenario 'when UserOtpSender#reset_otp_state is not defined' do + visit new_user_session_path + complete_sign_in_form_for(user) + + stub_const 'UserOtpSender', Class.new + otp_sender = instance_double(UserOtpSender) + allow(otp_sender).to receive(:respond_to?).with(:reset_otp_state).and_return(false) + + expect(UserOtpSender).to receive(:new).with(user).and_return(otp_sender) + expect(otp_sender).to_not receive(:reset_otp_state) + + visit destroy_user_session_path + end + end end diff --git a/spec/rails_app/config/initializers/devise.rb b/spec/rails_app/config/initializers/devise.rb index 3840c1b..8a9d06f 100644 --- a/spec/rails_app/config/initializers/devise.rb +++ b/spec/rails_app/config/initializers/devise.rb @@ -224,7 +224,7 @@ Devise.setup do |config| # config.navigational_formats = ['*/*', :html] # The default HTTP method used to sign out a resource. Default is :delete. - config.sign_out_via = :delete + config.sign_out_via = Rails.env.test? ? :get : :delete # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting