From 4d0acf1ddd19e0c90af397f88917318c1bccc073 Mon Sep 17 00:00:00 2001 From: Moncef Belyamani Date: Tue, 22 Dec 2015 16:34:27 -0500 Subject: [PATCH] Allow executing code after sign in and before sign out **Why** In some cases, it might be necessary to run some code right after the user signs in, but before the OTP is sent, and also right before a user signs out. For example, consider this scenario: - The app requires the user to confirm their phone number before it gets saved. This confirmation is done by sending an OTP to the phone and asking the user to enter it. - User mistypes the number, then closes the anonymous browser window, or signs out before confirming - User signs back in, and OTP is sent to the mistyped number. User is now unable to fully sign in since the OTP is being sent to the wrong number In order to prevent this scenario, we need to be able to reset the `unconfirmed_mobile` to nil before the OTP is sent, and before they sign out so that they can type it in again. **How** Allow the gem user to define an OtpSender class with a `reset_otp_state` method --- README.md | 33 +++++++++++ .../hooks/two_factor_authenticatable.rb | 17 ++++++ .../two_factor_authenticatable_spec.rb | 55 +++++++++++++++++++ spec/rails_app/config/initializers/devise.rb | 2 +- 4 files changed, 106 insertions(+), 1 deletion(-) 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