Browse Source

Merge pull request #61 from monfresh/customize-tfa-hook

Allow executing code after sign in and before sign out
master
Dmitrii Golub 9 years ago
parent
commit
d350b697da
  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 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 ### Example
[TwoFactorAuthenticationExample](https://github.com/Houdini/TwoFactorAuthenticationExample) [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| Warden::Manager.after_authentication do |user, auth, options|
reset_otp_state_for(user)
if user.respond_to?(:need_two_factor_authentication?) && if user.respond_to?(:need_two_factor_authentication?) &&
!auth.env["action_dispatch.cookies"].signed[TwoFactorAuthentication::REMEMBER_TFA_COOKIE_NAME] !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) 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 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 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 end

2
spec/rails_app/config/initializers/devise.rb

@ -224,7 +224,7 @@ Devise.setup do |config|
# config.navigational_formats = ['*/*', :html] # config.navigational_formats = ['*/*', :html]
# The default HTTP method used to sign out a resource. Default is :delete. # 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 # ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting # Add a new OmniAuth provider. Check the wiki for more information on setting

Loading…
Cancel
Save