Skip to content

Commit

Permalink
Merge pull request #614 from johnnyshields/idp-parser-name_id_format
Browse files Browse the repository at this point in the history
Support :name_id_format option for IdpMetadataParser
  • Loading branch information
pitbulk authored Aug 18, 2021
2 parents 993fc10 + 38e0f3c commit 4f49095
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 75 deletions.
173 changes: 99 additions & 74 deletions lib/onelogin/ruby-saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def self.get_idps(metadata_document, only_entity_id=nil)
SamlMetadata::NAMESPACE
)
end

# Parse the Identity Provider metadata and update the settings with the
# IdP values
#
Expand All @@ -56,9 +56,10 @@ def self.get_idps(metadata_document, only_entity_id=nil)
#
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
#
# @return [OneLogin::RubySaml::Settings]
#
Expand All @@ -74,9 +75,10 @@ def parse_remote(url, validate_cert = true, options = {})
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
#
# @param options [Hash] options used for parsing the metadata
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
#
# @return [Hash]
#
Expand All @@ -91,9 +93,10 @@ def parse_remote_to_hash(url, validate_cert = true, options = {})
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
#
# @param options [Hash] options used for parsing the metadata
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
#
# @return [Array<Hash>]
#
Expand All @@ -109,9 +112,10 @@ def parse_remote_to_array(url, validate_cert = true, options = {})
#
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
#
# @return [OneLogin::RubySaml::Settings]
def parse(idp_metadata, options = {})
Expand Down Expand Up @@ -145,9 +149,10 @@ def parse(idp_metadata, options = {})
# @param idp_metadata [String]
#
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
#
# @return [Hash]
def parse_to_hash(idp_metadata, options = {})
Expand All @@ -159,13 +164,14 @@ def parse_to_hash(idp_metadata, options = {})
# @param idp_metadata [String]
#
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
# @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.
#
# @return [Array<Hash>]
def parse_to_array(idp_metadata, options = {})
parse_to_idp_metadata_array(idp_metadata, options).map{|idp_md| idp_md.to_hash(options)}
parse_to_idp_metadata_array(idp_metadata, options).map { |idp_md| idp_md.to_hash(options) }
end

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

