Skip to content

Commit

Permalink
Add #373. Allow metadata to be retrieved from source containing data …
Browse files Browse the repository at this point in the history
…for multiple entities
  • Loading branch information
pitbulk committed Apr 21, 2017
2 parents af856f6 + 45071c2 commit 95b3fc6
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 69 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,21 @@ The following attributes are set:
* idp_slo_target_url
* idp_cert_fingerprint
### Retrieve one Entity Descriptor when many exist in Metadata
If the Metadata contains several entities, the relevant Entity
Descriptor can be specified when retrieving the settings from the
IdpMetadataParser by its Entity Id value:
```ruby
validate_cert = true
settings = idp_metadata_parser.parse_remote(
"https://example.com/auth/saml2/idp/metadata",
validate_cert,
entity_id: "http//example.com/target/entity"
)
```
## Retrieving Attributes
If you are using `saml:AttributeStatement` to transfer data like the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the
Expand Down
132 changes: 73 additions & 59 deletions lib/onelogin/ruby-saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class IdpMetadataParser

attr_reader :document
attr_reader :response
attr_reader :parse_options

# Parse the Identity Provider metadata and update the settings with the
# IdP values
Expand All @@ -36,22 +37,24 @@ def parse_remote(url, validate_cert = true, options = {})
end

# Parse the Identity Provider metadata and update the settings with the IdP values
# @param idp_metadata [String]
# @param idp_metadata [String]
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
#
def parse(idp_metadata, options = {})
def parse(idp_metadata, parse_options = {})
@document = REXML::Document.new(idp_metadata)
@parse_options = parse_options
@entity_descriptor = nil

settings = options[:settings]
settings = parse_options[:settings]
if settings.nil? || settings.is_a?(Hash)
settings = OneLogin::RubySaml::Settings.new(settings || {})
end

settings.tap do |settings|
settings.idp_entity_id = idp_entity_id
settings.name_identifier_format = idp_name_id_format
settings.idp_sso_target_url = single_signon_service_url(options)
settings.idp_slo_target_url = single_logout_service_url(options)
settings.idp_sso_target_url = single_signon_service_url
settings.idp_slo_target_url = single_logout_service_url
settings.idp_cert = certificate_base64
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm)
settings.idp_attribute_names = attribute_names
Expand All @@ -67,56 +70,58 @@ def parse(idp_metadata, options = {})
# @raise [HttpError] Failure to fetch remote IdP metadata
def get_idp_metadata(url, validate_cert)
uri = URI.parse(url)
if uri.scheme == "http"
response = Net::HTTP.get_response(uri)
meta_text = response.body
elsif uri.scheme == "https"
http = Net::HTTP.new(uri.host, uri.port)
raise ArgumentError.new("url must begin with http or https") unless /^https?/ =~ uri.scheme
http = Net::HTTP.new(uri.host, uri.port)

if uri.scheme == "https"
http.use_ssl = true
# Most IdPs will probably use self signed certs
if validate_cert
http.verify_mode = OpenSSL::SSL::VERIFY_PEER

# Net::HTTP in Ruby 1.8 did not set the default certificate store
# automatically when VERIFY_PEER was specified.
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
end
else
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.verify_mode = validate_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE

# Net::HTTP in Ruby 1.8 did not set the default certificate store
# automatically when VERIFY_PEER was specified.
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
end
get = Net::HTTP::Get.new(uri.request_uri)
response = http.request(get)
meta_text = response.body
else
raise ArgumentError.new("url must begin with http or https")
end

unless response.is_a? Net::HTTPSuccess
raise OneLogin::RubySaml::HttpError.new("Failed to fetch idp metadata")
end
get = Net::HTTP::Get.new(uri.request_uri)
response = http.request(get)
return response.body if response.is_a? Net::HTTPSuccess

raise OneLogin::RubySaml::HttpError.new(
"Failed to fetch idp metadata: #{response.code}: #{response.message}"
)
end

def entity_descriptor
@entity_descriptor ||= REXML::XPath.first(
document,
entity_descriptor_path,
namespace
)
end

