lib/rodauth/features/two_factor_base.rb



# frozen-string-literal: true

module Rodauth
  Feature.define(:two_factor_base, :TwoFactorBase) do
    loaded_templates %w'two-factor-manage two-factor-auth two-factor-disable'

    view 'two-factor-manage', 'Manage Multifactor Authentication', 'two_factor_manage'
    view 'two-factor-auth', 'Authenticate Using Additional Factor', 'two_factor_auth'
    view 'two-factor-disable', 'Remove All Multifactor Authentication Methods', 'two_factor_disable'

    before :two_factor_disable

    after :two_factor_authentication
    after :two_factor_disable

    additional_form_tags :two_factor_disable

    button "Remove All Multifactor Authentication Methods", :two_factor_disable

    redirect(:two_factor_auth)
    redirect(:two_factor_already_authenticated)
    redirect(:two_factor_disable)
    redirect(:two_factor_need_setup){two_factor_manage_path}
    redirect(:two_factor_auth_required){two_factor_auth_path}

    notice_flash "You have been multifactor authenticated", "two_factor_auth"
    notice_flash "All multifactor authentication methods have been disabled", "two_factor_disable"

    error_flash "This account has not been setup for multifactor authentication", 'two_factor_not_setup'
    error_flash "You have already been multifactor authenticated", 'two_factor_already_authenticated'
    error_flash "You need to authenticate via an additional factor before continuing", 'two_factor_need_authentication'
    error_flash "Unable to remove all multifactor authentication methods", "two_factor_disable"

    auth_value_method :two_factor_already_authenticated_error_status, 403
    auth_value_method :two_factor_need_authentication_error_status, 401
    auth_value_method :two_factor_not_setup_error_status, 403

    session_key :two_factor_setup_session_key, :two_factor_auth_setup
    session_key :two_factor_auth_redirect_session_key, :two_factor_auth_redirect

    translatable_method :two_factor_setup_heading, "<h2>Setup Multifactor Authentication</h2>"
    translatable_method :two_factor_remove_heading, "<h2>Remove Multifactor Authentication</h2>"
    translatable_method :two_factor_disable_link_text, "Remove All Multifactor Authentication Methods"
    auth_value_method :two_factor_auth_return_to_requested_location?, false

    auth_cached_method :two_factor_auth_links
    auth_cached_method :two_factor_setup_links
    auth_cached_method :two_factor_remove_links

    auth_value_methods :two_factor_modifications_require_password?

    auth_methods(
      :two_factor_authenticated?,
      :two_factor_remove,
      :two_factor_remove_auth_failures,
      :two_factor_remove_session,
      :two_factor_update_session
    )

    internal_request_method :two_factor_disable

    route(:two_factor_manage, 'multifactor-manage') do |r|
      require_account
      before_two_factor_manage_route

      r.get do
        all_links = two_factor_setup_links + two_factor_remove_links
        if all_links.length == 1
          redirect all_links[0][1]
        end
        two_factor_manage_view
      end
    end

    route(:two_factor_auth, 'multifactor-auth') do |r|
      require_login
      require_account_session
      require_two_factor_setup
      require_two_factor_not_authenticated
      before_two_factor_auth_route

      r.get do
        if two_factor_auth_links.length == 1
          redirect two_factor_auth_links[0][1]
        end
        two_factor_auth_view
      end
    end

    route(:two_factor_disable, 'multifactor-disable') do |r|
      require_account
      require_two_factor_setup
      before_two_factor_disable_route

      r.get do
        two_factor_disable_view
      end

      r.post do
        if two_factor_password_match?(param(password_param))
          transaction do
            before_two_factor_disable
            two_factor_remove
            _two_factor_remove_all_from_session
            after_two_factor_disable
          end
          set_notice_flash two_factor_disable_notice_flash
          redirect two_factor_disable_redirect
        end

        set_response_error_reason_status(:invalid_password, invalid_password_error_status)
        set_field_error(password_param, invalid_password_message)
        set_error_flash two_factor_disable_error_flash
        two_factor_disable_view
      end
    end

    def two_factor_modifications_require_password?
      modifications_require_password?
    end

    def authenticated?
      # False if not authenticated via single factor
      return false unless super

      # True if already authenticated via 2nd factor
      return true if two_factor_authenticated?

      # True if authenticated via single factor and 2nd factor not setup 
      !uses_two_factor_authentication?
    end

    def require_authentication
      super

      # Avoid database query if already authenticated via 2nd factor
      return if two_factor_authenticated?

      require_two_factor_authenticated if uses_two_factor_authentication?
    end

    def require_two_factor_setup
      # Avoid database query if already authenticated via 2nd factor
      return if two_factor_authenticated?

      return if uses_two_factor_authentication?

      set_redirect_error_status(two_factor_not_setup_error_status)
      set_error_reason :two_factor_not_setup
      set_redirect_error_flash two_factor_not_setup_error_flash
      redirect two_factor_need_setup_redirect
    end
    
    def require_two_factor_not_authenticated(auth_type = nil)
      if two_factor_authenticated? || (auth_type && two_factor_login_type_match?(auth_type))
        set_redirect_error_status(two_factor_already_authenticated_error_status)
        set_error_reason :two_factor_already_authenticated
        set_redirect_error_flash two_factor_already_authenticated_error_flash
        redirect two_factor_already_authenticated_redirect
      end
    end

    def require_two_factor_authenticated
      unless two_factor_authenticated?
        if two_factor_auth_return_to_requested_location?
          set_session_value(two_factor_auth_redirect_session_key, request.fullpath)
        end
        set_redirect_error_status(two_factor_need_authentication_error_status)
        set_error_reason :two_factor_need_authentication
        set_redirect_error_flash two_factor_need_authentication_error_flash
        redirect two_factor_auth_required_redirect
      end
    end

    def two_factor_remove_auth_failures
      nil
    end

    def two_factor_password_match?(password)
      if two_factor_modifications_require_password?
        password_match?(password)
      else
        true
      end
    end

    def two_factor_authenticated?
      authenticated_by && authenticated_by.length >= 2
    end

    def two_factor_authentication_setup?
      possible_authentication_methods.length >= 2
    end

    def uses_two_factor_authentication?
      return false unless logged_in?
      set_session_value(two_factor_setup_session_key, two_factor_authentication_setup?) unless session.has_key?(two_factor_setup_session_key)
      session[two_factor_setup_session_key]
    end

    def two_factor_login_type_match?(type)
      authenticated_by && authenticated_by.include?(type)
    end

    def two_factor_remove
      nil
    end

    private

    def _two_factor_auth_links
      (super if defined?(super)) || []
    end

    def _two_factor_setup_links
      []
    end

    def _two_factor_remove_links
      []
    end

    def _two_factor_remove_all_from_session
      nil
    end

    def after_close_account
      super if defined?(super)
      two_factor_remove
    end

    def two_factor_authenticate(type)
      two_factor_update_session(type)
      two_factor_remove_auth_failures
      after_two_factor_authentication
      set_notice_flash two_factor_auth_notice_flash
      redirect_two_factor_authenticated
    end

    def redirect_two_factor_authenticated
      saved_two_factor_auth_redirect = remove_session_value(two_factor_auth_redirect_session_key)
      redirect saved_two_factor_auth_redirect || two_factor_auth_redirect
    end

    def two_factor_remove_session(type)
      authenticated_by.delete(type)
      remove_session_value(two_factor_setup_session_key)
      if authenticated_by.empty?
        clear_session
      end
    end

    def two_factor_update_session(auth_type)
      authenticated_by << auth_type
      set_session_value(two_factor_setup_session_key, true)
    end
  end
end