applicake/doorkeeper

View on GitHub
lib/doorkeeper/models/application_mixin.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

module Doorkeeper
  module ApplicationMixin
    extend ActiveSupport::Concern

    include OAuth::Helpers
    include Models::Orderable
    include Models::Hashable
    include Models::Scopes

    included do
      configure_secrets_hashing
    end

    # :nodoc
    module ClassMethods
      # We want to perform secret hashing whenever the user
      # enables the configuration option +hash_application_secrets+
      def perform_secret_hashing?
        Doorkeeper.configuration.hash_application_secrets?
      end

      # Returns an instance of the Doorkeeper::Application with
      # specific UID and secret.
      #
      # Public/Non-confidential applications will only find by uid if secret is
      # blank.
      #
      # @param uid [#to_s] UID (any object that responds to `#to_s`)
      # @param secret [#to_s] secret (any object that responds to `#to_s`)
      #
      # @return [Doorkeeper::Application, nil] Application instance or nil
      #   if there is no record with such credentials
      #
      def by_uid_and_secret(uid, secret)
        app = by_uid(uid)
        return unless app
        return app if secret.blank? && !app.confidential?
        return unless app.secret_matches?(secret)
        app
      end

      # Returns an instance of the Doorkeeper::Application with specific UID.
      #
      # @param uid [#to_s] UID (any object that responds to `#to_s`)
      #
      # @return [Doorkeeper::Application, nil] Application instance or nil
      #   if there is no record with such UID
      #
      def by_uid(uid)
        find_by(uid: uid.to_s)
      end

      ##
      # Configure secrets hashing for applications.
      # We will try to load bcrypt for hashing, and fall back
      # when it cannot be loaded.
      def configure_secrets_hashing
        if perform_secret_hashing? && bcrypt_present?
          use_bcrypt_for_hashing
        else
          self.secret_comparer = nil
          self.secret_hash_function = nil
        end
      end

      private

      ##
      # Test if we can require the BCrypt gem
      def bcrypt_present?
        require 'bcrypt'
        true
      rescue LoadError
        false
      end

      ##
      # Set hash function and comparer to BCrypt password hashing
      # Requires to bcrypt gem to be present
      def use_bcrypt_for_hashing
        # If available, use BCrypt as the hashing function for applications
        self.secret_hash_function = lambda do |plain_token|
          ::BCrypt::Password.create(plain_token.to_s)
        end

        # Also need to override the comparer function for BCrypt
        self.secret_comparer = lambda do |plain, secret|
          begin
            ::BCrypt::Password.new(secret.to_s) == plain.to_s
          rescue BCrypt::Errors::InvalidHash
            false
          end
        end
      end
    end

    # Set an application's valid redirect URIs.
    #
    # @param uris [String, Array] Newline-separated string or array the URI(s)
    #
    # @return [String] The redirect URI(s) seperated by newlines.
    def redirect_uri=(uris)
      super(uris.is_a?(Array) ? uris.join("\n") : uris)
    end

    # Check whether the given plain text secret matches our stored secret
    #
    # @param input [#to_s] Plain secret provided by user
    #        (any object that responds to `#to_s`)
    #
    # @return [true] Whether the given secret matches the stored secret
    #                of this application.
    #
    def secret_matches?(input)
      # return false if either is nil, since secure_compare depends on strings
      # but Application secrets MAY be nil depending on confidentiality.
      return false if input.nil? || secret.nil?

      # When matching the secret by comparer function, all is well.
      return true if self.class.secret_matches?(input, secret)

      # When fallback lookup is enabled, ensure applications
      # with plain secrets can still be found
      if Doorkeeper.configuration.fallback_to_plain_secrets?
        ActiveSupport::SecurityUtils.secure_compare(input, secret)
      else
        false
      end
    end
  end
end