lib/saml/provider.rb
module Saml
  module Provider
    extend ActiveSupport::Concern

    def assertion_consumer_service_url(index = nil)
      find_indexed_service_url(sp_descriptor.assertion_consumer_services, index)
    end

    # @param [Symbol] type (see #descriptor)
    def artifact_resolution_service_url(index = nil, type = :descriptor)
      find_indexed_service_url(descriptor(type).artifact_resolution_services, index)
    end

    def attribute_consuming_service(index = nil)
      find_indexed_service(sp_descriptor.attribute_consuming_services, index)
    end

    def assertion_consumer_service(index = nil)
      find_indexed_service(sp_descriptor.assertion_consumer_services, index)
    end

    def assertion_consumer_service_indices
      if sp_descriptor.assertion_consumer_services.present?
        sp_descriptor.assertion_consumer_services.map(&:index)
      else
        []
      end
    end

    def entity_descriptor
      @entity_descriptor
    end

    def entity_id
      entity_descriptor.entity_id
    end

    # @param [Symbol] type (see #descriptor)
    def certificate(key_name = nil, use = "signing", type = :descriptor)
      key_descriptor = find_key_descriptor(key_name, use, type)
      key_descriptor.certificate if key_descriptor
    end

    # @param [Symbol] type (see #descriptor)
    def find_key_descriptor(key_name = nil, use = "signing", type = :descriptor)
      descriptor(type).find_key_descriptor(key_name, use)
    end

    def find_key_descriptors_by_use(use, type = :descriptor)
      descriptor(type).find_key_descriptors_by_use(use)
    end

    def find_key_descriptors_by_use_or_without(use, type = :descriptor)
      descriptor(type).find_key_descriptors_by_use_or_without(use)
    end

    def signing_key
      @signing_key || encryption_key
    end

    def encryption_key
      @encryption_key
    end

    def sign(signature_algorithm, data)
      signing_key.sign(digest_method(signature_algorithm).new, data)
    end

    def single_sign_on_service_url(binding)
      find_binding_service(idp_descriptor.single_sign_on_services, binding)
    end

    # @param [Symbol] type (see #descriptor)
    def single_logout_service_url(binding, type = :descriptor)
      find_binding_service(descriptor(type).single_logout_services, binding)
    end

    def attribute_service_url(binding)
      find_binding_service(aa_descriptor.attribute_service, binding)
    end

    def type
      if idp_descriptor(false)
        if sp_descriptor(false)
          "identity_and_service_provider"
        else
          "identity_provider"
        end
      else
        "service_provider"
      end
    end

    def verify(signature_algorithm, signature, data, key_name = nil)
      certificates = if key_name.blank? && iterate_certificates_until_verified?
        find_key_descriptors_by_use_or_without('signing').collect(&:certificate)
      else
        Array(certificate(key_name))
      end
      valid = certificates.any? do |cert|
        cert.public_key.verify(digest_method(signature_algorithm).new, signature, data) rescue false
      end

      # Clear OpenSSL error queue if verification fails - https://bugs.ruby-lang.org/issues/7215
      OpenSSL.errors if !valid

      valid
    end

    def authn_requests_signed?
      sp_descriptor(false).try(:authn_requests_signed)
    end

    def iterate_certificates_until_verified?
      false
    end

    private

    def digest_method(signature_algorithm)
      digest = signature_algorithm && signature_algorithm =~ /sha(.*?)$/i && $1.to_i
      case digest
        when 512 then
          OpenSSL::Digest::SHA512
        when 256 then
          OpenSSL::Digest::SHA256
        else
          OpenSSL::Digest::SHA1
      end
    end

    # @param type [Symbol] Descriptor type, available types :sp_descriptor, :idp_descriptor, :aa_descriptor or :descriptor
    # @return [Saml::ComplexTypes::SSODescriptorType]
    def descriptor(type = :descriptor)
      return sp_descriptor if :sp_descriptor == type
      return idp_descriptor if :idp_descriptor == type
      return aa_descriptor if :aa_descriptor == type
      entity_descriptor.sp_sso_descriptor || entity_descriptor.idp_sso_descriptor || entity_descriptor.attribute_authority_descriptor
    end

    # @return [Saml::Elements::SPSSODescriptor]
    def sp_descriptor(raise_error = true)
      entity_descriptor.sp_sso_descriptor || raise_error &&
          raise(Saml::Errors::InvalidProvider.new("Cannot find service provider with entity_id: #{entity_id}"))
    end

    # @return [Saml::Elements::IDPSSODescriptor]
    def idp_descriptor(raise_error = true)
      entity_descriptor.idp_sso_descriptor || raise_error &&
          raise(Saml::Errors::InvalidProvider.new("Cannot find identity provider with entity_id: #{entity_id}"))
    end

    # Attribute authority descriptor
    # @return [Saml::Elements::AttributeAuthorityDescriptor]
    def aa_descriptor(raise_error = true)
      entity_descriptor.attribute_authority_descriptor || raise_error &&
          raise(Saml::Errors::InvalidProvider.new("Cannot find attribute authority provider with entity_id: #{entity_id}"))
    end

    def find_indexed_service_url(service_list, index)
      service = find_indexed_service(service_list, index)
      service.location if service
    end

    def find_indexed_service(service_list, index)
      if index
        service_list.find { |service| service.index == index }
      else
        service_list.find { |service| service.is_default }
      end
    end

    def find_binding_service(service_list, binding)
      service = service_list.find { |service| service.binding == binding }
      service.location if service
    end
  end
end
