diff --git a/README.md b/README.md index f2353e47e..c66bf00e4 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ We created a demo project for Rails4 that uses the latest version of this librar ### Supported versions of Ruby * 1.8.7 * 1.9.x +* 2.0.x * 2.1.x * 2.2.x * JRuby 1.7.19 @@ -386,7 +387,10 @@ The settings related to sign are stored in the `security` attribute of the setti ```ruby settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request - settings.security[:logout_responses_signed] = true # Enable or not signature on Logout Response + settings.security[:logout_responses_signed] = true # Enable or not + signature on Logout Response + settings.security[:want_assertions_signed] = true # Enable or not + the requirement of signed assertion settings.security[:metadata_signed] = true # Enable or not signature on Metadata settings.security[:digest_method] = XMLSecurity::Document::SHA1 diff --git a/lib/onelogin/ruby-saml/idp_metadata_parser.rb b/lib/onelogin/ruby-saml/idp_metadata_parser.rb index 7d0a343eb..7642f17e6 100644 --- a/lib/onelogin/ruby-saml/idp_metadata_parser.rb +++ b/lib/onelogin/ruby-saml/idp_metadata_parser.rb @@ -28,27 +28,30 @@ class IdpMetadataParser # IdP values # # @param (see IdpMetadataParser#get_idp_metadata) + # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object # @return (see IdpMetadataParser#get_idp_metadata) # @raise (see IdpMetadataParser#get_idp_metadata) - def parse_remote(url, validate_cert = true) + def parse_remote(url, validate_cert = true, options = {}) idp_metadata = get_idp_metadata(url, validate_cert) - parse(idp_metadata) + parse(idp_metadata, options) end # Parse the Identity Provider metadata and update the settings with the IdP values # @param idp_metadata [String] + # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object # - def parse(idp_metadata) + def parse(idp_metadata, options = {}) @document = REXML::Document.new(idp_metadata) - OneLogin::RubySaml::Settings.new.tap do |settings| + (options[:settings] || OneLogin::RubySaml::Settings.new).tap do |settings| settings.idp_entity_id = idp_entity_id settings.name_identifier_format = idp_name_id_format - settings.idp_sso_target_url = single_signon_service_url - settings.idp_slo_target_url = single_logout_service_url + settings.idp_sso_target_url = single_signon_service_url(options) + settings.idp_slo_target_url = single_logout_service_url(options) settings.idp_cert = certificate_base64 - settings.idp_cert_fingerprint = fingerprint + settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm) settings.idp_attribute_names = attribute_names + settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm) end end @@ -115,23 +118,61 @@ def idp_name_id_format node.text if node end + # @param binding_priority [Array] + # @return [String|nil] SingleSignOnService binding if exists + # + def single_signon_service_binding(binding_priority = nil) + nodes = REXML::XPath.match( + document, + "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding", + { "md" => METADATA } + ) + if binding_priority + values = nodes.map(&:value) + binding_priority.detect{ |binding| values.include? binding } + else + nodes.first.value if nodes.any? + end + end + + # @param options [Hash] # @return [String|nil] SingleSignOnService endpoint if exists # - def single_signon_service_url + def single_signon_service_url(options = {}) + binding = options[:sso_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" node = REXML::XPath.first( document, - "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location", + "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location", { "md" => METADATA } ) node.value if node end + # @param binding_priority [Array] + # @return [String|nil] SingleLogoutService binding if exists + # + def single_logout_service_binding(binding_priority = nil) + nodes = REXML::XPath.match( + document, + "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Binding", + { "md" => METADATA } + ) + if binding_priority + values = nodes.map(&:value) + binding_priority.detect{ |binding| values.include? binding } + else + nodes.first.value if nodes.any? + end + end + + # @param options [Hash] # @return [String|nil] SingleLogoutService endpoint if exists # - def single_logout_service_url + def single_logout_service_url(options = {}) + binding = options[:slo_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" node = REXML::XPath.first( document, - "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location", + "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location", { "md" => METADATA } ) node.value if node @@ -146,6 +187,14 @@ def certificate_base64 "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate", { "md" => METADATA, "ds" => DSIG } ) + + unless node + node = REXML::XPath.first( + document, + "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate", + { "md" => METADATA, "ds" => DSIG } + ) + end node.text if node end end @@ -161,11 +210,13 @@ def certificate # @return [String|nil] the SHA-1 fingerpint of the X509Certificate if it exists # - def fingerprint + def fingerprint(fingerprint_algorithm) @fingerprint ||= begin if certificate cert = OpenSSL::X509::Certificate.new(certificate) - Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":") + + fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new + fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":") end end end diff --git a/lib/onelogin/ruby-saml/metadata.rb b/lib/onelogin/ruby-saml/metadata.rb index d64dda071..14e6f9c87 100644 --- a/lib/onelogin/ruby-saml/metadata.rb +++ b/lib/onelogin/ruby-saml/metadata.rb @@ -29,8 +29,7 @@ def generate(settings, pretty_print=false) sp_sso = root.add_element "md:SPSSODescriptor", { "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol", "AuthnRequestsSigned" => settings.security[:authn_requests_signed], - # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set - "WantAssertionsSigned" => !!(settings.idp_cert_fingerprint || settings.idp_cert) + "WantAssertionsSigned" => settings.security[:want_assertions_signed], } # Add KeyDescriptor if messages will be signed / encrypted diff --git a/lib/onelogin/ruby-saml/response.rb b/lib/onelogin/ruby-saml/response.rb index 4c6b3e2b1..82ddddf2d 100644 --- a/lib/onelogin/ruby-saml/response.rb +++ b/lib/onelogin/ruby-saml/response.rb @@ -45,10 +45,10 @@ def initialize(response, options = {}) @options = options @soft = true - if !options.empty? && !options[:settings].nil? + unless options[:settings].nil? @settings = options[:settings] - if !options[:settings].soft.nil? - @soft = options[:settings].soft + unless @settings.soft.nil? + @soft = @settings.soft end end @@ -448,6 +448,10 @@ def validate_signed_elements return append_error("Found an unexpected number of Signature Element. SAML Response rejected") end + if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion") + return append_error("The Assertion of the Response is not signed and the SP requires it") + end + true end diff --git a/lib/onelogin/ruby-saml/settings.rb b/lib/onelogin/ruby-saml/settings.rb index 52be192ae..fa640c6ab 100644 --- a/lib/onelogin/ruby-saml/settings.rb +++ b/lib/onelogin/ruby-saml/settings.rb @@ -154,6 +154,7 @@ def get_sp_key :authn_requests_signed => false, :logout_requests_signed => false, :logout_responses_signed => false, + :want_assertions_signed => false, :metadata_signed => false, :embed_sign => false, :digest_method => XMLSecurity::Document::SHA1, diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index 1d39760bf..bf1128272 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -37,10 +37,10 @@ def initialize(request, options = {}) @options = options @soft = true - if !options.empty? && !options[:settings].nil? + unless options[:settings].nil? @settings = options[:settings] - if !options[:settings].soft.nil? - @soft = options[:settings].soft + unless @settings.soft.nil? + @soft = @settings.soft end end @@ -213,7 +213,7 @@ def validate_request_state # @raise [ValidationError] if soft == false and validation fails # def validate_issuer - return true if settings.idp_entity_id.nil? || issuer.nil? + return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil? unless URI.parse(issuer) == URI.parse(settings.idp_entity_id) return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>") diff --git a/test/idp_metadata_parser_test.rb b/test/idp_metadata_parser_test.rb index b60068b4c..ed1309deb 100644 --- a/test/idp_metadata_parser_test.rb +++ b/test/idp_metadata_parser_test.rb @@ -29,7 +29,33 @@ def initialize; end assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names + 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 end + + it "extract certificate from md:KeyDescriptor[@use='signing']" do + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata = read_response("idp_descriptor.xml") + settings = idp_metadata_parser.parse(idp_metadata) + 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 + end + + it "extract certificate from md:KeyDescriptor[@use='encryption']" do + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata = read_response("idp_descriptor.xml") + idp_metadata = idp_metadata.sub(/(.*?)<\/md:KeyDescriptor>/m, "") + settings = idp_metadata_parser.parse(idp_metadata) + 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 + end + + it "extract certificate from md:KeyDescriptor" do + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata = read_response("idp_descriptor.xml") + idp_metadata = idp_metadata.sub(/(.*?)<\/md:KeyDescriptor>/m, "") + idp_metadata = idp_metadata.sub('', '') + settings = idp_metadata_parser.parse(idp_metadata) + 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 + end + end describe "download and parse IdP descriptor file" do diff --git a/test/metadata_test.rb b/test/metadata_test.rb index 93e903c38..f866aff90 100644 --- a/test/metadata_test.rb +++ b/test/metadata_test.rb @@ -75,6 +75,20 @@ class MetadataTest < Minitest::Test assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") end + describe "WantAssertionsSigned" do + it "generates Service Provider Metadata with WantAssertionsSigned = false" do + settings.security[:want_assertions_signed] = false + assert_equal "false", spsso_descriptor.attribute("WantAssertionsSigned").value + assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") + end + + it "generates Service Provider Metadata with WantAssertionsSigned = true" do + settings.security[:want_assertions_signed] = true + assert_equal "true", spsso_descriptor.attribute("WantAssertionsSigned").value + assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd") + end + end + describe "when auth requests are signed" do let(:key_descriptors) do REXML::XPath.match( diff --git a/test/response_test.rb b/test/response_test.rb index ddbeecb45..a7ddd237f 100644 --- a/test/response_test.rb +++ b/test/response_test.rb @@ -997,6 +997,26 @@ class RubySamlTest < Minitest::Test end end + describe '#want_assertion_signed' do + before do + settings.security[:want_assertions_signed] = true + @signed_assertion = OneLogin::RubySaml::Response.new(response_document_with_signed_assertion, :settings => settings) + @no_signed_assertion = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings) + end + + + it 'returns false if :want_assertion_signed enabled and Assertion not signed' do + assert !@no_signed_assertion.send(:validate_signed_elements) + assert_includes @no_signed_assertion.errors, "The Assertion of the Response is not signed and the SP requires it" + + end + + it 'returns true if :want_assertion_signed enabled and Assertion is signed' do + assert @signed_assertion.send(:validate_signed_elements) + assert_empty @signed_assertion.errors + end + end + describe "retrieve nameID" do it 'is possible when nameID inside the assertion' do response_valid_signed.settings = settings