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