Skip to content

Commit e85e8bd

Browse files
author
Rob Nichols
committed
Allow Entity Descriptor to be selected when many exist in Metadata.
1 parent 027acb8 commit e85e8bd

File tree

5 files changed

+190
-46
lines changed

5 files changed

+190
-46
lines changed

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,21 @@ The following attributes are set:
279279
* idp_slo_target_url
280280
* idp_cert_fingerprint
281281
282+
### Retrieve one Entity Descriptor when many exist in Metadata
283+
284+
If the Meta data contains the data for many SAML entities, the relevant Entity
285+
Descriptor can be specified when retrieving the settings from the
286+
IdpMetadataParser:
287+
288+
```ruby
289+
validate_cert = true
290+
settings = idp_metadata_parser.parse_remote(
291+
"https://example.com/auth/saml2/idp/metadata",
292+
validate_cert,
293+
entity_id: "http//example.com/target/entity"
294+
)
295+
```
296+
282297
## Retrieving Attributes
283298
284299
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
@@ -411,9 +426,9 @@ The settings related to sign are stored in the `security` attribute of the setti
411426
```ruby
412427
settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest
413428
settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request
414-
settings.security[:logout_responses_signed] = true # Enable or not
429+
settings.security[:logout_responses_signed] = true # Enable or not
415430
signature on Logout Response
416-
settings.security[:want_assertions_signed] = true # Enable or not
431+
settings.security[:want_assertions_signed] = true # Enable or not
417432
the requirement of signed assertion
418433
settings.security[:metadata_signed] = true # Enable or not signature on Metadata
419434
@@ -426,7 +441,7 @@ The settings related to sign are stored in the `security` attribute of the setti
426441
```
427442
428443
Notice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding.
429-
Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
444+
Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
430445
signature validation process will fail at the Identity Provider.
431446
432447
The Service Provider will sign the request/responses with its private key.

lib/onelogin/ruby-saml/idp_metadata_parser.rb

Lines changed: 56 additions & 34 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
@@ -39,19 +40,21 @@ def parse_remote(url, validate_cert = true, options = {})
3940
# @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(parse_options)
57+
settings.idp_slo_target_url = single_logout_service_url(parse_options)
5558
settings.idp_cert = certificate_base64
5659
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm)
5760
settings.idp_attribute_names = attribute_names
@@ -91,24 +94,34 @@ def get_idp_metadata(url, validate_cert)
9194
)
9295
end
9396

97+
def entity_descriptor
98+
@entity_descriptor ||= REXML::XPath.first(
99+
document,
100+
entity_descriptor_path,
101+
namespace
102+
)
103+
end
104+
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}\"]"
110+
end
111+
94112
# @return [String|nil] IdP Entity ID value if exists
95113
#
96114
def idp_entity_id
97-
node = REXML::XPath.first(
98-
document,
99-
"/md:EntityDescriptor/@entityID",
100-
{ "md" => METADATA }
101-
)
102-
node.value if node
115+
entity_descriptor.attributes["entityID"]
103116
end
104117

105118
# @return [String|nil] IdP Name ID Format value if exists
106119
#
107120
def idp_name_id_format
108121
node = REXML::XPath.first(
109-
document,
110-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
111-
{ "md" => METADATA }
122+
entity_descriptor,
123+
"md:IDPSSODescriptor/md:NameIDFormat",
124+
namespace
112125
)
113126
node.text if node
114127
end
@@ -118,9 +131,9 @@ def idp_name_id_format
118131
#
119132
def single_signon_service_binding(binding_priority = nil)
120133
nodes = REXML::XPath.match(
121-
document,
122-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
123-
{ "md" => METADATA }
134+
entity_descriptor,
135+
"md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
136+
namespace
124137
)
125138
if binding_priority
126139
values = nodes.map(&:value)
@@ -136,9 +149,9 @@ def single_signon_service_binding(binding_priority = nil)
136149
def single_signon_service_url(options = {})
137150
binding = options[:sso_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
138151
node = REXML::XPath.first(
139-
document,
140-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
141-
{ "md" => METADATA }
152+
entity_descriptor,
153+
"md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
154+
namespace
142155
)
143156
node.value if node
144157
end
@@ -148,9 +161,9 @@ def single_signon_service_url(options = {})
148161
#
149162
def single_logout_service_binding(binding_priority = nil)
150163
nodes = REXML::XPath.match(
151-
document,
152-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
153-
{ "md" => METADATA }
164+
entity_descriptor,
165+
"md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
166+
namespace
154167
)
155168
if binding_priority
156169
values = nodes.map(&:value)
@@ -166,9 +179,9 @@ def single_logout_service_binding(binding_priority = nil)
166179
def single_logout_service_url(options = {})
167180
binding = options[:slo_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
168181
node = REXML::XPath.first(
169-
document,
170-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
171-
{ "md" => METADATA }
182+
entity_descriptor,
183+
"md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
184+
namespace
172185
)
173186
node.value if node
174187
end
@@ -178,16 +191,16 @@ def single_logout_service_url(options = {})
178191
def certificate_base64
179192
@certificate_base64 ||= begin
180193
node = REXML::XPath.first(
181-
document,
182-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
183-
{ "md" => METADATA, "ds" => DSIG }
194+
entity_descriptor,
195+
"md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
196+
namespace
184197
)
185198

186199
unless node
187200
node = REXML::XPath.first(
188-
document,
189-
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
190-
{ "md" => METADATA, "ds" => DSIG }
201+
entity_descriptor,
202+
"md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
203+
namespace
191204
)
192205
end
193206
node.text if node
@@ -220,12 +233,21 @@ def fingerprint(fingerprint_algorithm = XMLSecurity::Document::SHA1)
220233
#
221234
def attribute_names
222235
nodes = REXML::XPath.match(
223-
document,
224-
"/md:EntityDescriptor/md:IDPSSODescriptor/saml:Attribute/@Name",
225-
{ "md" => METADATA, "NameFormat" => NAME_FORMAT, "saml" => SAML_ASSERTION }
236+
entity_descriptor,
237+
"md:IDPSSODescriptor/saml:Attribute/@Name",
238+
namespace
226239
)
227240
nodes.map(&:value)
228241
end
242+
243+
def namespace
244+
{
245+
"md" => METADATA,
246+
"NameFormat" => NAME_FORMAT,
247+
"saml" => SAML_ASSERTION,
248+
"ds" => DSIG
249+
}
250+
end
229251
end
230252
end
231253
end

test/idp_metadata_parser_test.rb

Lines changed: 35 additions & 7 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

136136
assert_match("Failed to fetch idp metadata", exception.message)
137137
end
138138
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
165+
end
166+
end
139167
end

0 commit comments

Comments
 (0)