Skip to content

Commit 5b58bae

Browse files
Merge branch 'master' into new-idp-binding-params
2 parents a714b73 + c6489cc commit 5b58bae

File tree

6 files changed

+151
-47
lines changed

6 files changed

+151
-47
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,3 +762,27 @@ end
762762
```
763763
764764
The `attribute_value` option additionally accepts an array of possible values.
765+
766+
## Custom Metadata Fields
767+
768+
Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.)
769+
into the SP metadata. This can be acheived by extending the `OneLogin::RubySaml::Metadata`
770+
class and overriding the `#add_extras` method as per the following example:
771+
772+
```ruby
773+
class MyMetadata < OneLogin::RubySaml::Metadata
774+
def add_extras(root, _settings)
775+
org = root.add_element("md:Organization")
776+
org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.'
777+
org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME'
778+
org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com'
779+
780+
cp = root.add_element("md:ContactPerson", 'contactType' => 'technical')
781+
cp.add_element("md:GivenName").text = 'ACME SAML Team'
782+
cp.add_element("md:EmailAddress").text = '[email protected]'
783+
end
784+
end
785+
786+
# Output XML with custom metadata
787+
MyMetadata.new.generate(settings)
788+
```

lib/onelogin/ruby-saml/idp_metadata_parser.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,10 @@ def merge_certificates_into(parsed_metadata)
423423
parsed_metadata[:idp_cert_fingerprint_algorithm]
424424
)
425425
end
426-
else
427-
# symbolize keys of certificates and pass it on
428-
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
429426
end
427+
428+
# symbolize keys of certificates and pass it on
429+
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
430430
end
431431

432432
def certificates_has_one(key)

