Skip to content

Commit c3321a1

Browse files
author
Torsten Schoenebaum
committed
Implement IdpMetadataParser#parse_to_hash
and IdpMetadataParser#parse_remote_to_hash. Having the parsed metadata as Hash may be useful for configuring omniauth-saml, for instance.
1 parent 319d5ec commit c3321a1

File tree

3 files changed

+217
-33
lines changed

3 files changed

+217
-33
lines changed

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ And on 'signing' and 'encryption' arrays, add the different IdP x509 public cert
281281
The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application) There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public
282282
key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata.
283283
284-
Using ```idp_metadata_parser.parse_remote``` IdP metadata will be added to the settings withouth further ado.
284+
Using ```idp_metadata_parser.parse_remote``` IdP metadata will be added to the settings without further ado.
285285
286286
```ruby
287287
def saml_settings
@@ -300,9 +300,14 @@ def saml_settings
300300
end
301301
```
302302
The following attributes are set:
303+
* idp_entity_id
304+
* name_identifier_format
303305
* idp_sso_target_url
304306
* idp_slo_target_url
305-
* idp_cert_fingerprint
307+
* idp_attribute_names
308+
* idp_cert
309+
* idp_cert_fingerprint
310+
* idp_cert_multi
306311
307312
### Retrieve one Entity Descriptor when many exist in Metadata
308313
@@ -319,6 +324,12 @@ IdpMetadataParser by its Entity Id value:
319324
)
320325
```
321326
327+
### Parsing Metadata into an Hash
328+
329+
The `OneLogin::RubySaml::IdpMetadataParser` also provides the methods `#parse_to_hash` and `#parse_remote_to_hash`.
330+
Those return an Hash instead of a `Settings` object, which may be useful for configuring
331+
[omniauth-saml](https://github.com/omniauth/omniauth-saml), for instance.
332+
322333
## Retrieving Attributes
323334
324335
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: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,51 +28,69 @@ class IdpMetadataParser
2828
# IdP values
2929
#
3030
# @param (see IdpMetadataParser#get_idp_metadata)
31-
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
31+
# @param parse_options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
3232
# @return (see IdpMetadataParser#get_idp_metadata)
3333
# @raise (see IdpMetadataParser#get_idp_metadata)
34-
def parse_remote(url, validate_cert = true, options = {})
34+
def parse_remote(url, validate_cert = true, parse_options = {})
3535
idp_metadata = get_idp_metadata(url, validate_cert)
36-
parse(idp_metadata, options)
36+
parse(idp_metadata, parse_options)
37+
end
38+
39+
# Parse the Identity Provider metadata and return the results as Hash
40+
#
41+
# @param url [String] Url where the XML of the Identity Provider Metadata is published.
42+
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
43+
# @param parse_options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
44+
# @return [Hash]
45+
# @raise [HttpError] Failure to fetch remote IdP metadata
46+
def parse_remote_to_hash(url, validate_cert = true, parse_options = {})
47+
idp_metadata = get_idp_metadata(url, validate_cert)
48+
parse_to_hash(idp_metadata, parse_options)
3749
end
3850

3951
# Parse the Identity Provider metadata and update the settings with the IdP values
52+
#
4053
# @param idp_metadata [String]
41-
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
54+
# @param parse_options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
4255
#
56+
# @return [Settings]
4357
def parse(idp_metadata, parse_options = {})
44-
@document = REXML::Document.new(idp_metadata)
45-
@parse_options = parse_options
46-
@entity_descriptor = nil
58+
parsed_metadata = parse_to_hash(idp_metadata, parse_options)
4759

4860
settings = parse_options[:settings]
49-
if settings.nil? || settings.is_a?(Hash)
50-
settings = OneLogin::RubySaml::Settings.new(settings || {})
61+
62+
if settings.nil?
63+
OneLogin::RubySaml::Settings.new(parsed_metadata)
64+
elsif settings.is_a?(Hash)
65+
OneLogin::RubySaml::Settings.new(settings.merge(parsed_metadata))
66+
else
67+
merge_parsed_metadata_into(settings, parsed_metadata)
5168
end
69+
end
5270

53-
settings.idp_entity_id = idp_entity_id
54-
settings.name_identifier_format = idp_name_id_format
55-
settings.idp_sso_target_url = single_signon_service_url(parse_options)
56-
settings.idp_slo_target_url = single_logout_service_url(parse_options)
57-
settings.idp_attribute_names = attribute_names
58-
59-
settings.idp_cert = nil
60-
settings.idp_cert_fingerprint = nil
61-
settings.idp_cert_multi = nil
62-
unless certificates.nil?
63-
if certificates.size == 1 || ((certificates.key?("signing") && certificates["signing"].size == 1) && (certificates.key?("encryption") && certificates["encryption"].size == 1) && certificates["signing"][0] == certificates["encryption"][0])
64-
if certificates.key?("signing")
65-
settings.idp_cert = certificates["signing"][0]
66-
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert, settings.idp_cert_fingerprint_algorithm)
67-
else
68-
settings.idp_cert = certificates["encryption"][0]
69-
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert, settings.idp_cert_fingerprint_algorithm)
70-
end
71-
else
72-
settings.idp_cert_multi = certificates
73-
end
71+
# Parse the Identity Provider metadata and return the results as Hash
72+
#
73+
# @param idp_metadata [String]
74+
# @param parse_options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
75+
#
76+
# @return [Settings]
77+
def parse_to_hash(idp_metadata, parse_options = {})
78+
@document = REXML::Document.new(idp_metadata)
79+
@parse_options = parse_options
80+
@entity_descriptor = nil
81+
82+
{
83+
:idp_entity_id => idp_entity_id,
84+
:name_identifier_format => idp_name_id_format,
85+
:idp_sso_target_url => single_signon_service_url(parse_options),
86+
:idp_slo_target_url => single_logout_service_url(parse_options),
87+
:idp_attribute_names => attribute_names,
88+
:idp_cert => nil,
89+
:idp_cert_fingerprint => nil,
90+
:idp_cert_multi => nil
91+
}.tap do |response_hash|
92+
merge_certificates_into(response_hash) unless certificates.nil?
7493
end
75-
settings
7694
end
7795

