Skip to content

Commit 95b3fc6

Browse files
committed
Add #373. Allow metadata to be retrieved from source containing data for multiple entities
2 parents af856f6 + 45071c2 commit 95b3fc6

File tree

5 files changed

+205
-69
lines changed

5 files changed

+205
-69
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,21 @@ The following attributes are set:
304304
* idp_slo_target_url
305305
* idp_cert_fingerprint
306306
307+
### Retrieve one Entity Descriptor when many exist in Metadata
308+
309+
If the Metadata contains several entities, the relevant Entity
310+
Descriptor can be specified when retrieving the settings from the
311+
IdpMetadataParser by its Entity Id value:
312+
313+
```ruby
314+
validate_cert = true
315+
settings = idp_metadata_parser.parse_remote(
316+
"https://example.com/auth/saml2/idp/metadata",
317+
validate_cert,
318+
entity_id: "http//example.com/target/entity"
319+
)
320+
```
321+
307322
## Retrieving Attributes
308323
309324
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

lib/onelogin/ruby-saml/idp_metadata_parser.rb

Lines changed: 73 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class IdpMetadataParser
2222

2323
attr_reader :document
2424
attr_reader :response
25+
attr_reader :parse_options
2526

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

3839
# Parse the Identity Provider metadata and update the settings with the IdP values
39-
# @param idp_metadata [String]
40+
# @param idp_metadata [String]
4041
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
4142
#
42-
def parse(idp_metadata, options = {})
43+
def parse(idp_metadata, parse_options = {})
4344
@document = REXML::Document.new(idp_metadata)
45+
@parse_options = parse_options
46+
@entity_descriptor = nil
4447

45-
settings = options[:settings]
48+
settings = parse_options[:settings]
4649
if settings.nil? || settings.is_a?(Hash)
4750
settings = OneLogin::RubySaml::Settings.new(settings || {})
4851
end
4952

5053
settings.tap do |settings|
5154
settings.idp_entity_id = idp_entity_id
5255
settings.name_identifier_format = idp_name_id_format
53-
settings.idp_sso_target_url = single_signon_service_url(options)
54-
settings.idp_slo_target_url = single_logout_service_url(options)
56+
settings.idp_sso_target_url = single_signon_service_url
57+
settings.idp_slo_target_url = single_logout_service_url
5558
settings.idp_cert = certificate_base64
5659
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm)
5760
settings.idp_attribute_names = attribute_names
@@ -67,56 +70,58 @@ def parse(idp_metadata, options = {})
6770
# @raise [HttpError] Failure to fetch remote IdP metadata
6871
def get_idp_metadata(url, validate_cert)
6972
uri = URI.parse(url)
70-
if uri.scheme == "http"
71-
response = Net::HTTP.get_response(uri)
72-
meta_text = response.body
73-
elsif uri.scheme == "https"
74-
http = Net::HTTP.new(uri.host, uri.port)
73+
raise ArgumentError.new("url must begin with http or https") unless /^https?/ =~ uri.scheme
74+
http = Net::HTTP.new(uri.host, uri.port)
75+
76+
if uri.scheme == "https"
7577
http.use_ssl = true
7678
# Most IdPs will probably use self signed certs
77-
if validate_cert
78-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
79-
80-
# Net::HTTP in Ruby 1.8 did not set the default certificate store
81-
# automatically when VERIFY_PEER was specified.
82-
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
83-
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
84-
end
85-
else
86-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
79+
http.verify_mode = validate_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
80+
81+
# Net::HTTP in Ruby 1.8 did not set the default certificate store
82+
# automatically when VERIFY_PEER was specified.
83+
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
84+
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
8785
end
88-
get = Net::HTTP::Get.new(uri.request_uri)
89-
response = http.request(get)
90-
meta_text = response.body
91-
else
92-
raise ArgumentError.new("url must begin with http or https")
9386
end
9487

95-
unless response.is_a? Net::HTTPSuccess
96-
raise OneLogin::RubySaml::HttpError.new("Failed to fetch idp metadata")
97-
end
88+
get = Net::HTTP::Get.new(uri.request_uri)
89+
response = http.request(get)
90+
return response.body if response.is_a? Net::HTTPSuccess
91+
92+
raise OneLogin::RubySaml::HttpError.new(
93+
"Failed to fetch idp metadata: #{response.code}: #{response.message}"
94+
)
95+
end
96+
97+
def entity_descriptor
98+
@entity_descriptor ||= REXML::XPath.first(
99+
document,
100+
entity_descriptor_path,
101+
namespace
102+
)
103+
end
98104

99-
meta_text
105+
def entity_descriptor_path
106+
path = "//md:EntityDescriptor"
107+
entity_id = parse_options[:entity_id]
108+
return path unless entity_id
109+
path << "[@entityID=\"#{entity_id}\"]"
100110
end
101111

102112
# @return [String|nil] IdP Entity ID value if exists
103113
#
104114
def idp_entity_id
105-
node = REXML::XPath.first(
106-
document,
107-
"/md:EntityDescriptor/@entityID",
108-
{ "md" => METADATA }
109-
)
110-
node.value if node
115+
entity_descriptor.attributes["entityID"]
111116
end
112117