return idpsso_descriptors.map{|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
end

private

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

class IdpMetadata
attr_reader :idpsso_descriptor, :entity_id

def initialize(idpsso_descriptor, entity_id)
@idpsso_descriptor = idpsso_descriptor
@entity_id = entity_id
end

def to_hash(options = {})
sso_binding = options[:sso_binding]
slo_binding = options[:slo_binding]
{
:idp_entity_id => @entity_id,
:name_identifier_format => idp_name_id_format,
:idp_sso_service_url => single_signon_service_url(options),
:idp_sso_service_binding => single_signon_service_binding(options[:sso_binding]),
:idp_slo_service_url => single_logout_service_url(options),
:idp_slo_service_binding => single_logout_service_binding(options[:slo_binding]),
:idp_slo_response_service_url => single_logout_response_service_url(options),
:name_identifier_format => idp_name_id_format(options[:name_id_format]),
:idp_sso_service_url => single_signon_service_url(sso_binding),
:idp_sso_service_binding => single_signon_service_binding(sso_binding),
:idp_slo_service_url => single_logout_service_url(slo_binding),
:idp_slo_service_binding => single_logout_service_binding(slo_binding),
:idp_slo_response_service_url => single_logout_response_service_url(slo_binding),
:idp_attribute_names => attribute_names,
:idp_cert => nil,
:idp_cert_fingerprint => nil,
Expand All @@ -242,17 +250,6 @@ def to_hash(options = {})
end
end

# @return [String|nil] IdP Name ID Format value if exists
#
def idp_name_id_format
node = REXML::XPath.first(
@idpsso_descriptor,
"md:NameIDFormat",
SamlMetadata::NAMESPACE
)
Utils.element_text(node)
end

# @return [String|nil] 'validUntil' attribute of metadata
#
def valid_until
Expand All @@ -267,39 +264,31 @@ def cache_duration
root.attributes['cacheDuration'] if root && root.attributes
end

# @param binding_priority [Array]
# @return [String|nil] SingleSignOnService binding if exists
# @param name_id_priority [String|Array<String>] The prioritized list of NameIDFormat values to select. Will select first value if nil.
# @return [String|nil] IdP NameIDFormat value if exists
#
def single_signon_service_binding(binding_priority = nil)
def idp_name_id_format(name_id_priority = nil)
nodes = REXML::XPath.match(
@idpsso_descriptor,
"md:SingleSignOnService/@Binding",
"md:NameIDFormat",
SamlMetadata::NAMESPACE
)
if binding_priority
values = nodes.map(&:value)
binding_priority.detect{ |binding| values.include? binding }
elsif nodes.any?
nodes.first.value
end
first_ranked_text(nodes, name_id_priority)
end

# @param options [Hash]
# @return [String|nil] SingleSignOnService endpoint if exists
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
# @return [String|nil] SingleSignOnService binding if exists
#
def single_signon_service_url(options = {})
binding = single_signon_service_binding(options[:sso_binding])
return if binding.nil?

node = REXML::XPath.first(
def single_signon_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
@idpsso_descriptor,
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
"md:SingleSignOnService/@Binding",
SamlMetadata::NAMESPACE
)
return node.value if node
first_ranked_value(nodes, binding_priority)
end

# @param binding_priority [Array]
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
# @return [String|nil] SingleLogoutService binding if exists
#
def single_logout_service_binding(binding_priority = nil)
Expand All @@ -308,42 +297,52 @@ def single_logout_service_binding(binding_priority = nil)
"md:SingleLogoutService/@Binding",
SamlMetadata::NAMESPACE
)
if binding_priority
values = nodes.map(&:value)
binding_priority.detect{ |binding| values.include? binding }
elsif nodes.any?
nodes.first.value
end
first_ranked_value(nodes, binding_priority)
end

# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
# @return [String|nil] SingleSignOnService endpoint if exists
#
def single_signon_service_url(binding_priority = nil)
binding = single_signon_service_binding(binding_priority)
return if binding.nil?

node = REXML::XPath.first(
@idpsso_descriptor,
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
SamlMetadata::NAMESPACE
)
node.value if node
end

# @param options [Hash]
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
# @return [String|nil] SingleLogoutService endpoint if exists
#
def single_logout_service_url(options = {})
binding = single_logout_service_binding(options[:slo_binding])
def single_logout_service_url(binding_priority = nil)
binding = single_logout_service_binding(binding_priority)
return if binding.nil?

node = REXML::XPath.first(
@idpsso_descriptor,
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
SamlMetadata::NAMESPACE
)
return node.value if node
node.value if node
end

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

node = REXML::XPath.first(
@idpsso_descriptor,
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation",
SamlMetadata::NAMESPACE
)
return node.value if node
node.value if node
end

# @return [String|nil] Unformatted Certificate if exists
Expand Down Expand Up @@ -434,6 +433,32 @@ def merge_certificates_into(parsed_metadata)
def certificates_has_one(key)
certificates.key?(key) && certificates[key].size == 1
end

private

def first_ranked_text(nodes, priority = nil)
return unless nodes.any?

priority = Array(priority)
if priority.any?
values = nodes.map(&:text)
Array(priority).detect { |candidate| values.include?(candidate) }
else
nodes.first.text
end
end

def first_ranked_value(nodes, priority = nil)
return unless nodes.any?

priority = Array(priority)
if priority.any?
values = nodes.map(&:value)
priority.detect { |candidate| values.include?(candidate) }
else
nodes.first.value
end
end
end

def merge_parsed_metadata_into(settings, parsed_metadata)
Expand Down
Loading

0 comments on commit 4f49095

Please # to comment.