Skip to content

Commit

Permalink
Merge pull request #602 from johnnyshields/refactor-metadata-class
Browse files Browse the repository at this point in the history
Refactor the OneLogin::RubySaml::Metadata class
  • Loading branch information
pitbulk authored Aug 16, 2021
2 parents 2897573 + cc295ea commit c6489cc
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 31 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,3 +764,27 @@ end
```
The `attribute_value` option additionally accepts an array of possible values.
## Custom Metadata Fields
Some IdPs may require to add SPs to add additional fields (Organization, ContactPerson, etc.)
into the SP metadata. This can be acheived by extending the `OneLogin::RubySaml::Metadata`
class and overriding the `#add_extras` method as per the following example:
```ruby
class MyMetadata < OneLogin::RubySaml::Metadata
def add_extras(root, _settings)
org = root.add_element("md:Organization")
org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.'
org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME'
org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com'
cp = root.add_element("md:ContactPerson", 'contactType' => 'technical')
cp.add_element("md:GivenName").text = 'ACME SAML Team'
cp.add_element("md:EmailAddress").text = '[email protected]'
end
end
# Output XML with custom metadata
MyMetadata.new.generate(settings)
```
81 changes: 59 additions & 22 deletions lib/onelogin/ruby-saml/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,50 @@ class Metadata
#
def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
meta_doc = XMLSecurity::Document.new
add_xml_declaration(meta_doc)
root = add_root_element(meta_doc, settings, valid_until, cache_duration)
sp_sso = add_sp_sso_element(root, settings)
add_sp_certificates(sp_sso, settings)
add_sp_service_elements(sp_sso, settings)
add_extras(root, settings)
embed_signature(meta_doc, settings)
output_xml(meta_doc, pretty_print)
end

protected

def add_xml_declaration(meta_doc)
meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8')
end

def add_root_element(meta_doc, settings, valid_until, cache_duration)
namespaces = {
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
}

if settings.attribute_consuming_service.configured?
namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion"
end
root = meta_doc.add_element "md:EntityDescriptor", namespaces
sp_sso = root.add_element "md:SPSSODescriptor", {

root = meta_doc.add_element("md:EntityDescriptor", namespaces)
root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z') if valid_until
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration
root
end

def add_sp_sso_element(root, settings)
root.add_element "md:SPSSODescriptor", {
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
"AuthnRequestsSigned" => settings.security[:authn_requests_signed],
"WantAssertionsSigned" => settings.security[:want_assertions_signed],
}
end

# Add KeyDescriptor if messages will be signed / encrypted
# with SP certificate, and new SP certificate if any
# Add KeyDescriptor if messages will be signed / encrypted
# with SP certificate, and new SP certificate if any
def add_sp_certificates(sp_sso, settings)
cert = settings.get_sp_cert
cert_new = settings.get_sp_cert_new

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

root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
if settings.sp_entity_id
root.attributes["entityID"] = settings.sp_entity_id
end
if valid_until
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z')
end
if cache_duration
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S"
end
sp_sso
end

def add_sp_service_elements(sp_sso, settings)
if settings.single_logout_service_url
sp_sso.add_element "md:SingleLogoutService", {
"Binding" => settings.single_logout_service_binding,
"Location" => settings.single_logout_service_url,
"ResponseLocation" => settings.single_logout_service_url
}
end

if settings.name_identifier_format
nameid = sp_sso.add_element "md:NameIDFormat"
nameid.text = settings.name_identifier_format
end

if settings.assertion_consumer_service_url
sp_sso.add_element "md:AssertionConsumerService", {
"Binding" => settings.assertion_consumer_service_binding,
Expand Down Expand Up @@ -117,23 +142,35 @@ def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
# <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"/>
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>

meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
sp_sso
end

# embed signature
if settings.security[:metadata_signed] && settings.private_key && settings.certificate
private_key = settings.get_sp_key
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
end
# can be overridden in subclass
def add_extras(root, _settings)
root
end

def embed_signature(meta_doc, settings)
return unless settings.security[:metadata_signed]

private_key = settings.get_sp_key
cert = settings.get_sp_cert
return unless private_key && cert

meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
end

def output_xml(meta_doc, pretty_print)
ret = ''

ret = ""
# pretty print the XML so IdP administrators can easily see what the SP supports
if pretty_print
meta_doc.write(ret, 1)
else
ret = meta_doc.to_s
end

return ret
ret
end
end
end
Expand Down
12 changes: 5 additions & 7 deletions lib/xml_security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,13 @@ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_
x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")

# add the signature
issuer_element = self.elements["//saml:Issuer"]
issuer_element = elements["//saml:Issuer"]
if issuer_element
self.root.insert_after issuer_element, signature_element
root.insert_after(issuer_element, signature_element)
elsif first_child = root.children[0]
root.insert_before(first_child, signature_element)
else
if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
self.root.insert_before sp_sso_descriptor, signature_element
else
self.root.add_element(signature_element)
end
root.add_element(signature_element)
end
end

Expand Down
47 changes: 45 additions & 2 deletions test/metadata_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ class MetadataTest < Minitest::Test
assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], xml_text
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], xml_text

signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)

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

signed_metadata_2 = XMLSecurity::SignedDocument.new(xml_text)
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)

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

describe "when custom metadata elements have been inserted" do
let(:xml_text) { subclass.new.generate(settings, false) }
let(:subclass) do
Class.new(OneLogin::RubySaml::Metadata) do
def add_extras(root, _settings)
idp = REXML::Element.new("md:IDPSSODescriptor")
idp.attributes['protocolSupportEnumeration'] = 'urn:oasis:names:tc:SAML:2.0:protocol'

nid = REXML::Element.new("md:NameIDFormat")
nid.text = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
idp.add_element(nid)

sso = REXML::Element.new("md:SingleSignOnService")
sso.attributes['Binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
sso.attributes['Location'] = 'https://foobar.com/sso'
idp.add_element(sso)
root.insert_before(root.children[0], idp)

org = REXML::Element.new("md:Organization")
org.add_element("md:OrganizationName", 'xml:lang' => "en-US").text = 'ACME Inc.'
org.add_element("md:OrganizationDisplayName", 'xml:lang' => "en-US").text = 'ACME'
org.add_element("md:OrganizationURL", 'xml:lang' => "en-US").text = 'https://www.acme.com'
root.insert_after(root.children[3], org)
end
end
end

it "inserts signature as the first child of root element" do
first_child = xml_doc.root.children[0]
assert_equal first_child.prefix, 'ds'
assert_equal first_child.name, 'Signature'

assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>]m, xml_text
assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], xml_text
assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], xml_text

assert signed_metadata_2.validate_document(ruby_saml_cert_fingerprint, false)
signed_metadata = XMLSecurity::SignedDocument.new(xml_text)
assert signed_metadata.validate_document(ruby_saml_cert_fingerprint, false)

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

0 comments on commit c6489cc

Please sign in to comment.