meta_text
def entity_descriptor_path
path = "//md:EntityDescriptor"
entity_id = parse_options[:entity_id]
return path unless entity_id
path << "[@entityID=\"#{entity_id}\"]"
end

# @return [String|nil] IdP Entity ID value if exists
#
def idp_entity_id
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/@entityID",
{ "md" => METADATA }
)
node.value if node
entity_descriptor.attributes["entityID"]
end

# @return [String|nil] IdP Name ID Format value if exists
#
def idp_name_id_format
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:NameIDFormat",
namespace
)
node.text if node
end
Expand All @@ -126,9 +131,9 @@ def idp_name_id_format
#
def single_signon_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
namespace
)
if binding_priority
values = nodes.map(&:value)
Expand All @@ -145,9 +150,9 @@ def single_signon_service_url(options = {})
binding = single_signon_service_binding(options[:sso_binding])
unless binding.nil?
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
namespace
)
return node.value if node
end
Expand All @@ -158,9 +163,9 @@ def single_signon_service_url(options = {})
#
def single_logout_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
namespace
)
if binding_priority
values = nodes.map(&:value)
Expand All @@ -177,9 +182,9 @@ def single_logout_service_url(options = {})
binding = single_logout_service_binding(options[:slo_binding])
unless binding.nil?
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
namespace
)
return node.value if node
end
Expand All @@ -190,16 +195,16 @@ def single_logout_service_url(options = {})
def certificate_base64
@certificate_base64 ||= begin
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
{ "md" => METADATA, "ds" => DSIG }
entity_descriptor,
"md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
namespace
)

unless node
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
{ "md" => METADATA, "ds" => DSIG }
entity_descriptor,
"md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
namespace
)
end
node.text if node
Expand Down Expand Up @@ -232,12 +237,21 @@ def fingerprint(fingerprint_algorithm = XMLSecurity::Document::SHA1)
#
def attribute_names
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/saml:Attribute/@Name",
{ "md" => METADATA, "NameFormat" => NAME_FORMAT, "saml" => SAML_ASSERTION }
entity_descriptor,
"md:IDPSSODescriptor/saml:Attribute/@Name",
namespace
)
nodes.map(&:value)
end

def namespace
{
"md" => METADATA,
"NameFormat" => NAME_FORMAT,
"saml" => SAML_ASSERTION,
"ds" => DSIG
}
end
end
end
end
44 changes: 36 additions & 8 deletions test/idp_metadata_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ def initialize; end

settings = idp_metadata_parser.parse(idp_metadata)

assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
Expand Down Expand Up @@ -90,10 +90,10 @@ def initialize; end
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
settings = idp_metadata_parser.parse_remote(@url)

assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
Expand Down Expand Up @@ -130,10 +130,38 @@ def initialize; end
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new

exception = assert_raises(OneLogin::RubySaml::HttpError) do
idp_metadata_parser.parse_remote("https://example.hello.com/access/saml/idp.xml")
idp_metadata_parser.parse_remote("https://hello.example.com/access/saml/idp.xml")
end

assert_equal("Failed to fetch idp metadata", exception.message)
assert_match("Failed to fetch idp metadata", exception.message)
end
end

describe "parsing metadata with many entity descriptors" do
before do
@idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
@idp_metadata = read_response("idp_multiple_descriptors.xml")
@settings = @idp_metadata_parser.parse(@idp_metadata)
end

it "should find first descriptor" do
assert_equal "https://foo.example.com/access/saml/idp.xml", @settings.idp_entity_id
end

it "should find named descriptor" do
entity_id = "https://bar.example.com/access/saml/idp.xml"
settings = @idp_metadata_parser.parse(
@idp_metadata, :entity_id => entity_id
)
assert_equal entity_id, settings.idp_entity_id
end

it "should retreive data" do
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", @settings.name_identifier_format
assert_equal "https://hello.example.com/access/saml/login", @settings.idp_sso_target_url
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", @settings.idp_cert_fingerprint
assert_equal "https://hello.example.com/access/saml/logout", @settings.idp_slo_target_url
assert_equal ["AuthToken", "SSOStartPage"], @settings.idp_attribute_names
end
end
end
Loading

0 comments on commit 95b3fc6

Please sign in to comment.