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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +21 -0
- data/.rubocop.yml +295 -0
- data/.travis.yml +4 -5
- data/CHANGELOG.md +24 -14
- data/README.md +57 -65
- data/app/controllers/devise/two_factor_authentication_controller.rb +28 -12
- data/app/views/devise/two_factor_authentication/show.html.erb +10 -1
- data/config/locales/en.yml +1 -0
- data/config/locales/es.yml +8 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/ru.yml +1 -0
- data/lib/generators/active_record/templates/migration.rb +3 -0
- data/lib/two_factor_authentication.rb +9 -0
- data/lib/two_factor_authentication/controllers/helpers.rb +1 -1
- data/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb +4 -23
- data/lib/two_factor_authentication/models/two_factor_authenticatable.rb +68 -19
- data/lib/two_factor_authentication/routes.rb +3 -1
- data/lib/two_factor_authentication/schema.rb +12 -0
- data/lib/two_factor_authentication/version.rb +1 -1
- data/spec/controllers/two_factor_authentication_controller_spec.rb +2 -2
- data/spec/features/two_factor_authenticatable_spec.rb +36 -73
- data/spec/lib/two_factor_authentication/models/two_factor_authenticatable_spec.rb +137 -80
- data/spec/rails_app/app/controllers/home_controller.rb +1 -1
- data/spec/rails_app/app/models/admin.rb +6 -0
- data/spec/rails_app/app/models/encrypted_user.rb +2 -1
- data/spec/rails_app/app/models/guest_user.rb +8 -1
- data/spec/rails_app/app/models/user.rb +2 -2
- data/spec/rails_app/config/initializers/devise.rb +2 -2
- data/spec/rails_app/config/routes.rb +1 -0
- data/spec/rails_app/db/migrate/20140403184646_devise_create_users.rb +1 -1
- data/spec/rails_app/db/migrate/20160209032439_devise_create_admins.rb +42 -0
- data/spec/rails_app/db/schema.rb +19 -1
- data/spec/support/authenticated_model_helper.rb +22 -15
- data/spec/support/controller_helper.rb +1 -1
- data/spec/support/totp_helper.rb +11 -0
- data/two_factor_authentication.gemspec +1 -1
- metadata +74 -7
| @@ -1,6 +1,8 @@ | |
| 1 | 
            +
            require 'devise/version'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            class Devise::TwoFactorAuthenticationController < DeviseController
         | 
| 2 | 
            -
               | 
| 3 | 
            -
               | 
| 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. | 
| 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 | 
            -
                 | 
| 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 | 
            -
             | 
| 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 %>
         | 
    
        data/config/locales/en.yml
    CHANGED
    
    
| @@ -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."
         | 
    
        data/config/locales/fr.yml
    CHANGED
    
    
    
        data/config/locales/ru.yml
    CHANGED
    
    
| @@ -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
         | 
| @@ -1,32 +1,13 @@ | |
| 1 1 | 
             
            Warden::Manager.after_authentication do |user, auth, options|
         | 
| 2 | 
            -
               | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 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. | 
| 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 | 
            -
                       | 
| 26 | 
            -
             | 
| 27 | 
            -
                       | 
| 28 | 
            -
             | 
| 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 | 
            -
             | 
| 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  | 
| 34 | 
            -
                       | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
                       | 
| 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 | 
            -
                       | 
| 42 | 
            -
                       | 
| 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  | 
| 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  | 
| 62 | 
            -
                       | 
| 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
         | 
| @@ -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 | 
            -
                     | 
| 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  | 
| 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. | 
| 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:  | 
| 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:  | 
| 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. | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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  | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
            -
                   | 
| 156 | 
            +
                  def sms_sign_in
         | 
| 157 | 
            +
                    SMSProvider.messages.clear()
         | 
| 164 158 | 
             
                    visit user_two_factor_authentication_path
         | 
| 165 | 
            -
                    fill_in 'code', with:  | 
| 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( | 
| 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 ' | 
| 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 | 
            -
                   | 
| 210 | 
            -
             | 
| 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 | 
            -
             | 
| 217 | 
            -
                   | 
| 197 | 
            +
                scenario 'admin signs in' do
         | 
| 198 | 
            +
                  visit new_admin_session_path
         | 
| 199 | 
            +
                  complete_sign_in_form_for(admin)
         | 
| 218 200 |  | 
| 219 | 
            -
                   | 
| 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 ' | 
| 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 | 
            -
                   | 
| 249 | 
            -
             | 
| 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 | 
            -
             | 
| 257 | 
            -
                   | 
| 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 | 
            -
                   | 
| 222 | 
            +
                  expect(page).to have_content('Signed out successfully.')
         | 
| 260 223 | 
             
                end
         | 
| 261 224 | 
             
              end
         | 
| 262 225 | 
             
            end
         |