113118
# @return [String|nil] IdP Name ID Format value if exists
114119
#
115120
def idp_name_id_format
116121
node = REXML::XPath.first(
117-
document,
118-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
119-
{ "md" => METADATA }
122+
entity_descriptor,
123+
"md:IDPSSODescriptor/md:NameIDFormat",
124+
namespace
120125
)
121126
node.text if node
122127
end
@@ -126,9 +131,9 @@ def idp_name_id_format
126131
#
127132
def single_signon_service_binding(binding_priority = nil)
128133
nodes = REXML::XPath.match(
129-
document,
130-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
131-
{ "md" => METADATA }
134+
entity_descriptor,
135+
"md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
136+
namespace
132137
)
133138
if binding_priority
134139
values = nodes.map(&:value)
@@ -145,9 +150,9 @@ def single_signon_service_url(options = {})
145150
binding = single_signon_service_binding(options[:sso_binding])
146151
unless binding.nil?
147152
node = REXML::XPath.first(
148-
document,
149-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
150-
{ "md" => METADATA }
153+
entity_descriptor,
154+
"md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
155+
namespace
151156
)
152157
return node.value if node
153158
end
@@ -158,9 +163,9 @@ def single_signon_service_url(options = {})
158163
#
159164
def single_logout_service_binding(binding_priority = nil)
160165
nodes = REXML::XPath.match(
161-
document,
162-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
163-
{ "md" => METADATA }
166+
entity_descriptor,
167+
"md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
168+
namespace
164169
)
165170
if binding_priority
166171
values = nodes.map(&:value)
@@ -177,9 +182,9 @@ def single_logout_service_url(options = {})
177182
binding = single_logout_service_binding(options[:slo_binding])
178183
unless binding.nil?
179184
node = REXML::XPath.first(
180-
document,
181-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
182-
{ "md" => METADATA }
185+
entity_descriptor,
186+
"md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
187+
namespace
183188
)
184189
return node.value if node
185190
end
@@ -190,16 +195,16 @@ def single_logout_service_url(options = {})
190195
def certificate_base64
191196
@certificate_base64 ||= begin
192197
node = REXML::XPath.first(
193-
document,
194-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
195-
{ "md" => METADATA, "ds" => DSIG }
198+
entity_descriptor,
199+
"md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
200+
namespace
196201
)
197202

198203
unless node
199204
node = REXML::XPath.first(
200-
document,
201-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
202-
{ "md" => METADATA, "ds" => DSIG }
205+
entity_descriptor,
206+
"md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
207+
namespace
203208
)
204209
end
205210
node.text if node
@@ -232,12 +237,21 @@ def fingerprint(fingerprint_algorithm = XMLSecurity::Document::SHA1)
232237
#
233238
def attribute_names
234239
nodes = REXML::XPath.match(
235-
document,
236-
"/md:EntityDescriptor/md:IDPSSODescriptor/saml:Attribute/@Name",
237-
{ "md" => METADATA, "NameFormat" => NAME_FORMAT, "saml" => SAML_ASSERTION }
240+
entity_descriptor,
241+
"md:IDPSSODescriptor/saml:Attribute/@Name",
242+
namespace
238243
)
239244
nodes.map(&:value)
240245
end
246+
247+
def namespace
248+
{
249+
"md" => METADATA,
250+
"NameFormat" => NAME_FORMAT,
251+
"saml" => SAML_ASSERTION,
252+
"ds" => DSIG
253+
}
254+
end
241255
end
242256
end
243257
end

test/idp_metadata_parser_test.rb

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ def initialize; end
2323

2424
settings = idp_metadata_parser.parse(idp_metadata)
2525

26-
assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
27-
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
26+
assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
27+
assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
2828
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
29-
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
29+
assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
3030
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
3131
assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
3232
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
@@ -90,10 +90,10 @@ def initialize; end
9090
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
9191
settings = idp_metadata_parser.parse_remote(@url)
9292

93-
assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
94-
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
93+
assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
94+
assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
9595
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
96-
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
96+
assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
9797
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
9898
assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
9999
assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
@@ -130,10 +130,38 @@ def initialize; end
130130
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
131131

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

136-
assert_equal("Failed to fetch idp metadata", exception.message)
136+
assert_match("Failed to fetch idp metadata", exception.message)
137+
end
138+
end
139+
140+
describe "parsing metadata with many entity descriptors" do
141+
before do
142+
@idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
143+
@idp_metadata = read_response("idp_multiple_descriptors.xml")
144+
@settings = @idp_metadata_parser.parse(@idp_metadata)
145+
end
146+
147+
it "should find first descriptor" do
148+
assert_equal "https://foo.example.com/access/saml/idp.xml", @settings.idp_entity_id
149+
end
150+
151+
it "should find named descriptor" do
152+
entity_id = "https://bar.example.com/access/saml/idp.xml"
153+
settings = @idp_metadata_parser.parse(
154+
@idp_metadata, :entity_id => entity_id
155+
)
156+
assert_equal entity_id, settings.idp_entity_id
157+
end
158+
159+
it "should retreive data" do
160+
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", @settings.name_identifier_format
161+
assert_equal "https://hello.example.com/access/saml/login", @settings.idp_sso_target_url
162+
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
163+
assert_equal "https://hello.example.com/access/saml/logout", @settings.idp_slo_target_url
164+
assert_equal ["AuthToken", "SSOStartPage"], @settings.idp_attribute_names
137165
end
138166
end
139167
end

0 commit comments

Comments
 (0)