Skip to content

Commit 4f49095

Browse files
authored
Merge pull request #614 from johnnyshields/idp-parser-name_id_format
Support :name_id_format option for IdpMetadataParser
2 parents 993fc10 + 38e0f3c commit 4f49095

File tree

3 files changed

+169
-75
lines changed

3 files changed

+169
-75
lines changed

lib/onelogin/ruby-saml/idp_metadata_parser.rb

Lines changed: 99 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def self.get_idps(metadata_document, only_entity_id=nil)
4747
SamlMetadata::NAMESPACE
4848
)
4949
end
50-
50+
5151
# Parse the Identity Provider metadata and update the settings with the
5252
# IdP values
5353
#
@@ -56,9 +56,10 @@ def self.get_idps(metadata_document, only_entity_id=nil)
5656
#
5757
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
5858
# @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides.
59-
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
60-
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
61-
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
59+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
60+
# @option options [String, Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
61+
# @option options [String, Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
62+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
6263
#
6364
# @return [OneLogin::RubySaml::Settings]
6465
#
@@ -74,9 +75,10 @@ def parse_remote(url, validate_cert = true, options = {})
7475
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
7576
#
7677
# @param options [Hash] options used for parsing the metadata
77-
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
78-
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
79-
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
78+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
79+
# @option options [String, Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
80+
# @option options [String, Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
81+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
8082
#
8183
# @return [Hash]
8284
#
@@ -91,9 +93,10 @@ def parse_remote_to_hash(url, validate_cert = true, options = {})
9193
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
9294
#
9395
# @param options [Hash] options used for parsing the metadata
94-
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
95-
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
96-
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, all found IdPs are returned.
96+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned.
97+
# @option options [String, Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
98+
# @option options [String, Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
99+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
97100
#
98101
# @return [Array<Hash>]
99102
#
@@ -109,9 +112,10 @@ def parse_remote_to_array(url, validate_cert = true, options = {})
109112
#
110113
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
111114
# @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides.
112-
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
113-
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
114-
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
115+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
116+
# @option options [String, Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
117+
# @option options [String, Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
118+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
115119
#
116120
# @return [OneLogin::RubySaml::Settings]
117121
def parse(idp_metadata, options = {})
@@ -145,9 +149,10 @@ def parse(idp_metadata, options = {})
145149
# @param idp_metadata [String]
146150
#
147151
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
148-
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
149-
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
150-
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
152+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
153+
# @option options [String, Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
154+
# @option options [String, Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
155+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
151156
#
152157
# @return [Hash]
153158
def parse_to_hash(idp_metadata, options = {})
@@ -159,13 +164,14 @@ def parse_to_hash(idp_metadata, options = {})
159164
# @param idp_metadata [String]
160165
#
161166
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
162-
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
163-
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
164-
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, all found IdPs are returned.
167+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned.
168+
# @option options [String, Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
169+
# @option options [String, Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
170+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
165171
#
166172
# @return [Array<Hash>]
167173
def parse_to_array(idp_metadata, options = {})
168-
parse_to_idp_metadata_array(idp_metadata, options).map{|idp_md| idp_md.to_hash(options)}
174+
parse_to_idp_metadata_array(idp_metadata, options).map { |idp_md| idp_md.to_hash(options) }
169175
end
170176

171177
def parse_to_idp_metadata_array(idp_metadata, options = {})
@@ -177,9 +183,9 @@ def parse_to_idp_metadata_array(idp_metadata, options = {})
177183
raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element")
178184
end
179185