7896
private
@@ -273,6 +291,38 @@ def namespace
273291
"ds" => DSIG
274292
}
275293
end
294+
295+
def merge_certificates_into(parsed_metadata)
296+
if certificates.size == 1 ||
297+
((certificates.key?("signing") && certificates["signing"].size == 1) &&
298+
(certificates.key?("encryption") && certificates["encryption"].size == 1) &&
299+
certificates["signing"][0] == certificates["encryption"][0])
300+
301+
if certificates.key?("signing")
302+
parsed_metadata[:idp_cert] = certificates["signing"][0]
303+
parsed_metadata[:idp_cert_fingerprint] = fingerprint(
304+
parsed_metadata[:idp_cert],
305+
parsed_metadata[:idp_cert_fingerprint_algorithm]
306+
)
307+
else
308+
parsed_metadata[:idp_cert] = certificates["encryption"][0]
309+
parsed_metadata[:idp_cert_fingerprint] = fingerprint(
310+
parsed_metadata[:idp_cert],
311+
parsed_metadata[:idp_cert_fingerprint_algorithm]
312+
)
313+
end
314+
else
315+
parsed_metadata[:idp_cert_multi] = certificates
316+
end
317+
end
318+
319+
def merge_parsed_metadata_into(settings, parsed_metadata)
320+
parsed_metadata.each do |key, value|
321+
settings.send("#{key}=".to_sym, value)
322+
end
323+
324+
settings
325+
end
276326
end
277327
end
278328
end

test/idp_metadata_parser_test.rb

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,96 @@ def initialize; end
9595
assert_equal XMLSecurity::Document::RSA_SHA256, settings.security[:signature_method]
9696
end
9797

