From a3d9d8cc5e284c9662f33f1464ac9126b8c24864 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 30 Sep 2023 04:30:37 +0200 Subject: [PATCH 1/3] Add support on LogoutRequest with Encrypted NameID --- lib/onelogin/ruby-saml/slo_logoutrequest.rb | 46 +++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index 7bd1aa02..aa819497 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -62,10 +62,7 @@ def is_valid?(collect_errors = false) # @return [String] Gets the NameID of the Logout Request. # def name_id - @name_id ||= begin - node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) - Utils.element_text(node) - end + @name_id ||= Utils.element_text(name_id_node) end alias_method :nameid, :name_id @@ -73,15 +70,50 @@ def name_id # @return [String] Gets the NameID Format of the Logout Request. # def name_id_format - @name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) @name_id_format ||= - if @name_id_node && @name_id_node.attribute("Format") - @name_id_node.attribute("Format").value + if name_id_node && name_id_node.attribute("Format") + name_id_node.attribute("Format").value end end alias_method :nameid_format, :name_id_format + def name_id_node + @name_id_node ||= + begin + encrypted_node = REXML::XPath.first(document, "/p:LogoutRequest/a:EncryptedID", { "p" => PROTOCOL, "a" => ASSERTION }) + if encrypted_node + node = decrypt_nameid(encrypted_node) + else + node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION }) + end + end + end + + # Decrypts an EncryptedID element + # @param encryptedid_node [REXML::Element] The EncryptedID element + # @return [REXML::Document] The decrypted EncrypedtID element + # + def decrypt_nameid(encrypt_node) + + if settings.nil? || !settings.get_sp_key + raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') + end + + node_header = '' + + elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key) + # If we get some problematic noise in the plaintext after decrypting. + # This quick regexp parse will grab only the Element and discard the noise. + elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0] + + # To avoid namespace errors if saml namespace is not defined + # create a parent node first with the namespace defined + elem_plaintext = node_header + elem_plaintext + '' + doc = REXML::Document.new(elem_plaintext) + doc.root[0] + end + # @return [String|nil] Gets the ID attribute from the Logout Request. if exists. # def id From 37b714c3be347723fd27aaf9204b9bb487486d66 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sat, 30 Sep 2023 04:46:35 +0200 Subject: [PATCH 2/3] Restrict version of minitest to avoid test errors --- ruby-saml.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby-saml.gemspec b/ruby-saml.gemspec index a439c302..2ddce136 100644 --- a/ruby-saml.gemspec +++ b/ruby-saml.gemspec @@ -66,7 +66,7 @@ Gem::Specification.new do |s| s.add_development_dependency('simplecov-lcov', '>0.7.0') end - s.add_development_dependency('minitest', '~> 5.5') + s.add_development_dependency('minitest', '~> 5.5', '<5.19.0') s.add_development_dependency('mocha', '~> 0.14') if RUBY_VERSION < '2.0' From 8f9976eed0567ec3ba29b939de87dee247036788 Mon Sep 17 00:00:00 2001 From: Sixto Martin Date: Sun, 1 Oct 2023 00:17:27 +0200 Subject: [PATCH 3/3] Add test coverage --- lib/onelogin/ruby-saml/slo_logoutrequest.rb | 3 +-- .../slo_request_encrypted_nameid.xml | 6 +++++ test/slo_logoutrequest_test.rb | 25 +++++++++++++++++++ test/test_helper.rb | 9 +++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 test/logout_requests/slo_request_encrypted_nameid.xml diff --git a/lib/onelogin/ruby-saml/slo_logoutrequest.rb b/lib/onelogin/ruby-saml/slo_logoutrequest.rb index aa819497..6c4a8867 100644 --- a/lib/onelogin/ruby-saml/slo_logoutrequest.rb +++ b/lib/onelogin/ruby-saml/slo_logoutrequest.rb @@ -100,8 +100,6 @@ def decrypt_nameid(encrypt_node) raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it') end - node_header = '' - elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key) # If we get some problematic noise in the plaintext after decrypting. # This quick regexp parse will grab only the Element and discard the noise. @@ -109,6 +107,7 @@ def decrypt_nameid(encrypt_node) # To avoid namespace errors if saml namespace is not defined # create a parent node first with the namespace defined + node_header = '' elem_plaintext = node_header + elem_plaintext + '' doc = REXML::Document.new(elem_plaintext) doc.root[0] diff --git a/test/logout_requests/slo_request_encrypted_nameid.xml b/test/logout_requests/slo_request_encrypted_nameid.xml new file mode 100644 index 00000000..f9b185e7 --- /dev/null +++ b/test/logout_requests/slo_request_encrypted_nameid.xml @@ -0,0 +1,6 @@ + + https://app.onelogin.com/saml/metadata/SOMEACCOUNT + L99BsKQL2iq5chjY+wRj6AH3jYxv9L4tscPJaDdsPWvPG47toC903oxEhjd1p9EMWkSPqD/HclvRhjcNVmhfUa3clTMM5PpZS1+oin2cDNFgKDkEaCXsGRgfn44uUKbEfUHNaljC72qh0lBLnoJe7ZkJHbFMbsA8Cd4UBtHzp4Y= + +dLZt52QiV39ltBeRNUev0jlD9ReI7lM3EDgfktPgKeIs6bvsLz9feWhlnydd+NjbwXTsBQjEhm80/O8szYZZZpQB3H+khA76HJoFeDdhDgnVMqeXVWVkeSjcDFHg6TPLPyydSNcsBPBOqP093xCF7je0PUgkK45cj6aN/hs7TckxCbeuOv/klz6jxc24TyNoGg3Z1TA/HlS2ePVY77LhQgqhsZIL52LTG3BjAHVvpzSXyuYbeR5OeiYIM028Xyl + + \ No newline at end of file diff --git a/test/slo_logoutrequest_test.rb b/test/slo_logoutrequest_test.rb index 5825a62a..f7d6e7db 100644 --- a/test/slo_logoutrequest_test.rb +++ b/test/slo_logoutrequest_test.rb @@ -10,6 +10,7 @@ class RubySamlTest < Minitest::Test let(:settings) { OneLogin::RubySaml::Settings.new } let(:logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_document) } + let(:logout_request_encrypted_nameid) { OneLogin::RubySaml::SloLogoutrequest.new(logout_request_encrypted_nameid_document) } let(:invalid_logout_request) { OneLogin::RubySaml::SloLogoutrequest.new(invalid_logout_request_document) } before do @@ -87,6 +88,18 @@ class RubySamlTest < Minitest::Test it "extract the value of the name id element" do assert_equal "someone@example.org", logout_request.nameid end + + it 'is not possible when encryptID but no private key' do + assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid + end + end + + it "extract the value of the name id element inside an EncryptedId" do + settings.private_key = ruby_saml_key_text + logout_request_encrypted_nameid.settings = settings + assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid + end end describe "#nameid_format" do @@ -95,6 +108,18 @@ class RubySamlTest < Minitest::Test it "extract the format attribute of the name id element" do assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", logout_request.nameid_format end + + it 'is not possible when encryptID but no private key' do + assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do + assert_equal "someone@example.org", logout_request_encrypted_nameid.nameid + end + end + + it "extract the format attribute of the name id element" do + settings.private_key = ruby_saml_key_text + logout_request_encrypted_nameid.settings = settings + assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", logout_request_encrypted_nameid.nameid_format + end end describe "#issuer" do diff --git a/test/test_helper.rb b/test/test_helper.rb index 83dd904a..ae927ae5 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -237,6 +237,15 @@ def logout_request_document_with_name_id_format @logout_request_document_with_name_id_format end + def logout_request_encrypted_nameid_document + unless @logout_request_encrypted_nameid_document + xml = read_logout_request("slo_request_encrypted_nameid.xml") + deflated = Zlib::Deflate.deflate(xml, 9)[2..-5] + @logout_request_encrypted_nameid_document = Base64.encode64(deflated) + end + @logout_request_encrypted_nameid_document + end + def logout_request_xml_with_session_index @logout_request_xml_with_session_index ||= File.read(File.join(File.dirname(__FILE__), 'logout_requests', 'slo_request_with_session_index.xml')) end