180-
return idpsso_descriptors.map{|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
186+
idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
181187
end
182-
188+
183189
private
184190

185191
# Retrieve the remote IdP metadata from the URL or a cached copy.
@@ -216,21 +222,23 @@ def get_idp_metadata(url, validate_cert)
216222

217223
class IdpMetadata
218224
attr_reader :idpsso_descriptor, :entity_id
219-
225+
220226
def initialize(idpsso_descriptor, entity_id)
221227
@idpsso_descriptor = idpsso_descriptor
222228
@entity_id = entity_id
223229
end
224230

225231
def to_hash(options = {})
232+
sso_binding = options[:sso_binding]
233+
slo_binding = options[:slo_binding]
226234
{
227235
:idp_entity_id => @entity_id,
228-
:name_identifier_format => idp_name_id_format,
229-
:idp_sso_service_url => single_signon_service_url(options),
230-
:idp_sso_service_binding => single_signon_service_binding(options[:sso_binding]),
231-
:idp_slo_service_url => single_logout_service_url(options),
232-
:idp_slo_service_binding => single_logout_service_binding(options[:slo_binding]),
233-
:idp_slo_response_service_url => single_logout_response_service_url(options),
236+
:name_identifier_format => idp_name_id_format(options[:name_id_format]),
237+
:idp_sso_service_url => single_signon_service_url(sso_binding),
238+
:idp_sso_service_binding => single_signon_service_binding(sso_binding),
239+
:idp_slo_service_url => single_logout_service_url(slo_binding),
240+
:idp_slo_service_binding => single_logout_service_binding(slo_binding),
241+
:idp_slo_response_service_url => single_logout_response_service_url(slo_binding),
234242
:idp_attribute_names => attribute_names,
235243
:idp_cert => nil,
236244
:idp_cert_fingerprint => nil,
@@ -242,17 +250,6 @@ def to_hash(options = {})
242250
end
243251
end
244252

245-
# @return [String|nil] IdP Name ID Format value if exists
246-
#
247-
def idp_name_id_format
248-
node = REXML::XPath.first(
249-
@idpsso_descriptor,
250-
"md:NameIDFormat",
251-
SamlMetadata::NAMESPACE
252-
)
253-
Utils.element_text(node)
254-
end
255-
256253
# @return [String|nil] 'validUntil' attribute of metadata
257254
#
258255
def valid_until
@@ -267,39 +264,31 @@ def cache_duration
267264
root.attributes['cacheDuration'] if root && root.attributes
268265
end
269266

270-
# @param binding_priority [Array]
271-
# @return [String|nil] SingleSignOnService binding if exists
267+
# @param name_id_priority [String|Array<String>] The prioritized list of NameIDFormat values to select. Will select first value if nil.
268+
# @return [String|nil] IdP NameIDFormat value if exists
272269
#
273-
def single_signon_service_binding(binding_priority = nil)
270+
def idp_name_id_format(name_id_priority = nil)
274271
nodes = REXML::XPath.match(
275272
@idpsso_descriptor,
276-
"md:SingleSignOnService/@Binding",
273+
"md:NameIDFormat",
277274
SamlMetadata::NAMESPACE
278275
)
279-
if binding_priority
280-
values = nodes.map(&:value)
281-
binding_priority.detect{ |binding| values.include? binding }
282-
elsif nodes.any?
283-
nodes.first.value
284-
end
276+
first_ranked_text(nodes, name_id_priority)
285277
end
286278

287-
# @param options [Hash]
288-
# @return [String|nil] SingleSignOnService endpoint if exists
279+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
280+
# @return [String|nil] SingleSignOnService binding if exists
289281
#
290-
def single_signon_service_url(options = {})
291-
binding = single_signon_service_binding(options[:sso_binding])
292-
return if binding.nil?
293-
294-
node = REXML::XPath.first(
282+
def single_signon_service_binding(binding_priority = nil)
283+
nodes = REXML::XPath.match(
295284
@idpsso_descriptor,
296-
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
285+
"md:SingleSignOnService/@Binding",
297286
SamlMetadata::NAMESPACE
298287
)
299-
return node.value if node
288+
first_ranked_value(nodes, binding_priority)
300289
end
301290

302-
# @param binding_priority [Array]
291+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
303292
# @return [String|nil] SingleLogoutService binding if exists
304293
#
305294
def single_logout_service_binding(binding_priority = nil)
@@ -308,42 +297,52 @@ def single_logout_service_binding(binding_priority = nil)
308297
"md:SingleLogoutService/@Binding",
309298
SamlMetadata::NAMESPACE
310299
)
311-
if binding_priority
312-
values = nodes.map(&:value)
313-
binding_priority.detect{ |binding| values.include? binding }
314-
elsif nodes.any?
315-
nodes.first.value
316-
end
300+
first_ranked_value(nodes, binding_priority)
301+
end
302+
303+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
304+
# @return [String|nil] SingleSignOnService endpoint if exists
305+
#
306+
def single_signon_service_url(binding_priority = nil)
307+
binding = single_signon_service_binding(binding_priority)
308+
return if binding.nil?
309+
310+
node = REXML::XPath.first(
311+
@idpsso_descriptor,
312+
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
313+
SamlMetadata::NAMESPACE
314+
)
315+
node.value if node
317316
end
318317

319-
# @param options [Hash]
318+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
320319
# @return [String|nil] SingleLogoutService endpoint if exists
321320
#
322-
def single_logout_service_url(options = {})
323-
binding = single_logout_service_binding(options[:slo_binding])
321+
def single_logout_service_url(binding_priority = nil)
322+
binding = single_logout_service_binding(binding_priority)
324323
return if binding.nil?
325324

326325
node = REXML::XPath.first(
327326
@idpsso_descriptor,
328327
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
329328
SamlMetadata::NAMESPACE
330329
)
331-
return node.value if node
330+
node.value if node
332331
end
333332

334-
# @param options [Hash]
333+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
335334
# @return [String|nil] SingleLogoutService response url if exists
336335
#
337-
def single_logout_response_service_url(options = {})
338-
binding = single_logout_service_binding(options[:slo_binding])
336+
def single_logout_response_service_url(binding_priority = nil)
337+
binding = single_logout_service_binding(binding_priority)
339338
return if binding.nil?
340339

341340
node = REXML::XPath.first(
342341
@idpsso_descriptor,
343342
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation",
344343
SamlMetadata::NAMESPACE
345344
)
346-
return node.value if node
345+
node.value if node
347346
end
348347

349348
# @return [String|nil] Unformatted Certificate if exists
@@ -434,6 +433,32 @@ def merge_certificates_into(parsed_metadata)
434433
def certificates_has_one(key)
435434
certificates.key?(key) && certificates[key].size == 1
436435
end
436+
437+
private
438+
439+
def first_ranked_text(nodes, priority = nil)
440+
return unless nodes.any?
441+
442+
priority = Array(priority)
443+
if priority.any?
444+
values = nodes.map(&:text)
445+
Array(priority).detect { |candidate| values.include?(candidate) }
446+
else
447+
nodes.first.text
448+
end
449+
end
450+
451+
def first_ranked_value(nodes, priority = nil)
452+
return unless nodes.any?
453+
454+
priority = Array(priority)
455+
if priority.any?
456+
values = nodes.map(&:value)
457+
priority.detect { |candidate| values.include?(candidate) }
458+
else
459+
nodes.first.value
460+
end
461+
end
437462
end
438463

439464
def merge_parsed_metadata_into(settings, parsed_metadata)

0 commit comments

Comments
 (0)