Browse Source

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
master
Moncef Belyamani 9 years ago
parent
commit
4d0acf1ddd
  1. 33
      README.md
  2. 17
      lib/two_factor_authentication/hooks/two_factor_authenticatable.rb
  3. 55
      spec/features/two_factor_authenticatable_spec.rb
  4. 2
      spec/rails_app/config/initializers/devise.rb

33
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)

17
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

55
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

2
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

Loading…
Cancel
Save