main
  1# frozen_string_literal: true
  2
  3module Saml
  4  module Kit
  5    # The Metadata object can be used to parse an XML string of metadata.
  6    #
  7    #   metadata = Saml::Kit::Metadata.from(raw_xml)
  8    #
  9    # It can also be used to generate a new metadata string.
 10    #
 11    #  metadata = Saml::Kit::Metadata.build do |builder|
 12    #    builder.entity_id = "my-issuer"
 13    #    builder.build_service_provider do |x|
 14    #      x.add_assertion_consumer_service(assertions_url, binding: :http_post)
 15    #      x.add_single_logout_service(logout_url, binding: :http_post)
 16    #    end
 17    #    builder.build_identity_provider do |x|
 18    #      x.add_single_sign_on_service(login_url, binding: :http_redirect)
 19    #      x.add_single_logout_service(logout_url, binding: :http_post)
 20    #    end
 21    #  end
 22    #  puts metadata.to_xml(pretty: true)
 23    #
 24    # See {Saml::Kit::Builders::ServiceProviderMetadata} and
 25    # {Saml::Kit::Builders::IdentityProviderMetadata}
 26    # for a list of options that can be specified.
 27    # {include:file:spec/examples/metadata_spec.rb}
 28    class Metadata
 29      include Validatable
 30      include Buildable
 31      include Translatable
 32      include XmlParseable
 33      include XsdValidatable
 34      extend Forwardable
 35
 36      def_delegator :organization, :organization_name, :organization_url
 37
 38      validates_presence_of :metadata
 39      validate :must_contain_descriptor
 40      validate :must_match_xsd
 41      validate :must_have_valid_signature
 42
 43      attr_reader :name, :content
 44
 45      def initialize(name, content)
 46        @name = name
 47        @content = content
 48      end
 49
 50      # Returns the /EntityDescriptor/@entityID
 51      def entity_id
 52        at_xpath('/md:EntityDescriptor/@entityID').try(:value)
 53      end
 54
 55      # Returns the supported NameIDFormats.
 56      def name_id_formats
 57        search("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
 58      end
 59
 60      def organization(xpath = '/md:EntityDescriptor/md:Organization')
 61        @organization ||= Organization.new(at_xpath(xpath))
 62      end
 63
 64      # Returns the Company
 65      def contact_person_company
 66        at_xpath('/md:EntityDescriptor/md:ContactPerson/md:Company').try(:text)
 67      end
 68
 69      # Returns each of the X509 certificates.
 70      def certificates(
 71        xpath = "/md:EntityDescriptor/md:#{name}/md:KeyDescriptor"
 72      )
 73        @certificates ||= search(xpath).map do |item|
 74          xpath = './ds:KeyInfo/ds:X509Data/ds:X509Certificate'
 75          namespaces = { 'ds' => ::Xml::Kit::Namespaces::XMLDSIG }
 76          cert = item.at_xpath(xpath, namespaces).try(:text)
 77          use_attribute = item.attribute('use')
 78          ::Xml::Kit::Certificate.new(cert, use: use_attribute.try(:value))
 79        end
 80      end
 81
 82      # Returns the encryption certificates
 83      def encryption_certificates
 84        certificates.find_all(&:encryption?)
 85      end
 86
 87      # Returns the signing certificates.
 88      def signing_certificates
 89        certificates.find_all(&:signing?)
 90      end
 91
 92      # Returns each of the service endpoints supported by this metadata.
 93      #
 94      # @param type [String] the type of service.
 95      # .E.g. `AssertionConsumerServiceURL`
 96      def services(type)
 97        search("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
 98          binding = item.attribute('Binding').value
 99          location = item.attribute('Location').value
100          Saml::Kit::Bindings.create_for(binding, location)
101        end
102      end
103
104      # Returns a specifing service binding.
105      #
106      # @param binding [Symbol] can be `:http_post` or `:http_redirect`.
107      # @param type [Symbol] can be on the service element like
108      # `AssertionConsumerServiceURL`, `SingleSignOnService` or
109      # `SingleLogoutService`.
110      def service_for(binding:, type:)
111        binding = Saml::Kit::Bindings.binding_for(binding)
112        services(type).find { |x| x.binding?(binding) }
113      end
114
115      # Returns each of the SingleLogoutService bindings
116      def single_logout_services
117        services('SingleLogoutService')
118      end
119
120      # Returns the SingleLogoutService that matches the specified binding.
121      #
122      # @param binding [Symbol] can be `:http_post` or `:http_redirect`.
123      def single_logout_service_for(binding:)
124        service_for(binding: binding, type: 'SingleLogoutService')
125      end
126
127      # Creates a serialized LogoutRequest.
128      #
129      # @param user [Object] a user object that responds to `name_id_for` and
130      # `assertion_attributes_for`.
131      # @param binding [Symbol] can be `:http_post` or `:http_redirect`.
132      # @param relay_state [String] the relay state to have echo'd back.
133      # @return [Array] Returns an array with a url and Hash of parameters to
134      # send to the other party.
135      def logout_request_for(user, binding: :http_post, relay_state: nil)
136        builder = LogoutRequest.builder(user) { |x| yield x if block_given? }
137        request_binding = single_logout_service_for(binding: binding)
138        request_binding.serialize(builder, relay_state: relay_state)
139      end
140
141      # Returns the certificate that matches the fingerprint
142      #
143      # @param fingerprint [Saml::Kit::Fingerprint] the fingerprint to search.
144      # @param use [Symbol] the type of certificates to look at.
145      # Can be `:signing` or `:encryption`.
146      # @return [Xml::Kit::Certificate] returns the matching
147      # `{Xml::Kit::Certificate}`
148      def matches?(fingerprint, use: :signing)
149        certificates.find { |x| x.for?(use) && x.fingerprint == fingerprint }
150      end
151
152      # Verifies the signature and data using the signing certificates.
153      #
154      # @param algorithm [OpenSSL::Digest] the digest algorithm to use.
155      # E.g. `OpenSSL::Digest::SHA256`
156      # @param signature [String] the signature to verify
157      # @param data [String] the data that is used to produce the signature.
158      # @return [Xml::Kit::Certificate] the certificate that was used to
159      # produce the signature.
160      def verify(algorithm, signature, data)
161        signing_certificates.find do |certificate|
162          certificate.public_key.verify(algorithm, signature, data)
163        end
164      end
165
166      def signature(xpath = '/md:EntityDescriptor/ds:Signature')
167        @signature ||= Signature.new(at_xpath(xpath))
168      end
169
170      def self.from(content)
171        Saml::Kit::Parser.new.metadata_from(content)
172      end
173
174      def self.builder_class
175        Saml::Kit::Builders::Metadata
176      end
177
178      private
179
180      def metadata
181        at_xpath("/md:EntityDescriptor/md:#{name}").present?
182      end
183
184      def must_contain_descriptor
185        errors.add(:base, error_message(:invalid)) unless metadata
186      end
187
188      def must_match_xsd
189        matches_xsd?(METADATA_XSD)
190      end
191
192      def must_have_valid_signature
193        return if !signature.present? || signature.valid?
194
195        signature.each_error do |attribute, error|
196          errors.add(attribute, error)
197        end
198      end
199    end
200  end
201end