From 79120326def96b7e239f3cee68e6895c86c21ffc Mon Sep 17 00:00:00 2001 From: Lawrence Pit Date: Mon, 30 Apr 2012 14:08:43 +1000 Subject: [PATCH 01/35] Documentation: the SAML spec states "the content type of the metadata instance MUST be application/samlmetadata+xml" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 008b37699..118bbba9b 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ to the IdP settings. def metadata settings = Account.get_saml_settings meta = Onelogin::Saml::Metadata.new - render :xml => meta.generate(settings) + render :xml => meta.generate(settings), :content_type => "application/samlmetadata+xml" end end ``` From c8f788ad07bdb95a59e1a7c2d7c23cf93d97db09 Mon Sep 17 00:00:00 2001 From: Lawrence Pit Date: Mon, 30 Apr 2012 14:59:03 +1000 Subject: [PATCH 02/35] Metadata AuthnRequestsSigned and WantsAssertionsSigned + added MetadataTest Conflicts: lib/onelogin/ruby-saml/metadata.rb --- lib/onelogin/ruby-saml/metadata.rb | 9 ++++---- test/metadata_test.rb | 34 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 test/metadata_test.rb diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index 46c67ea43..7cb6d40b1 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -3,7 +3,7 @@ require "uri" # Class to return SP metadata based on the settings requested. -# Return this XML in a controller, then give that URL to the the +# Return this XML in a controller, then give that URL to the the # IdP administrator. The IdP will poll the URL and your settings # will be updated automatically module Onelogin @@ -48,18 +48,17 @@ def generate(settings) "index" => 0 } end + # With OpenSSO, it might be required to also include # # - meta_doc << REXML::XMLDecl.new + meta_doc << REXML::XMLDecl.new("1.0", "UTF-8", "yes") ret = "" # pretty print the XML so IdP administrators can easily see what the SP supports meta_doc.write(ret, 1) - Logging.debug "Generated metadata:\n#{ret}" - - ret + return ret end end end diff --git a/test/metadata_test.rb b/test/metadata_test.rb new file mode 100644 index 000000000..2f8c7754d --- /dev/null +++ b/test/metadata_test.rb @@ -0,0 +1,34 @@ +require 'test_helper' + +class MetadataTest < Test::Unit::TestCase + + should "should generate Service Provider Metadata" do + settings = Onelogin::Saml::Settings.new + settings.issuer = "https://example.com/" + settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + settings.assertion_consumer_service_url = "https://foo.example/saml/consume" + + xml_text = Onelogin::Saml::Metadata.new.generate(settings) + + # assert correct xml declaration + start = "\n Date: Tue, 22 Apr 2014 13:39:17 -0400 Subject: [PATCH 03/35] Support parsing of entityID and protocol_binding from IdP metadata --- lib/onelogin/ruby-saml/idp_metadata_parser.rb | 18 ++++++++++++++---- lib/onelogin/ruby-saml/settings.rb | 1 + test/idp_metadata_parser_test.rb | 2 ++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/onelogin/ruby-saml/idp_metadata_parser.rb b/lib/onelogin/ruby-saml/idp_metadata_parser.rb index a87427676..2491c77a7 100644 --- a/lib/onelogin/ruby-saml/idp_metadata_parser.rb +++ b/lib/onelogin/ruby-saml/idp_metadata_parser.rb @@ -20,15 +20,26 @@ def parse(idp_metadata) @document = REXML::Document.new(idp_metadata) OneLogin::RubySaml::Settings.new.tap do |settings| - + settings.entity_id = entity_id settings.idp_sso_target_url = single_signon_service_url settings.idp_slo_target_url = single_logout_service_url + settings.idp_cert = certificate settings.idp_cert_fingerprint = fingerprint + settings.protocol_binding = single_signon_service_binding end end private + def entity_id + document.root.attributes["entityID"] + end + + def single_signon_service_binding + node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding", { "md" => METADATA }) + node.value if node + end + def single_signon_service_url node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location", { "md" => METADATA }) node.value if node @@ -42,15 +53,14 @@ def single_logout_service_url def certificate @certificate ||= begin node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate", { "md" => METADATA, "ds" => DSIG }) - Base64.decode64(node.text) if node + OpenSSL::X509::Certificate.new(Base64.decode64(node.text)) if node end end def fingerprint @fingerprint ||= begin if certificate - cert = OpenSSL::X509::Certificate.new(certificate) - Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":") + Digest::SHA1.hexdigest(certificate.to_der).upcase.scan(/../).join(":") end end end diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index d3f2a87ef..cafaac15d 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -10,6 +10,7 @@ def initialize(overrides = {}) end attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format + attr_accessor :entity_id attr_accessor :authn_context attr_accessor :idp_slo_target_url attr_accessor :name_identifier_value diff --git a/test/idp_metadata_parser_test.rb b/test/idp_metadata_parser_test.rb index daab729b3..430406cd5 100644 --- a/test/idp_metadata_parser_test.rb +++ b/test/idp_metadata_parser_test.rb @@ -9,8 +9,10 @@ class IdpMetadataParserTest < Test::Unit::TestCase settings = idp_metadata_parser.parse(idp_metadata) assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url + assert_equal "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", settings.protocol_binding 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 assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url + assert_equal "https://example.hello.com/access/saml/idp.xml", settings.entity_id end end From 863426f44bb4dddb6252beee141e2a9dabee5f94 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Fri, 25 Apr 2014 14:40:03 -0400 Subject: [PATCH 04/35] create_pramas method for HTTP-Post Binding Allows ease of using a HTTP-Post binding, the client call call create_params get a hash of parameters to use in the html form. Now the caller does not have to decode and parse a URL to get the SAMLReqeust for use in the form. --- lib/onelogin/ruby-saml/authrequest.rb | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index affcb23ef..8e558f077 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -9,7 +9,19 @@ module OneLogin module RubySaml include REXML class Authrequest + def create(settings, params = {}) + params = create_params(settings, params) + request_params = "" + params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?' + params.each_pair do |key, value| + request_params << "#{params_prefix}#{key.to_s}=#{CGI.escape(value.to_s)}" + params_prefix = "&" + end + settings.idp_sso_target_url + request_params + end + + def create_params(settings, params={}) params = {} if params.nil? request_doc = create_authentication_xml_doc(settings) @@ -22,15 +34,14 @@ def create(settings, params = {}) request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request base64_request = Base64.encode64(request) - encoded_request = CGI.escape(base64_request) - params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?' - request_params = "#{params_prefix}SAMLRequest=#{encoded_request}" + encoded_request = base64_request + request_params = {"SAMLRequest" => encoded_request} params.each_pair do |key, value| - request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}" + request_params[key] = value.to_s end - settings.idp_sso_target_url + request_params + request_params end def create_authentication_xml_doc(settings) From f95c60c8acbafd0bb0dd76d4b3297b6138f96d26 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Fri, 25 Apr 2014 14:47:29 -0400 Subject: [PATCH 05/35] XMLSecurity::RequestDocument class Extends REXML::Document to add XML Signature functionality. --- lib/xml_security.rb | 140 ++++++++++++++++++++++++++------ test/certificates/ruby-saml.crt | 14 ++++ test/certificates/ruby-saml.key | 15 ++++ test/test_helper.rb | 21 +++++ test/xml_security_test.rb | 18 ++++ 5 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 test/certificates/ruby-saml.crt create mode 100644 test/certificates/ruby-saml.key diff --git a/lib/xml_security.rb b/lib/xml_security.rb index de1530cae..8f2f9f412 100644 --- a/lib/xml_security.rb +++ b/lib/xml_security.rb @@ -33,10 +33,125 @@ module XMLSecurity - class SignedDocument < REXML::Document + class BaseDocument < REXML::Document + C14N = "http://www.w3.org/2001/10/xml-exc-c14n#" DSIG = "http://www.w3.org/2000/09/xmldsig#" + def canon_algorithm(element) + algorithm = element + if algorithm.is_a?(REXML::Element) + algorithm = element.attribute('Algorithm').value + end + + case algorithm + when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 + when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0 + when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1 + else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 + end + end + + def algorithm(element) + algorithm = element + if algorithm.is_a?(REXML::Element) + algorithm = element.attribute("Algorithm").value + algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i + end + + case algorithm + when 256 then OpenSSL::Digest::SHA256 + when 384 then OpenSSL::Digest::SHA384 + when 512 then OpenSSL::Digest::SHA512 + else + OpenSSL::Digest::SHA1 + end + end + + end + + class RequestDocument < BaseDocument + + SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" + SHA256 = "http://www.w3.org/2000/09/xmldsig#sha256" + SHA384 = "http://www.w3.org/2000/09/xmldsig#sha384" + SHA512 = "http://www.w3.org/2000/09/xmldsig#sha512" + ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" + INC_PREFIX_LIST = "#default samlp saml ds xs xsi" + + attr_accessor :uuid + + # + # + # + # + # + # + # + # + # + # etc. + # + # + # + # + # + def sign_document(private_key, certificate, signature_method = SHA1, digest_method = SHA1) + + noko = Nokogiri.parse(self.to_s) + canon_doc = noko.canonicalize(canon_algorithm(C14N)) + + signature_element = REXML::Element.new("Signature").add_namespace(DSIG) + signed_info_element = signature_element.add_element("SignedInfo") + signed_info_element.add_element("CanonicalizationMethod", {"Algorithm" => C14N}) + signed_info_element.add_element("SignatureMethod", {"Algorithm"=>SHA1}) + + # Add Reference + reference_element = signed_info_element.add_element("Reference", {"URI" => "##{uuid}"}) + digest_method_element = reference_element.add_element("DigestMethod", {"Algorithm" => digest_method}) + reference_element.add_element("DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element)) + + # Add Transforms + transforms_element = reference_element.add_element("Transforms") + transforms_element.add_element("Transform", {"Algorithm" => ENVELOPED_SIG}) + transforms_element.add_element("Transform", {"Algorithm" => C14N}) + transforms_element.add_element("InclusiveNamespaces", {"xmlns" => C14N, "PrefixList" => INC_PREFIX_LIST}) + + # add SignatureValue + noko_sig_element = Nokogiri.parse(signature_element.to_s) + noko_signed_info_element = noko_sig_element.at_xpath('//ds:SignedInfo', 'ds' => DSIG) + canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N)) + signature = compute_signature(private_key, algorithm(signature_method).new, canon_string) + signature_element.add_element("SignatureValue").text = signature + + # add KeyInfo + key_info_element = signature_element.add_element("KeyInfo") + x509_element = key_info_element.add_element("X509Data") + x509_cert_element = x509_element.add_element("X509Certificate") + if certificate.is_a?(String) + certificate = OpenSSL::X509::Certificate.new(certificate) + end + x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "") + + # add the signature + self.root.add_element(signature_element) + end + + protected + + def compute_signature(private_key, signature_algorithm, document) + Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "") + end + + def compute_digest(document, digest_algorithm) + digest = digest_algorithm.digest(document) + Base64.encode64(digest).strip! + end + + end + + class SignedDocument < BaseDocument + attr_accessor :signed_element_id def initialize(response) @@ -79,7 +194,6 @@ def validate_signature(base64_cert, soft = true) element.remove end - # verify signature signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG}) noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG) @@ -134,28 +248,6 @@ def extract_signed_element_id self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil? end - def canon_algorithm(element) - algorithm = element.attribute('Algorithm').value if element - case algorithm - when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 - when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0 - when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1 - else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0 - end - end - - def algorithm(element) - algorithm = element.attribute("Algorithm").value if element - algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i - case algorithm - when 256 then OpenSSL::Digest::SHA256 - when 384 then OpenSSL::Digest::SHA384 - when 512 then OpenSSL::Digest::SHA512 - else - OpenSSL::Digest::SHA1 - end - end - def extract_inclusive_namespaces if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N }) prefix_list = element.attributes.get_attribute("PrefixList").value diff --git a/test/certificates/ruby-saml.crt b/test/certificates/ruby-saml.crt new file mode 100644 index 000000000..60a1291d4 --- /dev/null +++ b/test/certificates/ruby-saml.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGzCCAYQCCQCNNcQXom32VDANBgkqhkiG9w0BAQUFADBSMQswCQYDVQQGEwJV +UzELMAkGA1UECBMCSU4xFTATBgNVBAcTDEluZGlhbmFwb2xpczERMA8GA1UEChMI +T25lTG9naW4xDDAKBgNVBAsTA0VuZzAeFw0xNDA0MjMxODQxMDFaFw0xNTA0MjMx +ODQxMDFaMFIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTjEVMBMGA1UEBxMMSW5k +aWFuYXBvbGlzMREwDwYDVQQKEwhPbmVMb2dpbjEMMAoGA1UECxMDRW5nMIGfMA0G +CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo6m+QZvYQ/xL0ElLgupK1QDcYL4f5Pckw +sNgS9pUvV7fzTqCHk8ThLxTk42MQ2McJsOeUJVP728KhymjFCqxgP4VuwRk9rpAl +0+mhy6MPdyjyA6G14jrDWS65ysLchK4t/vwpEDz0SQlEoG1kMzllSm7zZS3XregA +7DjNaUYQqwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALM2vGCiQ/vm+a6v40+VX2zd +qHA2Q/1vF1ibQzJ54MJCOVWvs+vQXfZFhdm0OPM2IrDU7oqvKPqP6xOAeJK6H0yP +7M4YL3fatSvIYmmfyXC9kt3Svz/NyrHzPhUnJ0ye/sUSXxnzQxwcm/9PwAqrQaA3 +QpQkH57ybF/OoryPe+2h +-----END CERTIFICATE----- diff --git a/test/certificates/ruby-saml.key b/test/certificates/ruby-saml.key new file mode 100644 index 000000000..902d3b0f8 --- /dev/null +++ b/test/certificates/ruby-saml.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDo6m+QZvYQ/xL0ElLgupK1QDcYL4f5PckwsNgS9pUvV7fzTqCH +k8ThLxTk42MQ2McJsOeUJVP728KhymjFCqxgP4VuwRk9rpAl0+mhy6MPdyjyA6G1 +4jrDWS65ysLchK4t/vwpEDz0SQlEoG1kMzllSm7zZS3XregA7DjNaUYQqwIDAQAB +AoGBALGR6bRBit+yV5TUU3MZSrf8WQSLWDLgs/33FQSAEYSib4+DJke2lKbI6jkG +UoSJgFUXFbaQLtMY2+3VDsMKPBdAge9gIdvbkC4yoKjLGm/FBDOxxZcfLpR+9OPq +U3qM9D0CNuliBWI7Je+p/zs09HIYucpDXy9E18KA1KNF6rfhAkEA9KoNam6wAKnm +vMzz31ws3RuIOUeo2rx6aaVY95+P9tTxd6U+pNkwxy1aCGP+InVSwlYNA1aQ4Axi +/GdMIWMkxwJBAPO1CP7cQNZQmu7yusY+GUObDII5YK9WLaY4RAicn5378crPBFxv +Ukqf9G6FHo7u88iTCIp+vwa3Hn9Tumg3iP0CQQDgUXWBasCVqzCxU5wY4tMDWjXY +hpoLCpmVeRML3dDJt004rFm2HKe7Rhpw7PTZNQZOxUSjFeA4e0LaNf838UWLAkB8 +QfbHM3ffjhOg96PhhjINdVWoZCb230LBOHj/xxPfUmFTHcBEfQIBSJMxcrBFAnLL +9qPpMXymqOFk3ETz9DTlAj8E0qGbp78aVbTOtuwEwNJII+RPw+Zkc+lKR+yaWkAz +fIXw527NPHH3+rnBG72wyZr9ud4LAum9jh+5No1LQpk= +-----END RSA PRIVATE KEY----- diff --git a/test/test_helper.rb b/test/test_helper.rb index d515bd120..01afd633f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,6 +4,7 @@ require 'ruby-debug' require 'mocha/setup' require 'timecop' +require 'openssl' $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) @@ -72,4 +73,24 @@ def r1_signature_2 @signature2 ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'r1_certificate2_base64')) end + def ruby_saml_cert + @ruby_saml_cert ||= OpenSSL::X509::Certificate.new(ruby_saml_cert_text) + end + + def ruby_saml_cert_fingerprint + @ruby_saml_cert_fingerprint ||= Digest::SHA1.hexdigest(ruby_saml_cert.to_der).scan(/../).join(":") + end + + def ruby_saml_cert_text + File.read(File.join(File.dirname(__FILE__), 'certificates', 'ruby-saml.crt')) + end + + def ruby_saml_key + @ruby_saml_key ||= OpenSSL::PKey::RSA.new(ruby_saml_key_text) + end + + def ruby_saml_key_text + File.read(File.join(File.dirname(__FILE__), 'certificates', 'ruby-saml.key')) + end + end diff --git a/test/xml_security_test.rb b/test/xml_security_test.rb index 2ecea9f71..1dca21e7d 100644 --- a/test/xml_security_test.rb +++ b/test/xml_security_test.rb @@ -128,6 +128,24 @@ class XmlSecurityTest < Test::Unit::TestCase end end + context "XMLSecurity::DSIG" do + should "sign an xml document" do + settings = OneLogin::RubySaml::Settings.new({ + :idp_sso_target_url => "https://idp.example.com/sso", + :protocol_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + :issuer => "https://sp.exmapl.com/saml2", + :assertion_consumer_service_url => "https://sp.example.com/acs" + }) + + request = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings) + request.sign_document(ruby_saml_key, ruby_saml_cert) + + # verify our signature + signed_doc = XMLSecurity::SignedDocument.new(request.to_s) + signed_doc.validate_document(ruby_saml_cert_fingerprint, false) + end + end + context "StarfieldTMS" do setup do @response = OneLogin::RubySaml::Response.new(fixture(:starfield_response)) From 4e136dbb0c078ffa611a070ca5429a1672de87c0 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Fri, 25 Apr 2014 14:51:15 -0400 Subject: [PATCH 06/35] Preform Authrequest signing if settings specify request signing New settings were added to hold XML Signature attributes --- lib/onelogin/ruby-saml/authrequest.rb | 8 +++++++- lib/onelogin/ruby-saml/settings.rb | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index 8e558f077..d35fa8d07 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -48,7 +48,8 @@ def create_authentication_xml_doc(settings) uuid = "_" + UUID.new.generate time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") # Create AuthnRequest root element using REXML - request_doc = REXML::Document.new + request_doc = XMLSecurity::RequestDocument.new + request_doc.uuid = uuid root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" } root.attributes['ID'] = uuid @@ -88,6 +89,11 @@ def create_authentication_xml_doc(settings) } class_ref.text = settings.authn_context end + + if settings.sign_request && settings.private_key && settings.certificate + request_doc.sign_document(settings.private_key, settings.certificate, settings.signature_method, settings.digest_method) + end + request_doc end diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index d3f2a87ef..e24682b13 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -19,10 +19,11 @@ def initialize(overrides = {}) attr_accessor :double_quote_xml_attribute_values attr_accessor :passive attr_accessor :protocol_binding + attr_accessor :sign_request, :certificate, :private_key, :digest_method, :signature_method private - DEFAULTS = {:compress_request => true, :double_quote_xml_attribute_values => false} + DEFAULTS = {:compress_request => true, :sign_request => false, :double_quote_xml_attribute_values => false, :digest_method => "SHA1", :signature_method => "SHA1"} end end end From f6d0b47e385d9fcc625a2719782c25d927409624 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Tue, 29 Apr 2014 10:34:50 -0400 Subject: [PATCH 07/35] XML signature support for LogoutRequest --- lib/onelogin/ruby-saml/logoutrequest.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 8cf9e76ce..83b096a43 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -37,7 +37,7 @@ def create_unauth_xml_doc(settings, params) time = Time.new().strftime("%Y-%m-%dT%H:%M:%S") - request_doc = REXML::Document.new + request_doc = XMLSecurity::RequestDocument.new root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" } root.attributes['ID'] = @uuid root.attributes['IssueInstant'] = time @@ -75,6 +75,11 @@ def create_unauth_xml_doc(settings, params) } class_ref.text = settings.authn_context end + + if settings.sign_request && settings.private_key && settings.certificate + request_doc.sign_document(settings.private_key, settings.certificate, settings.signature_method, settings.digest_method) + end + request_doc end end From 03ce038394ce63c81e7f684762938e07b970bb3c Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Tue, 29 Apr 2014 10:35:02 -0400 Subject: [PATCH 08/35] Request signature test cases --- test/logoutrequest_test.rb | 18 ++++++++++++++++++ test/request_test.rb | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index 9e419919d..7f29cd909 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -95,6 +95,24 @@ class RequestTest < Test::Unit::TestCase assert_match %r[ID='#{unauth_req.uuid}'], inflated end end + + context "when the settings indicate to sign the request" do + should "created a signed request" do + settings = OneLogin::RubySaml::Settings.new + settings.idp_slo_target_url = "http://example.com/?field=value" + settings.name_identifier_value = "f00f00" + # sign the request + settings.sign_request = true + settings.certificate = ruby_saml_cert + settings.private_key = ruby_saml_key + + unauth_req = OneLogin::RubySaml::Logoutrequest.new + unauth_url = unauth_req.create(settings) + + inflated = decode_saml_request_payload(unauth_url) + assert_match %r[([a-zA-Z0-9/+=]+)], inflated + end + end end def decode_saml_request_payload(unauth_url) diff --git a/test/request_test.rb b/test/request_test.rb index 8728a3962..d51025837 100644 --- a/test/request_test.rb +++ b/test/request_test.rb @@ -110,5 +110,21 @@ class RequestTest < Test::Unit::TestCase assert auth_url =~ /^http:\/\/example.com\?field=value&SAMLRequest/ end end + + context "when the settings indicate to sign the request" do + should "create a signed request" do + settings = OneLogin::RubySaml::Settings.new + settings.compress_request = false + settings.idp_sso_target_url = "http://example.com/?field=value" + settings.sign_request = true + settings.certificate = ruby_saml_cert + settings.private_key = ruby_saml_key + + params = OneLogin::RubySaml::Authrequest.new.create_params(settings) + request_xml = Base64.decode64(params["SAMLRequest"]) + assert_match %r[([a-zA-Z0-9/+=]+)], request_xml + end + + end end end From 36160f256cbdcf75221fddedaea4ab977cfdbb5b Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Tue, 29 Apr 2014 10:41:18 -0400 Subject: [PATCH 09/35] Update docs to demonstrate signature functionality --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 48a8b1aa7..4607f7275 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,19 @@ response.settings = saml_settings response.attributes[:username] ``` +## Request Signing + +XML Dsig request signing is supported. Use the following settings to preform request signing: + +```ruby + settings = OneLogin::RubySaml::Settings.new + settings.sign_request = true + settings.certificate = X509::Certificate.new("CERTIFICATE TEXT") + settings.private_key = X509::PKey::RSA.new("PRIVATE KEY") + + signed_request = request.create(settings) +``` + ## Service Provider Metadata To form a trusted pair relationship with the IdP, the SP (you) need to provide metadata XML From 764b54b6b7f05090c41365b07f8e7bbd44e22ce6 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Tue, 29 Apr 2014 12:52:43 -0400 Subject: [PATCH 10/35] fixes test failure tests were looking for extra prams at the END of the request URL. --- lib/onelogin/ruby-saml/authrequest.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index d35fa8d07..192acd386 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -12,11 +12,11 @@ class Authrequest def create(settings, params = {}) params = create_params(settings, params) - request_params = "" params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?' + saml_request = params.delete("SAMLRequest") + request_params = "#{params_prefix}SAMLRequest=#{saml_request}" params.each_pair do |key, value| - request_params << "#{params_prefix}#{key.to_s}=#{CGI.escape(value.to_s)}" - params_prefix = "&" + request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}" end settings.idp_sso_target_url + request_params end From 2094d26622455fbca225186e6376a5b65895ea8b Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Tue, 29 Apr 2014 13:05:30 -0400 Subject: [PATCH 11/35] SAMLRequest needs to be URL-encoded --- lib/onelogin/ruby-saml/authrequest.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index 192acd386..d95233683 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -13,7 +13,7 @@ class Authrequest def create(settings, params = {}) params = create_params(settings, params) params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?' - saml_request = params.delete("SAMLRequest") + saml_request = CGI.escape(params.delete("SAMLRequest")) request_params = "#{params_prefix}SAMLRequest=#{saml_request}" params.each_pair do |key, value| request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}" @@ -34,8 +34,7 @@ def create_params(settings, params={}) request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request base64_request = Base64.encode64(request) - encoded_request = base64_request - request_params = {"SAMLRequest" => encoded_request} + request_params = {"SAMLRequest" => base64_request} params.each_pair do |key, value| request_params[key] = value.to_s From d78ba5c13049062d228f271d2e445f6dc7f3f4d0 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Thu, 1 May 2014 12:00:10 -0400 Subject: [PATCH 12/35] Address module class name changes from c8f788ad0..HEAD --- test/metadata_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/metadata_test.rb b/test/metadata_test.rb index 2f8c7754d..4fdf9a864 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -1,14 +1,14 @@ -require 'test_helper' +require File.expand_path(File.join(File.dirname(__FILE__), "test_helper")) class MetadataTest < Test::Unit::TestCase should "should generate Service Provider Metadata" do - settings = Onelogin::Saml::Settings.new + settings = OneLogin::RubySaml::Settings.new settings.issuer = "https://example.com/" settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" settings.assertion_consumer_service_url = "https://foo.example/saml/consume" - xml_text = Onelogin::Saml::Metadata.new.generate(settings) + xml_text = OneLogin::RubySaml::Metadata.new.generate(settings) # assert correct xml declaration start = "\n Date: Thu, 1 May 2014 13:07:00 -0400 Subject: [PATCH 13/35] Allow metadata generation to publish the cert if request signing is specified. --- lib/onelogin/ruby-saml/metadata.rb | 13 ++++++++++-- test/metadata_test.rb | 33 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 test/metadata_test.rb diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index db38fe98a..502f16b50 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -17,8 +17,7 @@ def generate(settings) } sp_sso = root.add_element "md:SPSSODescriptor", { "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol", - # Metadata request need not be signed (as we don't publish our cert) - "AuthnRequestsSigned" => false, + "AuthnRequestsSigned" => settings.sign_request, # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set "WantAssertionsSigned" => (!settings.idp_cert_fingerprint.nil? || !settings.idp_cert.nil?) } @@ -48,6 +47,16 @@ def generate(settings) "index" => 0 } end + + # Add KeyDescriptor if requests are signed + if settings.sign_request && !settings.certificate.nil? + kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } + ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} + xd = ki.add_element "ds:X509Data" + xc = xd.add_element "ds:X509Certificate" + xc.text = Base64.encode64(settings.certificate.to_der) + end + # With OpenSSO, it might be required to also include # # diff --git a/test/metadata_test.rb b/test/metadata_test.rb new file mode 100644 index 000000000..3173bb5a2 --- /dev/null +++ b/test/metadata_test.rb @@ -0,0 +1,33 @@ +require File.expand_path(File.join(File.dirname(__FILE__), "test_helper")) + +class MetadataTest < Test::Unit::TestCase + + def setup + @settings = OneLogin::RubySaml::Settings.new + @settings.issuer = "https://example.com/" + @settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + @settings.assertion_consumer_service_url = "https://foo.example/saml/consume" + end + + should "generate Service Provider Metadata with X509Certificate" do + @settings.sign_request = true + @settings.certificate = ruby_saml_cert + + xml_text = OneLogin::RubySaml::Metadata.new.generate(@settings) + + # assert xml_text can be parsed into an xml doc + xml_doc = REXML::Document.new(xml_text) + + spsso_descriptor = REXML::XPath.first(xml_doc, "//md:SPSSODescriptor") + assert_equal "true", spsso_descriptor.attribute("AuthnRequestsSigned").value + + cert_node = REXML::XPath.first(xml_doc, "//md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate", { + "md" => "urn:oasis:names:tc:SAML:2.0:metadata", + "ds" => "http://www.w3.org/2000/09/xmldsig#" + }) + cert_text = cert_node.text + cert = OpenSSL::X509::Certificate.new(Base64.decode64(cert_text)) + assert_equal ruby_saml_cert.to_der, cert.to_der + end + +end From 3789b228f98c2cb3fba6e9f7823e5735377d1a5d Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Thu, 1 May 2014 13:15:01 -0400 Subject: [PATCH 14/35] Setting support for assertion_consumer_service & assertion_consumer_logout_service --- README.md | 4 ++++ lib/onelogin/ruby-saml/metadata.rb | 6 ++---- lib/onelogin/ruby-saml/settings.rb | 12 +++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4607f7275..43d41fbb2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ def saml_settings # Optional for most SAML IdPs settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + # Optional bindings (defaults to Redirect for logout POST for acs) + settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + settings.assertion_consumer_logout_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" + settings end ``` diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index 502f16b50..82376d110 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -26,8 +26,7 @@ def generate(settings) end if settings.assertion_consumer_logout_service_url != nil sp_sso.add_element "md:SingleLogoutService", { - # Add this as a setting to create different bindings? - "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", + "Binding" => settings.assertion_consumer_logout_service_binding, "Location" => settings.assertion_consumer_logout_service_url, "ResponseLocation" => settings.assertion_consumer_logout_service_url, "isDefault" => true, @@ -40,8 +39,7 @@ def generate(settings) end if settings.assertion_consumer_service_url != nil sp_sso.add_element "md:AssertionConsumerService", { - # Add this as a setting to create different bindings? - "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + "Binding" => settings.assertion_consumer_service_binding, "Location" => settings.assertion_consumer_service_url, "isDefault" => true, "index" => 0 diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index e24682b13..be2ad528b 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -9,12 +9,14 @@ def initialize(overrides = {}) end end attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier + attr_accessor :assertion_consumer_service_binding attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format attr_accessor :authn_context attr_accessor :idp_slo_target_url attr_accessor :name_identifier_value attr_accessor :sessionindex attr_accessor :assertion_consumer_logout_service_url + attr_accessor :assertion_consumer_logout_service_binding attr_accessor :compress_request attr_accessor :double_quote_xml_attribute_values attr_accessor :passive @@ -23,7 +25,15 @@ def initialize(overrides = {}) private - DEFAULTS = {:compress_request => true, :sign_request => false, :double_quote_xml_attribute_values => false, :digest_method => "SHA1", :signature_method => "SHA1"} + DEFAULTS = { + :assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + :assertion_consumer_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect", + :compress_request => true, + :sign_request => false, + :double_quote_xml_attribute_values => false, + :digest_method => "SHA1", + :signature_method => "SHA1" + } end end end From a6b899d1e152c7f481fa4c44c7c0ff600d1d036e Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Mon, 12 May 2014 10:09:30 -0400 Subject: [PATCH 15/35] bumping version --- lib/onelogin/ruby-saml/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/onelogin/ruby-saml/version.rb b/lib/onelogin/ruby-saml/version.rb index 452692bfb..75abc8115 100644 --- a/lib/onelogin/ruby-saml/version.rb +++ b/lib/onelogin/ruby-saml/version.rb @@ -1,5 +1,5 @@ module OneLogin module RubySaml - VERSION = '0.8.1' + VERSION = '0.9.0' end end From bee6eec5cbfba7cd86b9bbeedbc44a4fac9afd3b Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Fri, 6 Jun 2014 15:12:41 -0400 Subject: [PATCH 16/35] parse WantAuthnRequestsSigned in metadata parsing --- lib/onelogin/ruby-saml/idp_metadata_parser.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/onelogin/ruby-saml/idp_metadata_parser.rb b/lib/onelogin/ruby-saml/idp_metadata_parser.rb index 2491c77a7..4240d59f2 100644 --- a/lib/onelogin/ruby-saml/idp_metadata_parser.rb +++ b/lib/onelogin/ruby-saml/idp_metadata_parser.rb @@ -22,6 +22,7 @@ def parse(idp_metadata) OneLogin::RubySaml::Settings.new.tap do |settings| settings.entity_id = entity_id settings.idp_sso_target_url = single_signon_service_url + settings.sign_request = sign_request settings.idp_slo_target_url = single_logout_service_url settings.idp_cert = certificate settings.idp_cert_fingerprint = fingerprint @@ -57,6 +58,13 @@ def certificate end end + def sign_request + @sign_request ||= begin + node = REXML::XPath.first(document, "/md:IDPSSODescriptor", { "md" => METADATA }) + (node.attributes["WantAuthnRequestsSigned"] || false) if node + end + end + def fingerprint @fingerprint ||= begin if certificate From 8d3c825319688b302dba8484fa6246db9e03aa2b Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Thu, 12 Jun 2014 16:30:48 -0400 Subject: [PATCH 17/35] * Add xml declaration to XML docs * use saml2p tag instead of samlp on RequestAuthn --- lib/onelogin/ruby-saml/authrequest.rb | 2 +- lib/xml_security.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index d95233683..ec4002d39 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -50,7 +50,7 @@ def create_authentication_xml_doc(settings) request_doc = XMLSecurity::RequestDocument.new request_doc.uuid = uuid - root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" } + root = request_doc.add_element "saml2p:AuthnRequest", { "xmlns:saml2p" => "urn:oasis:names:tc:SAML:2.0:protocol" } root.attributes['ID'] = uuid root.attributes['IssueInstant'] = time root.attributes['Version'] = "2.0" diff --git a/lib/xml_security.rb b/lib/xml_security.rb index 8f2f9f412..33ec0a820 100644 --- a/lib/xml_security.rb +++ b/lib/xml_security.rb @@ -38,6 +38,13 @@ class BaseDocument < REXML::Document C14N = "http://www.w3.org/2001/10/xml-exc-c14n#" DSIG = "http://www.w3.org/2000/09/xmldsig#" + def initialize(source = nil) + super(source) + xml_delc = REXML::XMLDecl.new + xml_delc.encoding = "UTF-8" + self << xml_delc + end + def canon_algorithm(element) algorithm = element if algorithm.is_a?(REXML::Element) From 942fbb513643e4c4b53f6452c9a2340ebfe4d42d Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Wed, 18 Jun 2014 10:51:46 -0400 Subject: [PATCH 18/35] Support for multiple NameIDFormats in metadata --- lib/onelogin/ruby-saml/metadata.rb | 25 +++++++++++++++++++++---- test/metadata_test.rb | 16 +++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index dc470361d..6ec24dc6c 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -33,9 +33,15 @@ def generate(settings) "index" => 0 } end - if settings.name_identifier_format != nil - name_id = sp_sso.add_element "md:NameIDFormat" - name_id.text = settings.name_identifier_format + if settings.name_identifier_format + formats = settings.name_identifier_format + if !formats.is_a?(Array) + formats = [formats] + end + formats.each do |format| + nf = sp_sso.add_element "md:NameIDFormat" + nf.text = format + end end if settings.assertion_consumer_service_url != nil sp_sso.add_element "md:AssertionConsumerService", { @@ -47,12 +53,23 @@ def generate(settings) end # Add KeyDescriptor if requests are signed - if settings.sign_request && !settings.certificate.nil? + if !settings.certificate.nil? + # Add signing Cert kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" } ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} xd = ki.add_element "ds:X509Data" xc = xd.add_element "ds:X509Certificate" xc.text = Base64.encode64(settings.certificate.to_der) + + # Add encryption Cert + kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" } + ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"} + xd = ki.add_element "ds:X509Data" + xc = xd.add_element "ds:X509Certificate" + xc.text = Base64.encode64(settings.certificate.to_der) + end + + if settings.name_identifier_format.is_a?(Array) end # With OpenSSO, it might be required to also include diff --git a/test/metadata_test.rb b/test/metadata_test.rb index d37e46d95..c01006a7c 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -10,7 +10,7 @@ def setup end should "should generate Service Provider Metadata" do - xml_text = OneLogin::RubySaml::Metadata.new.generate(settings) + xml_text = OneLogin::RubySaml::Metadata.new.generate(@settings) # assert correct xml declaration start = "\n Date: Wed, 18 Jun 2014 10:52:52 -0400 Subject: [PATCH 19/35] Include xml declaration on request documents --- lib/onelogin/ruby-saml/authrequest.rb | 2 +- lib/xml_security.rb | 4 +--- test/logoutrequest_test.rb | 3 ++- test/request_test.rb | 8 ++++++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index ec4002d39..d95233683 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -50,7 +50,7 @@ def create_authentication_xml_doc(settings) request_doc = XMLSecurity::RequestDocument.new request_doc.uuid = uuid - root = request_doc.add_element "saml2p:AuthnRequest", { "xmlns:saml2p" => "urn:oasis:names:tc:SAML:2.0:protocol" } + root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" } root.attributes['ID'] = uuid root.attributes['IssueInstant'] = time root.attributes['Version'] = "2.0" diff --git a/lib/xml_security.rb b/lib/xml_security.rb index 33ec0a820..be738ba01 100644 --- a/lib/xml_security.rb +++ b/lib/xml_security.rb @@ -40,9 +40,7 @@ class BaseDocument < REXML::Document def initialize(source = nil) super(source) - xml_delc = REXML::XMLDecl.new - xml_delc.encoding = "UTF-8" - self << xml_delc + self << REXML::XMLDecl.new("1.0", "UTF-8") end def canon_algorithm(element) diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index 7f29cd909..64438e03b 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -14,7 +14,8 @@ class RequestTest < Test::Unit::TestCase inflated = decode_saml_request_payload(unauth_url) - assert_match /^/, inflated + assert_match //, inflated + assert_match //, inflated assert_match //, decoded + assert_match //, inflated assert_match / Date: Wed, 18 Jun 2014 12:55:55 -0400 Subject: [PATCH 20/35] Allow multiple NameIDPolicy elements in AuthnReqeust --- lib/onelogin/ruby-saml/authrequest.rb | 9 ++++++--- lib/onelogin/ruby-saml/metadata.rb | 3 --- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index d95233683..2783b4f98 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -67,12 +67,15 @@ def create_authentication_xml_doc(settings) issuer.text = settings.issuer end if settings.name_identifier_format != nil - root.add_element "samlp:NameIDPolicy", { + formats = settings.name_identifier_format.is_a?(Array) ? settings.name_identifier_format : [settings.name_identifier_format] + formats.each do |format| + root.add_element "samlp:NameIDPolicy", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", # Might want to make AllowCreate a setting? "AllowCreate" => "true", - "Format" => settings.name_identifier_format - } + "Format" => format + } + end end # BUG fix here -- if an authn_context is defined, add the tags with an "exact" diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index 6ec24dc6c..e322dde2d 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -69,9 +69,6 @@ def generate(settings) xc.text = Base64.encode64(settings.certificate.to_der) end - if settings.name_identifier_format.is_a?(Array) - end - # With OpenSSO, it might be required to also include # # From 8d9d6cc1e3a1abdda039e7de636c55c8c8b4a353 Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Fri, 10 Oct 2014 23:17:19 -0400 Subject: [PATCH 21/35] Fix response parsing --- lib/onelogin/ruby-saml/logoutrequest.rb | 4 ++-- lib/onelogin/ruby-saml/logoutresponse.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 83b096a43..b40bb7ad0 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -35,12 +35,12 @@ def create(settings, params={}) def create_unauth_xml_doc(settings, params) - time = Time.new().strftime("%Y-%m-%dT%H:%M:%S") + time = Time.new().strftime("%Y-%m-%dT%H:%M:%S")+"Z" request_doc = XMLSecurity::RequestDocument.new root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" } root.attributes['ID'] = @uuid - root.attributes['IssueInstant'] = time + root.attributes['IssueInstant'] = time root.attributes['Version'] = "2.0" if settings.issuer diff --git a/lib/onelogin/ruby-saml/logoutresponse.rb b/lib/onelogin/ruby-saml/logoutresponse.rb index cf9a56d70..37110df21 100644 --- a/lib/onelogin/ruby-saml/logoutresponse.rb +++ b/lib/onelogin/ruby-saml/logoutresponse.rb @@ -31,7 +31,7 @@ def initialize(response, settings = nil, options = {}) @options = options @response = decode_raw_response(response) - @document = XMLSecurity::SignedDocument.new(response) + @document = XMLSecurity::SignedDocument.new(@response) end def validate! From aa138d1c64369b260786653358dbb4bb28d6b7dc Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Tue, 14 Oct 2014 10:41:34 -0400 Subject: [PATCH 22/35] refacor logoutrequest/response to have parse/create methods --- lib/onelogin/ruby-saml/logoutrequest.rb | 79 +++++++++++++++---- lib/onelogin/ruby-saml/logoutresponse.rb | 98 +++++++++++++++++++++--- test/logoutrequest_test.rb | 37 +++++---- test/logoutresponse_test.rb | 26 +++---- 4 files changed, 189 insertions(+), 51 deletions(-) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index b40bb7ad0..9de6aa3e6 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -7,39 +7,83 @@ module OneLogin module RubySaml include REXML class Logoutrequest + + ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" + PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" + STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success" - attr_reader :uuid # Can be obtained if neccessary - def initialize - @uuid = "_" + UUID.new.generate + attr_reader :request # Can be obtained if neccessary + attr_accessor :params + + def initialize(request, doc, settings) + @request = request + @document = doc + @settings = settings end - def create(settings, params={}) + def self.create(settings, params={}) request_doc = create_unauth_xml_doc(settings, params) request = "" request_doc.write(request) + req = new(request, request_doc, settings) + req.params = params + return req + end - deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5] - base64_request = Base64.encode64(deflated_request) - encoded_request = CGI.escape(base64_request) + def encoded_message + deflated_request = Zlib::Deflate.deflate(@request, 9)[2..-5] + msg = Base64.encode64(deflated_request) + msg + end - params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?' - request_params = "#{params_prefix}SAMLRequest=#{encoded_request}" + def logout_url + params_prefix = (@settings.idp_slo_target_url =~ /\?/) ? '&' : '?' + request_params = "#{params_prefix}SAMLRequest=#{CGI.escape(encoded_message.gsub("\n",'').gsub("\r",''))}" - params.each_pair do |key, value| + @params.each_pair do |key, value| request_params << "&#{key}=#{CGI.escape(value.to_s)}" end - @logout_url = settings.idp_slo_target_url + request_params + @settings.idp_slo_target_url + request_params + + end + + def decode(encoded) + Base64.decode64(encoded) + end + + def inflate(deflated) + zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS) + zlib.inflate(deflated) + end + + def decode_raw_request(request) + if request =~ /^ "urn:oasis:names:tc:SAML:2.0:protocol" } - root.attributes['ID'] = @uuid + root.attributes['ID'] = uuid root.attributes['IssueInstant'] = time root.attributes['Version'] = "2.0" @@ -82,6 +126,15 @@ def create_unauth_xml_doc(settings, params) request_doc end + + + def uuid + @uuid ||= begin + node = REXML::XPath.first(@document, "/p:LogoutRequest", { "p" => PROTOCOL}) + node.nil? ? nil : node.attributes['ID'] + end + end + end end end diff --git a/lib/onelogin/ruby-saml/logoutresponse.rb b/lib/onelogin/ruby-saml/logoutresponse.rb index 37110df21..3137642f9 100644 --- a/lib/onelogin/ruby-saml/logoutresponse.rb +++ b/lib/onelogin/ruby-saml/logoutresponse.rb @@ -9,6 +9,7 @@ class Logoutresponse ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion" PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol" + STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success" # For API compability, this is mutable. attr_accessor :settings @@ -25,13 +26,92 @@ class Logoutresponse # It will validate that the logout response matches the ID of the request. # You can also do this yourself through the in_response_to accessor. # - def initialize(response, settings = nil, options = {}) + def initialize(response, document, settings = nil, options = {}) raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? - self.settings = settings - + @settings = settings @options = options - @response = decode_raw_response(response) - @document = XMLSecurity::SignedDocument.new(@response) + @response = response + @document = document + end + + + def self.parse(response, settings = nil, options = {}) + raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? + settings = settings + + options = options + resp = decode_raw_response(response) + document = XMLSecurity::SignedDocument.new(resp) + + + new(resp, document, settings, options) + end + + + def encode_message + resp = @response + + deflated_resp = Zlib::Deflate.deflate(resp, 9)[2..-5] + base64_resp = Base64.encode64(deflated_resp) + + return base64_resp + end + + + def logout_url + params_prefix = (@settings.idp_slo_target_url =~ /\?/) ? '&' : '?' + request_params = "#{params_prefix}SAMLResponse=#{encoded_message}" + + params.each_pair do |key, value| + request_params << "&#{key}=#{CGI.escape(value.to_s)}" + end + @settings.idp_slo_target_url + request_params + end + + def self.create(settings, params={}, status_code=STATUS_SUCCESS, status_message="Logout Successful") + doc = create_unauth_xml_doc(settings, params, status_code,status_message) + resp = "" + doc.write(resp) + + newl(resp, doc, settings, params) + end + + def self.create_unauth_xml_doc(settings, params={}, status_code = STATUS_SUCCESS, status_message="Logout Successful") + + time = Time.new().strftime("%Y-%m-%dT%H:%M:%S")+"Z" + + response_doc = XMLSecurity::RequestDocument.new + root = response_doc.add_element "samlp:LogoutResponse", { "xmlns:samlp" => PROTOCOL, "xmlns:saml" => ASSERTION } + uuid = "_" + UUID.new.generate + root.attributes['ID'] = uuid + root.attributes['IssueInstant'] = time + root.attributes['Version'] = "2.0" + + if settings.idp_slo_target_url + root.attributes['Destination'] = idp_slo_target_url + end + + if params.key? :in_response_to + root.attributes['InResponseTo'] = params[:in_response_to] + end + + + if settings.issuer + issuer = root.add_element "saml:Issuer", { "xmlns:saml" => ASSERTION} + issuer.text = settings.issuer + else + fail ArgumentError, "No issuer supplied" + end + + if success + status = root.add_element "samlp:Status", { "xmlns:samlp" => PROTOCOL} + status.add_element "samlp:StatusCode", { "xmlns:samlp" => PROTOCOL, "Value" => status_code} + status.add_element "samlp:Message", { "xmlns:samlp" => PROTOCOL, "Value" => status_message} + else + + end + + end def validate! @@ -45,7 +125,7 @@ def validate(soft = true) end def success?(soft = true) - unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success" + unless status_code == STATUS_SUCCESS return soft ? false : validation_error("Bad status code. Expected , but was: <#@status_code> ") end true @@ -75,16 +155,16 @@ def status_code private - def decode(encoded) + def self.decode(encoded) Base64.decode64(encoded) end - def inflate(deflated) + def self.inflate(deflated) zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS) zlib.inflate(deflated) end - def decode_raw_response(response) + def self.decode_raw_response(response) if response =~ /^/, inflated - assert_match //, inflated) + assert_match(/ nil }) + unauth_url = OneLogin::RubySaml::Logoutrequest.create(settings, { :hello => nil }).logout_url assert unauth_url =~ /&hello=$/ - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :foo => "bar" }) + unauth_url = OneLogin::RubySaml::Logoutrequest.create(settings, { :foo => "bar" }).logout_url assert unauth_url =~ /&foo=bar$/ end @@ -32,10 +32,10 @@ class RequestTest < Test::Unit::TestCase sessionidx = UUID.new.generate settings.sessionindex = sessionidx - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :name_id => "there" }) + unauth_url = OneLogin::RubySaml::Logoutrequest.create(settings, { :name_id => "there" }).logout_url inflated = decode_saml_request_payload(unauth_url) - assert_match /), inflated end @@ -46,10 +46,10 @@ class RequestTest < Test::Unit::TestCase name_identifier_value = "abc123" settings.name_identifier_value = name_identifier_value - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :name_id => "there" }) + unauth_url = OneLogin::RubySaml::Logoutrequest.create(settings, { :name_id => "there" }).logout_url inflated = decode_saml_request_payload(unauth_url) - assert_match /), inflated end @@ -58,7 +58,7 @@ class RequestTest < Test::Unit::TestCase settings.idp_slo_target_url = "http://example.com/" settings.name_identifier_format = nil - assert_raises(OneLogin::RubySaml::ValidationError) { OneLogin::RubySaml::Logoutrequest.new.create(settings) } + assert_raises(OneLogin::RubySaml::ValidationError) { OneLogin::RubySaml::Logoutrequest.create(settings).logout_url } end context "when the target url doesn't contain a query string" do @@ -67,7 +67,7 @@ class RequestTest < Test::Unit::TestCase settings.idp_slo_target_url = "http://example.com/" settings.name_identifier_value = "f00f00" - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings) + unauth_url = OneLogin::RubySaml::Logoutrequest.create(settings).logout_url assert unauth_url =~ /^http:\/\/example.com\?SAMLRequest/ end end @@ -78,7 +78,7 @@ class RequestTest < Test::Unit::TestCase settings.idp_slo_target_url = "http://example.com/?field=value" settings.name_identifier_value = "f00f00" - unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings) + unauth_url = OneLogin::RubySaml::Logoutrequest.create(settings).logout_url assert unauth_url =~ /^http:\/\/example.com\?field=value&SAMLRequest/ end end @@ -89,10 +89,14 @@ class RequestTest < Test::Unit::TestCase settings.idp_slo_target_url = "http://example.com/?field=value" settings.name_identifier_value = "f00f00" - unauth_req = OneLogin::RubySaml::Logoutrequest.new - unauth_url = unauth_req.create(settings) + unauth_req = OneLogin::RubySaml::Logoutrequest.create(settings) + unauth_url = unauth_req.logout_url inflated = decode_saml_request_payload(unauth_url) + + puts " \n\n#{inflated} \n\n #{unauth_req.request}" + + assert_match %r[ID='#{unauth_req.uuid}'], inflated end end @@ -107,8 +111,8 @@ class RequestTest < Test::Unit::TestCase settings.certificate = ruby_saml_cert settings.private_key = ruby_saml_key - unauth_req = OneLogin::RubySaml::Logoutrequest.new - unauth_url = unauth_req.create(settings) + unauth_req = OneLogin::RubySaml::Logoutrequest.create(settings) + unauth_url = unauth_req.logout_url inflated = decode_saml_request_payload(unauth_url) assert_match %r[([a-zA-Z0-9/+=]+)], inflated @@ -121,6 +125,7 @@ def decode_saml_request_payload(unauth_url) decoded = Base64.decode64(payload) zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS) + inflated = zstream.inflate(decoded) zstream.finish zstream.close diff --git a/test/logoutresponse_test.rb b/test/logoutresponse_test.rb index 73995fcc7..ab321ded1 100644 --- a/test/logoutresponse_test.rb +++ b/test/logoutresponse_test.rb @@ -9,20 +9,20 @@ class RubySamlTest < Test::Unit::TestCase assert_raises(ArgumentError) { OneLogin::RubySaml::Logoutresponse.new(nil) } end should "default to empty settings" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new( valid_response) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse( valid_response) assert logoutresponse.settings.nil? end should "accept constructor-injected settings" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_response, settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(valid_response, settings) assert !logoutresponse.settings.nil? end should "accept constructor-injected options" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_response, nil, { :foo => :bar} ) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(valid_response, nil, { :foo => :bar} ) assert !logoutresponse.options.empty? end should "support base64 encoded responses" do expected_response = valid_response - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(Base64.encode64(expected_response), settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(Base64.encode64(expected_response), settings) assert_equal expected_response, logoutresponse.response end @@ -32,7 +32,7 @@ class RubySamlTest < Test::Unit::TestCase should "validate the response" do in_relation_to_request_id = random_id - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_response({:uuid => in_relation_to_request_id}), settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(valid_response({:uuid => in_relation_to_request_id}), settings) assert logoutresponse.validate @@ -47,14 +47,14 @@ class RubySamlTest < Test::Unit::TestCase expected_request_id = "_some_other_expected_uuid" opts = { :matches_request_id => expected_request_id} - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_response, settings, opts) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(valid_response, settings, opts) assert !logoutresponse.validate assert_not_equal expected_request_id, logoutresponse.in_response_to end should "invalidate responses with wrong request status" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_response, settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(unsuccessful_response, settings) assert !logoutresponse.validate assert !logoutresponse.success? @@ -65,7 +65,7 @@ class RubySamlTest < Test::Unit::TestCase should "validates good responses" do in_relation_to_request_id = random_id - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_response({:uuid => in_relation_to_request_id}), settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(valid_response({:uuid => in_relation_to_request_id}), settings) logoutresponse.validate! end @@ -75,32 +75,32 @@ class RubySamlTest < Test::Unit::TestCase expected_request_id = "_some_other_expected_id" opts = { :matches_request_id => expected_request_id} - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(valid_response, settings, opts) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(valid_response, settings, opts) assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate! } end should "raise validation error for wrong request status" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_response, settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(unsuccessful_response, settings) assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate! } end should "raise validation error when in bad state" do # no settings - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_response) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(unsuccessful_response) assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate! } end should "raise validation error when in lack of issuer setting" do bad_settings = settings bad_settings.issuer = nil - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(unsuccessful_response, bad_settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(unsuccessful_response, bad_settings) assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate! } end should "raise error for invalid xml" do - logoutresponse = OneLogin::RubySaml::Logoutresponse.new(invalid_xml_response, settings) + logoutresponse = OneLogin::RubySaml::Logoutresponse.parse(invalid_xml_response, settings) assert_raises(OneLogin::RubySaml::ValidationError) { logoutresponse.validate! } end From eeaf080a71802f2a691965e9da95e0a0cd7ade51 Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Tue, 14 Oct 2014 11:13:17 -0400 Subject: [PATCH 23/35] allow us to set logger --- lib/onelogin/ruby-saml/logging.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/onelogin/ruby-saml/logging.rb b/lib/onelogin/ruby-saml/logging.rb index a6e45ad3f..5a1e4d279 100644 --- a/lib/onelogin/ruby-saml/logging.rb +++ b/lib/onelogin/ruby-saml/logging.rb @@ -2,13 +2,16 @@ module OneLogin module RubySaml class Logging + def self.logger=(logger) + @logger = logger + end def self.debug(message) return if !!ENV["ruby-saml/testing"] if defined? Rails Rails.logger.debug message else - puts message + @logger.debug(message) end end @@ -18,7 +21,7 @@ def self.info(message) if defined? Rails Rails.logger.info message else - puts message + @logger.info(message) end end end From 94136b6b72c0041fba26ae088538e0f19cbdf875 Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Tue, 14 Oct 2014 11:14:43 -0400 Subject: [PATCH 24/35] remove debug logging, only log if there is a logger --- lib/onelogin/ruby-saml/logging.rb | 4 ++-- test/logoutrequest_test.rb | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/onelogin/ruby-saml/logging.rb b/lib/onelogin/ruby-saml/logging.rb index 5a1e4d279..9e2640d75 100644 --- a/lib/onelogin/ruby-saml/logging.rb +++ b/lib/onelogin/ruby-saml/logging.rb @@ -11,7 +11,7 @@ def self.debug(message) if defined? Rails Rails.logger.debug message else - @logger.debug(message) + @logger.debug(message) unless @logger.nil? end end @@ -21,7 +21,7 @@ def self.info(message) if defined? Rails Rails.logger.info message else - @logger.info(message) + @logger.info(message) unless @logger.nil? end end end diff --git a/test/logoutrequest_test.rb b/test/logoutrequest_test.rb index 7be6668ad..9c1b979ae 100644 --- a/test/logoutrequest_test.rb +++ b/test/logoutrequest_test.rb @@ -94,7 +94,6 @@ class RequestTest < Test::Unit::TestCase inflated = decode_saml_request_payload(unauth_url) - puts " \n\n#{inflated} \n\n #{unauth_req.request}" assert_match %r[ID='#{unauth_req.uuid}'], inflated From ec6f2c304203256eb8453293910e77252e582a1f Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Mon, 20 Oct 2014 13:29:20 -0400 Subject: [PATCH 25/35] some minor formatting fixes --- lib/onelogin/ruby-saml/logoutrequest.rb | 69 +++++++++++++++++------- lib/onelogin/ruby-saml/logoutresponse.rb | 4 -- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 9de6aa3e6..28db4b413 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -13,8 +13,8 @@ class Logoutrequest STATUS_SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success" - attr_reader :request # Can be obtained if neccessary - attr_accessor :params + attr_reader :request, :document # Can be obtained if neccessary + attr_accessor :params, :settings def initialize(request, doc, settings) @request = request @@ -49,16 +49,16 @@ def logout_url end - def decode(encoded) + def self.decode(encoded) Base64.decode64(encoded) end - def inflate(deflated) + def self.inflate(deflated) zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS) zlib.inflate(deflated) end - def decode_raw_request(request) + def self.decode_raw_request(request) if request =~ /^ "urn:oasis:names:tc:SAML:2.0:protocol", - "Comparison" => "exact", - } - class_ref = requested_context.add_element "saml:AuthnContextClassRef", { - "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion", - } - class_ref.text = settings.authn_context - end if settings.sign_request && settings.private_key && settings.certificate request_doc.sign_document(settings.private_key, settings.certificate, settings.signature_method, settings.digest_method) @@ -127,6 +114,15 @@ def self.create_unauth_xml_doc(settings, params) request_doc end + def issuer + @issuer ||= begin + node = REXML::XPath.first(@document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION }) + node ||= REXML::XPath.first(@document, "/p:LogoutRequest/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION }) + node.nil? ? nil : node.text + end + end + + def uuid @uuid ||= begin @@ -135,6 +131,43 @@ def uuid end end + def name_id + @name_id ||= begin + node = REXML::XPath.first(@document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) + node ||= REXML::XPath.first(@document, "/p:LogoutRequest/a:Assertion/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) + node.nil? ? nil : node.text + end + end + + + def attributes + {} + end + + def validate! + validate(false) + end + + def validate(soft = true) + return false unless valid_saml?(soft) + end + + def valid_saml?(soft = true) + Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do + @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd')) + @xml = Nokogiri::XML(@document.to_s) + end + if soft + @schema.validate(@xml).map{ return false } + else + @schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") } + end + end + + + + + end end end diff --git a/lib/onelogin/ruby-saml/logoutresponse.rb b/lib/onelogin/ruby-saml/logoutresponse.rb index 3137642f9..935483875 100644 --- a/lib/onelogin/ruby-saml/logoutresponse.rb +++ b/lib/onelogin/ruby-saml/logoutresponse.rb @@ -37,13 +37,9 @@ def initialize(response, document, settings = nil, options = {}) def self.parse(response, settings = nil, options = {}) raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil? - settings = settings - - options = options resp = decode_raw_response(response) document = XMLSecurity::SignedDocument.new(resp) - new(resp, document, settings, options) end From 4254281b71ffdb253915b182723d29603ebe0752 Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Mon, 20 Oct 2014 16:20:58 -0400 Subject: [PATCH 26/35] more updates for logout compatability --- lib/onelogin/ruby-saml/logoutrequest.rb | 8 ++++++++ lib/onelogin/ruby-saml/logoutresponse.rb | 25 +++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 28db4b413..743dd1f62 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -131,6 +131,14 @@ def uuid end end + def session_index + @session_index ||= begin + node = REXML::XPath.first(@document, "/p:LogoutRequest/p:SessionIndex", { "p" => PROTOCOL, "a" => ASSERTION}) + node.nil? ? nil : node.text + end + end + + def name_id @name_id ||= begin node = REXML::XPath.first(@document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) diff --git a/lib/onelogin/ruby-saml/logoutresponse.rb b/lib/onelogin/ruby-saml/logoutresponse.rb index 935483875..3b309a371 100644 --- a/lib/onelogin/ruby-saml/logoutresponse.rb +++ b/lib/onelogin/ruby-saml/logoutresponse.rb @@ -44,7 +44,7 @@ def self.parse(response, settings = nil, options = {}) end - def encode_message + def encoded_message resp = @response deflated_resp = Zlib::Deflate.deflate(resp, 9)[2..-5] @@ -55,12 +55,13 @@ def encode_message def logout_url - params_prefix = (@settings.idp_slo_target_url =~ /\?/) ? '&' : '?' + params_prefix = (@settings.assertion_consumer_service_url =~ /\?/) ? '&' : '?' request_params = "#{params_prefix}SAMLResponse=#{encoded_message}" - params.each_pair do |key, value| - request_params << "&#{key}=#{CGI.escape(value.to_s)}" - end + # TODO support request_params like such + # params.each_pair do |key, value| + # request_params << "&#{key}=#{CGI.escape(value.to_s)}" + # end @settings.idp_slo_target_url + request_params end @@ -69,7 +70,7 @@ def self.create(settings, params={}, status_code=STATUS_SUCCESS, status_message= resp = "" doc.write(resp) - newl(resp, doc, settings, params) + new(resp, doc, settings, params) end def self.create_unauth_xml_doc(settings, params={}, status_code = STATUS_SUCCESS, status_message="Logout Successful") @@ -84,7 +85,7 @@ def self.create_unauth_xml_doc(settings, params={}, status_code = STATUS_SUCCESS root.attributes['Version'] = "2.0" if settings.idp_slo_target_url - root.attributes['Destination'] = idp_slo_target_url + root.attributes['Destination'] = settings.idp_slo_target_url end if params.key? :in_response_to @@ -99,13 +100,9 @@ def self.create_unauth_xml_doc(settings, params={}, status_code = STATUS_SUCCESS fail ArgumentError, "No issuer supplied" end - if success - status = root.add_element "samlp:Status", { "xmlns:samlp" => PROTOCOL} - status.add_element "samlp:StatusCode", { "xmlns:samlp" => PROTOCOL, "Value" => status_code} - status.add_element "samlp:Message", { "xmlns:samlp" => PROTOCOL, "Value" => status_message} - else - - end + status_el = root.add_element "samlp:Status", { "xmlns:samlp" => PROTOCOL} + status_el.add_element "samlp:StatusCode", { "xmlns:samlp" => PROTOCOL, "Value" => status_code} + status_el.add_element "samlp:Message", { "xmlns:samlp" => PROTOCOL, "Value" => status_message} end From b86d6ac6547810b3d63b919d8c2b58449ba19c56 Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Wed, 29 Oct 2014 15:43:15 -0400 Subject: [PATCH 27/35] add "create_params" to logoutrequest --- lib/onelogin/ruby-saml/logoutrequest.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 743dd1f62..6386e86c9 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -49,6 +49,8 @@ def logout_url end + + def self.decode(encoded) Base64.decode64(encoded) end @@ -114,6 +116,21 @@ def self.create_unauth_xml_doc(settings, params) request_doc end + + def create_params(settings, params={}) + params = {} if params.nil? + + Logging.debug "Created Logoutrequest: #{request}" + + request_params = {"SAMLRequest" => encoded_message} + + params.each_pair do |key, value| + request_params[key] = value.to_s + end + + request_params + end + def issuer @issuer ||= begin node = REXML::XPath.first(@document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION }) From 8da6ea24fcf7e6b482709b86581baef8c9c21921 Mon Sep 17 00:00:00 2001 From: Stephen Gregory Date: Thu, 30 Oct 2014 12:19:57 -0400 Subject: [PATCH 28/35] add extra compress override to create_params --- lib/onelogin/ruby-saml/logoutrequest.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/onelogin/ruby-saml/logoutrequest.rb b/lib/onelogin/ruby-saml/logoutrequest.rb index 6386e86c9..814b0de80 100644 --- a/lib/onelogin/ruby-saml/logoutrequest.rb +++ b/lib/onelogin/ruby-saml/logoutrequest.rb @@ -31,9 +31,10 @@ def self.create(settings, params={}) return req end - def encoded_message - deflated_request = Zlib::Deflate.deflate(@request, 9)[2..-5] - msg = Base64.encode64(deflated_request) + def encoded_message(compress = true) + encoding_request = request + encoding_request = Zlib::Deflate.deflate(encoding_request, 9)[2..-5] if (settings.compress_request && compress) + msg = Base64.encode64(encoding_request) msg end @@ -117,12 +118,12 @@ def self.create_unauth_xml_doc(settings, params) end - def create_params(settings, params={}) + def create_params(settings, params={}, compress=true) params = {} if params.nil? Logging.debug "Created Logoutrequest: #{request}" - request_params = {"SAMLRequest" => encoded_message} + request_params = {"SAMLRequest" => encoded_message(compress)} params.each_pair do |key, value| request_params[key] = value.to_s From 73da664e93a0ba2560d08f916a18d28e8bc1456c Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Thu, 20 Nov 2014 14:22:07 -0500 Subject: [PATCH 29/35] make the auth request id accessible --- lib/onelogin/ruby-saml/authrequest.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index 2783b4f98..5b5c5e09d 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -10,6 +10,12 @@ module RubySaml include REXML class Authrequest + attr_accessor :uuid + + def initialize + @uuid = "_" + UUID.new.generate + end + def create(settings, params = {}) params = create_params(settings, params) params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?' @@ -44,7 +50,6 @@ def create_params(settings, params={}) end def create_authentication_xml_doc(settings) - uuid = "_" + UUID.new.generate time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") # Create AuthnRequest root element using REXML request_doc = XMLSecurity::RequestDocument.new From b1a8df75cc6e707ede502d042a3876fc07deb686 Mon Sep 17 00:00:00 2001 From: Dan McFadden Date: Thu, 20 Nov 2014 14:22:22 -0500 Subject: [PATCH 30/35] provide method to get in_response_to from SAML response --- lib/onelogin/ruby-saml/response.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index cb1e49ef9..14210e539 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -41,6 +41,13 @@ def name_id end end + def in_response_to + @in_reponse_to ||= begin + node = REXML::XPath.first(document, "/p:Response", { "p" => PROTOCOL }) + node.attributes["InResponseTo"] + end + end + def sessionindex @sessionindex ||= begin node = xpath_first_from_signed_assertion('/a:AuthnStatement') From fdc36fd5d17a2a2490c4875a4fc63bc316a54705 Mon Sep 17 00:00:00 2001 From: Bill Barbour Date: Wed, 25 Nov 2015 12:02:54 -0500 Subject: [PATCH 31/35] Updates SAMLResponse object to be able to decrypt an encrypted assertion --- lib/onelogin/ruby-saml/response.rb | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 14210e539..91d8fb1ba 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -23,6 +23,7 @@ def initialize(response, options = {}) @options = options @response = (response =~ /^") + begin_tag = actual_output.index("".length - 1)) + end + end + + return @clean_assertion + end + + def response + if encrypted? && !@private_key.nil? + @response = decrypt_assertion + end + + return @response + end + private def validation_error(message) @@ -131,6 +194,10 @@ def validation_error(message) end def validate(soft = true) + if encrypted? + return true + end + validate_structure(soft) && validate_response_state(soft) && validate_conditions(soft) && @@ -167,6 +234,12 @@ def validate_response_state(soft = true) end def xpath_first_from_signed_assertion(subelt=nil) + unless @clean_assertion.nil? + doc = XMLSecurity::SignedDocument.new(@clean_assertion) + node = REXML::XPath.first(doc, "//saml:NameID") + return node + end + node = REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION }) node ||= REXML::XPath.first(document, "/p:Response[@ID='#{document.signed_element_id}']/a:Assertion#{subelt}", { "p" => PROTOCOL, "a" => ASSERTION }) node From 4c48669e04b6ccf4607e4e5e33c692b3f4a69cbd Mon Sep 17 00:00:00 2001 From: Bill Barbour Date: Mon, 30 Nov 2015 15:42:41 -0500 Subject: [PATCH 32/35] Verify fingerprints match even when encrypted --- lib/onelogin/ruby-saml/response.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 91d8fb1ba..4bb359736 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -195,7 +195,7 @@ def validation_error(message) def validate(soft = true) if encrypted? - return true + return document.validate_document(get_fingerprint, soft) end validate_structure(soft) && From 3315e01ececc4a3be9d7c893661ed31d025c801d Mon Sep 17 00:00:00 2001 From: Bill Barbour Date: Mon, 30 Nov 2015 15:47:07 -0500 Subject: [PATCH 33/35] Remove my WA for not having the right cert altogether as it is not necessary. Right cert works --- lib/onelogin/ruby-saml/response.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 4bb359736..1d2f0d697 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -194,10 +194,6 @@ def validation_error(message) end def validate(soft = true) - if encrypted? - return document.validate_document(get_fingerprint, soft) - end - validate_structure(soft) && validate_response_state(soft) && validate_conditions(soft) && From 74f29a74c39a019a790078b4882a19ca1c3ef3ad Mon Sep 17 00:00:00 2001 From: Bill Barbour Date: Tue, 1 Dec 2015 09:40:18 -0500 Subject: [PATCH 34/35] Added way to set digest method to SHA 256 --- lib/onelogin/ruby-saml/authrequest.rb | 4 ++++ lib/onelogin/ruby-saml/response.rb | 2 +- lib/onelogin/ruby-saml/settings.rb | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index 5b5c5e09d..95b00e3e8 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -97,6 +97,10 @@ def create_authentication_xml_doc(settings) class_ref.text = settings.authn_context end + if settings.use_sha256 + settings.digest_method = XMLSecurity::RequestDocument::SHA256 + end + if settings.sign_request && settings.private_key && settings.certificate request_doc.sign_document(settings.private_key, settings.certificate, settings.signature_method, settings.digest_method) end diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 1d2f0d697..c0cdc875a 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -152,7 +152,7 @@ def decrypt_assertion pk = OpenSSL::PKey::RSA.new(@private_key) data_key = pk.private_decrypt(Base64.decode64(encrypted_key.text), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) - # Dervice the encryption alogrithm from mthe document + # Derive the encryption alogrithm from the document algorithm = "aes-256-cbc" # default algorithm # In future we need to map values in response with valid ciphers. e.g. aes256-cbc vs aes-256-cbc diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index 9e86be1cd..c5ddf8edf 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -22,7 +22,7 @@ def initialize(overrides = {}) attr_accessor :double_quote_xml_attribute_values attr_accessor :passive attr_accessor :protocol_binding - attr_accessor :sign_request, :certificate, :private_key, :digest_method, :signature_method + attr_accessor :sign_request, :certificate, :private_key, :digest_method, :signature_method, :use_sha256 private @@ -32,6 +32,7 @@ def initialize(overrides = {}) :compress_request => true, :sign_request => false, :double_quote_xml_attribute_values => false, + :use_sha256 => false, :digest_method => "SHA1", :signature_method => "SHA1" } From 3e8bdbc08ad43e7b59bd36623052ea88ffe871ea Mon Sep 17 00:00:00 2001 From: Bill Barbour Date: Tue, 1 Dec 2015 09:58:36 -0500 Subject: [PATCH 35/35] Removed unnecessary property --- lib/onelogin/ruby-saml/authrequest.rb | 4 ---- lib/onelogin/ruby-saml/settings.rb | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/onelogin/ruby-saml/authrequest.rb b/lib/onelogin/ruby-saml/authrequest.rb index 95b00e3e8..5b5c5e09d 100644 --- a/lib/onelogin/ruby-saml/authrequest.rb +++ b/lib/onelogin/ruby-saml/authrequest.rb @@ -97,10 +97,6 @@ def create_authentication_xml_doc(settings) class_ref.text = settings.authn_context end - if settings.use_sha256 - settings.digest_method = XMLSecurity::RequestDocument::SHA256 - end - if settings.sign_request && settings.private_key && settings.certificate request_doc.sign_document(settings.private_key, settings.certificate, settings.signature_method, settings.digest_method) end diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index c5ddf8edf..9e86be1cd 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -22,7 +22,7 @@ def initialize(overrides = {}) attr_accessor :double_quote_xml_attribute_values attr_accessor :passive attr_accessor :protocol_binding - attr_accessor :sign_request, :certificate, :private_key, :digest_method, :signature_method, :use_sha256 + attr_accessor :sign_request, :certificate, :private_key, :digest_method, :signature_method private @@ -32,7 +32,6 @@ def initialize(overrides = {}) :compress_request => true, :sign_request => false, :double_quote_xml_attribute_values => false, - :use_sha256 => false, :digest_method => "SHA1", :signature_method => "SHA1" }