98+
it "merges results into given settings object" do
99+
settings = OneLogin::RubySaml::Settings.new(:security => {
100+
:digest_method => XMLSecurity::Document::SHA256,
101+
:signature_method => XMLSecurity::Document::RSA_SHA256
102+
})
103+
104+
OneLogin::RubySaml::IdpMetadataParser.new.parse(idp_metadata_descriptor, :settings => settings)
105+
106+
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
107+
assert_equal XMLSecurity::Document::SHA256, settings.security[:digest_method]
108+
assert_equal XMLSecurity::Document::RSA_SHA256, settings.security[:signature_method]
109+
end
110+
end
111+
112+
describe "parsing an IdP descriptor file into an Hash" do
113+
it "extract settings details from xml" do
114+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
115+
116+
metadata = idp_metadata_parser.parse_to_hash(idp_metadata_descriptor)
117+
118+
assert_equal "https://hello.example.com/access/saml/idp.xml", metadata[:idp_entity_id]
119+
assert_equal "https://hello.example.com/access/saml/login", metadata[:idp_sso_target_url]
120+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", metadata[:idp_cert_fingerprint]
121+
assert_equal "https://hello.example.com/access/saml/logout", metadata[:idp_slo_target_url]
122+
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", metadata[:name_identifier_format]
123+
assert_equal ["AuthToken", "SSOStartPage"], metadata[:idp_attribute_names]
124+
end
125+
126+
it "extract certificate from md:KeyDescriptor[@use='signing']" do
127+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
128+
idp_metadata = idp_metadata_descriptor
129+
metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
130+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", metadata[:idp_cert_fingerprint]
131+
end
132+
133+
it "extract certificate from md:KeyDescriptor[@use='encryption']" do
134+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
135+
idp_metadata = idp_metadata_descriptor
136+
idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
137+
parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
138+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
139+
end
140+
141+
it "extract certificate from md:KeyDescriptor" do
142+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
143+
idp_metadata = idp_metadata_descriptor
144+
idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
145+
idp_metadata = idp_metadata.sub('<md:KeyDescriptor use="encryption">', '<md:KeyDescriptor>')
146+
parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
147+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
148+
end
149+
150+
it "extract SSO endpoint with no specific binding, it takes the first" do
151+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
152+
idp_metadata = idp_metadata_descriptor3
153+
metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
154+
assert_equal "https://idp.example.com/idp/profile/Shibboleth/SSO", metadata[:idp_sso_target_url]
155+
end
156+
157+
it "extract SSO endpoint with specific binding" do
158+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
159+
idp_metadata = idp_metadata_descriptor3
160+
options = {}
161+
options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']
162+
parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, options)
163+
assert_equal "https://idp.example.com/idp/profile/SAML2/POST/SSO", parsed_metadata[:idp_sso_target_url]
164+
165+
options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
166+
parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, options)
167+
assert_equal "https://idp.example.com/idp/profile/SAML2/Redirect/SSO", parsed_metadata[:idp_sso_target_url]
168+
169+
options[:sso_binding] = ['invalid_binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
170+
parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, options)
171+
assert_equal "https://idp.example.com/idp/profile/SAML2/Redirect/SSO", parsed_metadata[:idp_sso_target_url]
172+
end
173+
174+
it "ignores a given :settings hash" do
175+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
176+
idp_metadata = idp_metadata_descriptor
177+
parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, {
178+
:settings => {
179+
:security => {
180+
:digest_method => XMLSecurity::Document::SHA256,
181+
:signature_method => XMLSecurity::Document::RSA_SHA256
182+
}
183+
}
184+
})
185+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
186+
assert_nil parsed_metadata[:security]
187+
end
98188
end
99189

100190
describe "parsing an IdP descriptor file with multiple signing certs" do
@@ -152,6 +242,39 @@ def initialize; end
152242
end
153243
end
154244

245+
describe "download and parse IdP descriptor file into an Hash" do
246+
before do
247+
mock_response = MockSuccessResponse.new
248+
mock_response.body = idp_metadata_descriptor
249+
@url = "https://example.com"
250+
uri = URI(@url)
251+
252+
@http = Net::HTTP.new(uri.host, uri.port)
253+
Net::HTTP.expects(:new).returns(@http)
254+
@http.expects(:request).returns(mock_response)
255+
end
256+
257+
it "extract settings from remote xml" do
258+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
259+
parsed_metadata = idp_metadata_parser.parse_remote_to_hash(@url)
260+
261+
assert_equal "https://hello.example.com/access/saml/idp.xml", parsed_metadata[:idp_entity_id]
262+
assert_equal "https://hello.example.com/access/saml/login", parsed_metadata[:idp_sso_target_url]
263+
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
264+
assert_equal "https://hello.example.com/access/saml/logout", parsed_metadata[:idp_slo_target_url]
265+
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", parsed_metadata[:name_identifier_format]
266+
assert_equal ["AuthToken", "SSOStartPage"], parsed_metadata[:idp_attribute_names]
267+
assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
268+
end
269+
270+
it "accept self signed certificate if insturcted" do
271+
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
272+
idp_metadata_parser.parse_remote_to_hash(@url, false)
273+
274+
assert_equal OpenSSL::SSL::VERIFY_NONE, @http.verify_mode
275+
end
276+
end
277+
155278
describe "download failure cases" do
156279
it "raises an exception when the url has no scheme" do
157280
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new

0 commit comments

Comments
 (0)