opf/openproject

View on GitHub
modules/two_factor_authentication/app/services/two_factor_authentication/token_service.rb

Summary

Maintainability
A
15 mins
Test Coverage
module TwoFactorAuthentication
  class TokenService
    attr_reader :user, :device, :strategy, :channel

    ##
    # Create a token service for the given user.
    def initialize(user:, use_device: nil, use_channel: nil)
      @user = user
      @device = use_device || user.otp_devices.get_default
      @channel = use_channel || device.try(:channel)

      matching_strategy = get_matching_strategy
      if matching_strategy
        @strategy = matching_strategy.new(user: @user, device: @device, channel: @channel)
      end
    end

    ##
    # Determines whether a token should be entered by the user.
    def requires_token?
      # If 2FA is enforced, always required
      return true if manager.enforced?

      # Otherwise, only enabled if active and a device is present for the user
      manager.enabled? && device.present?
    end

    ##
    # Determines whether the given user needs to register a
    # device during the login flow.
    def needs_registration?
      return false unless manager.enforced?

      device.nil?
    end

    ##
    # Request a token through the active strategy
    # IF the instance is set up to have optional 2FA
    def request
      # Validate that we can request the token for this user
      # and get the matching strategy we will use
      verify_device_and_strategy

      # Produce the token with the given strategy (e.g., sending an sms)
      strategy.transmit

      ServiceResult.success(result: strategy.transmit_success_message)
    rescue StandardError => e
      Rails.logger.error "[2FA plugin] Error during token request to user##{user.id}: #{e}"

      result = ServiceResult.failure
      result.errors.add(:base, e.message)

      result
    end

    ##
    # Validate a token that was input by the user
    def verify(input, **options)
      # Validate that we can request the token for this user
      # and get the matching strategy we will use
      verify_device_and_strategy

      # Produce the token with the given strategy (e.g., sending an sms)
      result = strategy.verify(input, **options)

      ServiceResult.new(success: result)
    rescue StandardError => e
      Rails.logger.error "[2FA plugin] Error during token validation for user##{user.id}: #{e.class} #{e}"

      result = ServiceResult.failure
      result.errors.add(:base, e.message)

      result
    end

    private

    ##
    # Get the matching strategy from the desired channel, if set.
    def get_matching_strategy
      if @channel
        manager.find_matching_strategy(@channel)
      end
    end

    ##
    # Perform service checks for the request and validate endpoints of this service
    def verify_device_and_strategy
      raise I18n.t("two_factor_authentication.error_2fa_disabled") unless manager.enabled?

      # Ensure the user's default device for OTP exists
      raise I18n.t("two_factor_authentication.error_no_device") if device.nil?

      # Ensure a matching registered strategy for the device's channel exists
      raise I18n.t("two_factor_authentication.error_no_matching_strategy") if strategy.nil?
    end

    def manager
      ::OpenProject::TwoFactorAuthentication::TokenStrategyManager
    end
  end
end