lib/onelogin/ruby-saml/metadata.rb

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,50 @@ class Metadata
2121
#
2222
def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
2323
meta_doc = XMLSecurity::Document.new
24+
add_xml_declaration(meta_doc)
25+
root = add_root_element(meta_doc, settings, valid_until, cache_duration)
26+
sp_sso = add_sp_sso_element(root, settings)
27+
add_sp_certificates(sp_sso, settings)
28+
add_sp_service_elements(sp_sso, settings)
29+
add_extras(root, settings)
30+
embed_signature(meta_doc, settings)
31+
output_xml(meta_doc, pretty_print)
32+
end
33+
34+
protected
35+
36+
def add_xml_declaration(meta_doc)
37+
meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8')
38+
end
39+
40+
def add_root_element(meta_doc, settings, valid_until, cache_duration)
2441
namespaces = {
2542
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
2643
}
44+
2745
if settings.attribute_consuming_service.configured?
2846
namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion"
2947
end
30-
root = meta_doc.add_element "md:EntityDescriptor", namespaces
31-
sp_sso = root.add_element "md:SPSSODescriptor", {
48+
49+
root = meta_doc.add_element("md:EntityDescriptor", namespaces)
50+
root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
51+
root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id
52+
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z') if valid_until
53+
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration
54+
root
55+
end
56+
57+
def add_sp_sso_element(root, settings)
58+
root.add_element "md:SPSSODescriptor", {
3259
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
3360
"AuthnRequestsSigned" => settings.security[:authn_requests_signed],
3461
"WantAssertionsSigned" => settings.security[:want_assertions_signed],
3562
}
63+
end
3664

37-
# Add KeyDescriptor if messages will be signed / encrypted
38-
# with SP certificate, and new SP certificate if any
65+
# Add KeyDescriptor if messages will be signed / encrypted
66+
# with SP certificate, and new SP certificate if any
67+
def add_sp_certificates(sp_sso, settings)
3968
cert = settings.get_sp_cert
4069
cert_new = settings.get_sp_cert_new
4170

@@ -58,27 +87,23 @@ def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
5887
end
5988
end
6089

61-
root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
62-
if settings.sp_entity_id
63-
root.attributes["entityID"] = settings.sp_entity_id
64-
end
65-
if valid_until
66-
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z')
67-
end
68-
if cache_duration
69-
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S"
70-
end
90+
sp_sso
91+
end
92+
93+
def add_sp_service_elements(sp_sso, settings)
7194
if settings.single_logout_service_url
7295
sp_sso.add_element "md:SingleLogoutService", {
7396
"Binding" => settings.single_logout_service_binding,
7497
"Location" => settings.single_logout_service_url,
7598
"ResponseLocation" => settings.single_logout_service_url
7699
}
77100
end
101+
78102
if settings.name_identifier_format
79103
nameid = sp_sso.add_element "md:NameIDFormat"
80104
nameid.text = settings.name_identifier_format
81105
end
106+
82107
if settings.assertion_consumer_service_url
83108
sp_sso.add_element "md:AssertionConsumerService", {
84109
"Binding" => settings.assertion_consumer_service_binding,
@@ -117,23 +142,35 @@ def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
117142
# <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
118143
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
119144

120-
meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
145+
sp_sso
146+
end
121147

122-
# embed signature
123-
if settings.security[:metadata_signed] && settings.private_key && settings.certificate
124-
private_key = settings.get_sp_key
125-
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
126-
end
148+
# can be overridden in subclass
149+
def add_extras(root, _settings)
150+
root
151+
end
152+
153+
def embed_signature(meta_doc, settings)
154+
return unless settings.security[:metadata_signed]
155+
156+
private_key = settings.get_sp_key
157+
cert = settings.get_sp_cert
158+
return unless private_key && cert
159+
160+
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
161+
end
162+
163+
def output_xml(meta_doc, pretty_print)
164+
ret = ''
127165

128-
ret = ""
129166
# pretty print the XML so IdP administrators can easily see what the SP supports
130167
if pretty_print
131168
meta_doc.write(ret, 1)
132169
else
133170
ret = meta_doc.to_s
134171
end
135172

136-
return ret
173+
ret
137174
end
138175
end
139176
end

lib/xml_security.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,13 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
159159
x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
160160

161161
# add the signature
162-
issuer_element = self.elements["//saml:Issuer"]
162+
issuer_element = elements["//saml:Issuer"]
163163
if issuer_element
164-
self.root.insert_after issuer_element, signature_element
164+
root.insert_after(issuer_element, signature_element)
165+
elsif first_child = root.children[0]
166+
root.insert_before(first_child, signature_element)
165167
else
166-
if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
167-
self.root.insert_before sp_sso_descriptor, signature_element
168-
else
169-
self.root.add_element(signature_element)
170-
end
168+
root.add_element(signature_element)
171169
end
172170
end
173171

test/idp_metadata_parser_test.rb

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,8 @@ def initialize; end
556556
@settings = @idp_metadata_parser.parse(@idp_metadata)
557557
end
558558

559-
it "should return idp_cert and idp_cert_fingerprint and no idp_cert_multi" do
560-
assert_equal "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
559+
let(:expected_cert) do
560+
"MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
561561
MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
562562
VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
563563
MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
@@ -579,16 +579,19 @@ def initialize; end
579579
sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
580580
TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
581581
QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
582-
1sE=", @settings.idp_cert
583-
assert_equal "2D:A9:40:88:28:EE:67:BB:4A:5B:E0:58:A7:CC:71:95:2D:1B:C9:D3", @settings.idp_cert_fingerprint
584-
assert_nil @settings.idp_cert_multi
585-
assert_equal "https://app.onelogin.com/saml/metadata/383123", @settings.idp_entity_id
586-
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", @settings.name_identifier_format
587-
assert_equal "https://app.onelogin.com/trust/saml2/http-post/sso/383123", @settings.idp_sso_service_url
588-
assert_equal "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", @settings.idp_sso_service_binding
589-
assert_nil @settings.idp_slo_service_url
582+
1sE="
583+
end
584+
585+
it "should return idp_cert and idp_cert_fingerprint and no idp_cert_multi" do
586+
assert_equal(expected_cert, @settings.idp_cert)
587+
assert_equal("2D:A9:40:88:28:EE:67:BB:4A:5B:E0:58:A7:CC:71:95:2D:1B:C9:D3", @settings.idp_cert_fingerprint)
588+
assert_equal({ signing: [expected_cert], encryption: [expected_cert] }, @settings.idp_cert_multi)
589+
assert_equal("https://app.onelogin.com/saml/metadata/383123", @settings.idp_entity_id)
590+
assert_equal("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", @settings.name_identifier_format)
591+
assert_equal("https://app.onelogin.com/trust/saml2/http-post/sso/383123", @settings.idp_sso_service_url)
592+
assert_nil(@settings.idp_slo_service_url)
590593
# TODO: next line can be changed to `assert_nil @settings.idp_slo_service_binding` after :embed_sign is removed.
591-
assert_nil @settings.instance_variable_get('@idp_slo_service_binding')
594+
assert_nil(@settings.instance_variable_get('@idp_slo_service_binding'))
592595
end
593596
end
594597

@@ -662,13 +665,13 @@ def initialize; end
662665
assert_nil @settings.instance_variable_get('@idp_slo_service_binding')
663666
end
664667
end
668+
665669
describe "metadata with different singlelogout response location" do
666670
it "should return the responselocation if it exists" do
667671
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
668672

669673
settings = idp_metadata_parser.parse(idp_different_slo_response_location)
670674

671-
672675
assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_service_url
673676
assert_equal "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", settings.idp_slo_service_binding
674677
assert_equal "https://hello.example.com/access/saml/logout/return", settings.idp_slo_response_service_url
@@ -679,7 +682,6 @@ def initialize; end
679682

680683
settings = idp_metadata_parser.parse(idp_without_slo_response_location)
681684

682-
683685
assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_service_url
684686
assert_equal "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", settings.idp_slo_service_binding
685687
assert_nil settings.idp_slo_response_service_url

test/metadata_test.rb

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ class MetadataTest < Minitest::Test
314314
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
315315
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], xml_text
316316
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], xml_text
317+
317318
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
318319
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)
319320

@@ -331,9 +332,51 @@ class MetadataTest < Minitest::Test
331332
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'/>], xml_text
332333
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2001/04/xmlenc#sha512'/>], xml_text
333334

334-
signed_metadata_2 = XMLSecurity::SignedDocument.new(xml_text)
335+
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
336+
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)
337+
338+
assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd")
339+
end
340+
end
341+
342+
describe "when custom metadata elements have been inserted" do
343+
let(:xml_text) { subclass.new.generate(settings, false) }
344+
let(:subclass) do
345+
Class.new(OneLogin::RubySaml::Metadata) do
346+
def add_extras(root, _settings)
347+
idp = REXML::Element.new("md:IDPSSODescriptor")
348+
idp.attributes['protocolSupportEnumeration'] = 'urn:oasis:names:tc:SAML:2.0:protocol'
349+
350+
nid = REXML::Element.new("md:NameIDFormat")
351+
nid.text = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
352+
idp.add_element(nid)
353+
354+
sso = REXML::Element.new("md:SingleSignOnService")
355+
sso.attributes['Binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
356+
sso.attributes['Location'] = 'https://foobar.com/sso'
357+
idp.add_element(sso)
358+
root.insert_before(root.children[0], idp)
359+
360+
org = REXML::Element.new("md:Organization")
361+
org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.'
362+
org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME'
363+
org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com'
364+
root.insert_after(root.children[3], org)
365+
end
366+
end
367+
end
368+
369+
it "inserts signature as the first child of root element" do
370+
first_child = xml_doc.root.children[0]
371+
assert_equal first_child.prefix, 'ds'
372+
assert_equal first_child.name, 'Signature'
373+
374+
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
375+
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], xml_text
376+
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], xml_text
335377

336-
assert signed_metadata_2.validate_document(ruby_saml_cert_fingerprint, false)
378+
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
379+
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)
337380

338381
assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd")
339382
end

0 commit comments

Comments
 (0)