Skip to content

Commit 31d8500

Browse files
committed
Extract out RubySaml::XML::Decryptor
1 parent f360e3c commit 31d8500

File tree

8 files changed

+781
-422
lines changed

8 files changed

+781
-422
lines changed

lib/ruby_saml/response.rb

Lines changed: 136 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -85,38 +85,45 @@ def is_valid?(collect_errors = false)
8585
# @return [String] the NameID provided by the SAML response from the IdP.
8686
#
8787
def name_id
88-
@name_id ||= Utils.element_text(name_id_node)
88+
@name_id ||= if name_id_node.is_a?(REXML::Element)
89+
Utils.element_text(name_id_node)
90+
else
91+
name_id_node&.content
92+
end
8993
end
9094

9195
alias_method :nameid, :name_id
9296

9397
# @return [String] the NameID Format provided by the SAML response from the IdP.
9498
#
9599
def name_id_format
96-
@name_id_format ||=
97-
if name_id_node&.attribute("Format")
98-
name_id_node.attribute("Format").value
99-
end
100+
@name_id_format ||= if name_id_node.is_a?(REXML::Element)
101+
name_id_node&.attribute('Format')&.value
102+
else
103+
name_id_node&.[]('Format')
104+
end
100105
end
101106

102107
alias_method :nameid_format, :name_id_format
103108

104109
# @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
105110
#
106111
def name_id_spnamequalifier
107-
@name_id_spnamequalifier ||=
108-
if name_id_node&.attribute("SPNameQualifier")
109-
name_id_node.attribute("SPNameQualifier").value
110-
end
112+
@name_id_spnamequalifier ||= if name_id_node.is_a?(REXML::Element)
113+
name_id_node&.attribute('SPNameQualifier')&.value
114+
else
115+
name_id_node&.[]('SPNameQualifier')
116+
end
111117
end
112118

113119
# @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
114120
#
115121
def name_id_namequalifier
116-
@name_id_namequalifier ||=
117-
if name_id_node&.attribute("NameQualifier")
118-
name_id_node.attribute("NameQualifier").value
119-
end
122+
@name_id_namequalifier ||= if name_id_node.is_a?(REXML::Element)
123+
name_id_node&.attribute('NameQualifier')&.value
124+
else
125+
name_id_node&.[]('NameQualifier')
126+
end
120127
end
121128

122129
# Gets the SessionIndex from the AuthnStatement.
@@ -154,41 +161,78 @@ def attributes
154161
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
155162
stmt_elements.each do |stmt_element|
156163
stmt_element.elements.each do |attr_element|
157-
if attr_element.name == "EncryptedAttribute"
158-
node = decrypt_attribute(attr_element.dup)
164+
if attr_element.name == 'EncryptedAttribute'
165+
node = RubySaml::XML::Decryptor.decrypt_attribute(attr_element.dup, settings&.get_sp_decryption_keys)
159166
else
160167
node = attr_element
161168
end
162169

163-
name = node.attributes["Name"]
164-
165-
if options[:check_duplicated_attributes] && attributes.include?(name)
166-
raise ValidationError.new("Found an Attribute element with duplicated Name")
170+
if node.is_a?(Nokogiri::XML::Element)
171+
name, values = handle_nokogiri_attribute(node, attributes)
172+
else
173+
name, values = handle_rexml_attribute(node, attributes)
167174
end
175+
attributes.add(name, values)
176+
end
177+
end
168178

169-
values = node.elements.map do |e|
170-
if e.elements.nil? || e.elements.empty?
171-
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
172-
# otherwise the value is to be regarded as empty.
173-
%w[true 1].include?(e.attributes['xsi:nil']) ? nil : Utils.element_text(e)
174-
else
175-
# Explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
176-
# this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
177-
# identify the subject in an SP rather than email or other less opaque attributes
178-
# NameQualifier, if present is prefixed with a "/" to the value
179-
REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n|
180-
base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : ''
181-
"#{base_path}#{Utils.element_text(n)}"
182-
end
183-
end
184-
end
179+
attributes
180+
end
181+
end
182+
183+
def handle_rexml_attribute(node, attributes)
184+
name = node.attributes["Name"]
185185

186-
attributes.add(name, values.flatten)
186+
if options[:check_duplicated_attributes] && attributes.include?(name)
187+
raise ValidationError.new("Found an Attribute element with duplicated Name")
188+
end
189+
190+
values = node.elements.map do |e|
191+
if e.elements.nil? || e.elements.empty?
192+
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
193+
# otherwise the value is to be regarded as empty.
194+
%w[true 1].include?(e.attributes['xsi:nil']) ? nil : Utils.element_text(e)
195+
else
196+
# Explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
197+
# this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
198+
# identify the subject in an SP rather than email or other less opaque attributes
199+
# NameQualifier, if present is prefixed with a "/" to the value
200+
REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n|
201+
base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : ''
202+
"#{base_path}#{Utils.element_text(n)}"
187203
end
188204
end
205+
end.flatten
189206

190-
attributes
207+
[name, values]
208+
end
209+
210+
def handle_nokogiri_attribute(node, attributes)
211+
name = node['Name']
212+
213+
if options[:check_duplicated_attributes] && attributes.include?(name)
214+
raise ValidationError.new("Found an Attribute element with duplicated Name")
191215
end
216+
217+
values = node.elements.map do |e|
218+
if e.elements.nil? || e.elements.empty?
219+
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
220+
# otherwise the value is to be regarded as empty.
221+
%w[true 1].include?(e['xsi:nil']) ? nil : e&.content
222+
else
223+
# Explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
224+
# this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
225+
# identify the subject in an SP rather than email or other less opaque attributes
226+
# NameQualifier, if present is prefixed with a "/" to the value
227+
e.xpath('a:NameID', { "a" => ASSERTION }).map do |n|
228+
next unless (value = n&.content)
229+
base_path = n['NameQualifier'] ? "#{n['NameQualifier']}/" : ''
230+
"#{base_path}#{value}"
231+
end
232+
end
233+
end.flatten
234+
235+
[name, values]
192236
end
193237

194238
# Gets the SessionNotOnOrAfter from the AuthnStatement.
@@ -783,6 +827,7 @@ def validate_subject_confirmation
783827
valid_subject_confirmation = false
784828

785829
subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
830+
return validate_subject_confirmation_nokogiri(subject_confirmation_nodes) if subject_confirmation_nodes.first.is_a?(Nokogiri::XML::Element)
786831

787832
now = Time.now.utc
788833
subject_confirmation_nodes.each do |subject_confirmation|
@@ -816,6 +861,36 @@ def validate_subject_confirmation
816861
true
817862
end
818863

864+
def validate_subject_confirmation_nokogiri(subject_confirmation_nodes)
865+
valid_subject_confirmation = false
866+
867+
now = Time.now.utc
868+
subject_confirmation_nodes.each do |subject_confirmation|
869+
if subject_confirmation['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
870+
next
871+
end
872+
873+
confirmation_data_node = subject_confirmation.at_xpath('a:SubjectConfirmationData', { "a" => ASSERTION })
874+
875+
next unless confirmation_data_node
876+
877+
next if (confirmation_data_node['InResponseTo'] && confirmation_data_node['InResponseTo'] != in_response_to) ||
878+
(confirmation_data_node['NotBefore'] && now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
879+
(confirmation_data_node['NotOnOrAfter'] && now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
880+
(confirmation_data_node['Recipient'] && !options[:skip_recipient_check] && settings && confirmation_data_node['Recipient'] != settings.assertion_consumer_service_url)
881+
882+
valid_subject_confirmation = true
883+
break
884+
end
885+
886+
unless valid_subject_confirmation
887+
error_msg = "A valid SubjectConfirmation was not found on this Response"
888+
return append_error(error_msg)
889+
end
890+
891+
true
892+
end
893+
819894
# Validates the NameID element
820895
def validate_name_id
821896
if name_id_node.nil?
@@ -942,7 +1017,7 @@ def name_id_node
9421017
begin
9431018
encrypted_node = xpath_first_from_signed_assertion('/a:Subject/a:EncryptedID')
9441019
if encrypted_node
945-
decrypt_nameid(encrypted_node)
1020+
RubySaml::XML::Decryptor.decrypt_nameid(encrypted_node, settings&.get_sp_decryption_keys)
9461021
else
9471022
xpath_first_from_signed_assertion('/a:Subject/a:NameID')
9481023
end
@@ -965,7 +1040,7 @@ def cached_signed_assertion
9651040
if REXML::XPath.first(root, "a:Assertion", {"a" => ASSERTION})
9661041
assertion = REXML::XPath.first(root, "a:Assertion", {"a" => ASSERTION})
9671042
elsif REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION})
968-
assertion = decrypt_assertion(REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION}))
1043+
assertion = RubySaml::XML::Decryptor.decrypt_assertion(REXML::XPath.first(root, "a:EncryptedAssertion", {"a" => ASSERTION}), settings&.get_sp_decryption_keys)
9691044
end
9701045
elsif root.name == "Assertion"
9711046
assertion = root
@@ -985,11 +1060,16 @@ def signed_assertion
9851060
#
9861061
def xpath_first_from_signed_assertion(subelt = nil)
9871062
doc = signed_assertion
988-
REXML::XPath.first(
989-
doc,
990-
"./#{subelt}",
991-
SAML_NAMESPACES
992-
)
1063+
if doc.is_a?(REXML::Element)
1064+
REXML::XPath.first(
1065+
doc,
1066+
"./#{subelt}",
1067+
SAML_NAMESPACES
1068+
)
1069+
else
1070+
return if !subelt || subelt.empty?
1071+
doc.at_xpath("./#{subelt}", SAML_NAMESPACES)
1072+
end
9931073
end
9941074

9951075
# Extracts all the appearances that matchs the subelt (pattern)
@@ -999,97 +1079,24 @@ def xpath_first_from_signed_assertion(subelt = nil)
9991079
#
10001080
def xpath_from_signed_assertion(subelt = nil)
10011081
doc = signed_assertion
1002-
REXML::XPath.match(
1003-
doc,
1004-
"./#{subelt}",
1005-
SAML_NAMESPACES
1006-
)
1082+
if doc.is_a?(REXML::Element)
1083+
REXML::XPath.match(
1084+
doc,
1085+
"./#{subelt}",
1086+
SAML_NAMESPACES
1087+
)
1088+
else
1089+
return if !subelt || subelt.empty?
1090+
doc.xpath("./#{subelt}", SAML_NAMESPACES)
1091+
end
10071092
end
10081093

10091094
# Generates the decrypted_document
10101095
# @return [RubySaml::XML::SignedDocument] The SAML Response with the assertion decrypted
10111096
#
10121097
def generate_decrypted_document
1013-
if settings.nil? || settings.get_sp_decryption_keys.empty?
1014-
raise ValidationError.new('An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method')
1015-
end
1016-
1017-
document_copy = Marshal.load(Marshal.dump(document))
1018-
1019-
decrypt_assertion_from_document(document_copy)
1020-
end
1021-
1022-
# Obtains a SAML Response with the EncryptedAssertion element decrypted
1023-
# @param document_copy [RubySaml::XML::SignedDocument] A copy of the original SAML Response with the encrypted assertion
1024-
# @return [RubySaml::XML::SignedDocument] The SAML Response with the assertion decrypted
1025-
#
1026-
def decrypt_assertion_from_document(document_copy)
1027-
response_node = REXML::XPath.first(
1028-
document_copy,
1029-
"/p:Response/",
1030-
{ "p" => PROTOCOL }
1031-
)
1032-
encrypted_assertion_node = REXML::XPath.first(
1033-
document_copy,
1034-
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
1035-
SAML_NAMESPACES
1036-
)
1037-
response_node.add(decrypt_assertion(encrypted_assertion_node))
1038-
encrypted_assertion_node.remove
1039-
RubySaml::XML::SignedDocument.new(response_node.to_s)
1040-
end
1041-
1042-
# Decrypts an EncryptedAssertion element
1043-
# @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
1044-
# @return [REXML::Document] The decrypted EncryptedAssertion element
1045-
#
1046-
def decrypt_assertion(encrypted_assertion_node)
1047-
decrypt_element(encrypted_assertion_node, /(.*<\/(\w+:)?Assertion>)/m)
1048-
end
1049-
1050-
# Decrypts an EncryptedID element
1051-
# @param encrypted_id_node [REXML::Element] The EncryptedID element
1052-
# @return [REXML::Document] The decrypted EncrypedtID element
1053-
#
1054-
def decrypt_nameid(encrypted_id_node)
1055-
decrypt_element(encrypted_id_node, /(.*<\/(\w+:)?NameID>)/m)
1056-
end
1057-
1058-
# Decrypts an EncryptedAttribute element
1059-
# @param encrypted_attribute_node [REXML::Element] The EncryptedAttribute element
1060-
# @return [REXML::Document] The decrypted EncryptedAttribute element
1061-
#
1062-
def decrypt_attribute(encrypted_attribute_node)
1063-
decrypt_element(encrypted_attribute_node, /(.*<\/(\w+:)?Attribute>)/m)
1064-
end
1065-
1066-
# Decrypt an element
1067-
# @param encrypt_node [REXML::Element] The encrypted element
1068-
# @param regexp [Regexp] The regular expression to extract the decrypted data
1069-
# @return [REXML::Document] The decrypted element
1070-
#
1071-
def decrypt_element(encrypt_node, regexp)
1072-
if settings.nil? || settings.get_sp_decryption_keys.empty?
1073-
raise ValidationError.new("An #{encrypt_node.name} found and no SP private key found on the settings to decrypt it.")
1074-
end
1075-
1076-
node_header = if encrypt_node.name == 'EncryptedAttribute'
1077-
'<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
1078-
else
1079-
'<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
1080-
end
1081-
1082-
elem_plaintext = RubySaml::Utils.decrypt_multi(encrypt_node, settings.get_sp_decryption_keys)
1083-
1084-
# If we get some problematic noise in the plaintext after decrypting.
1085-
# This quick regexp parse will grab only the Element and discard the noise.
1086-
elem_plaintext = elem_plaintext.match(regexp)[0]
1087-
1088-
# To avoid namespace errors if saml namespace is not defined
1089-
# create a parent node first with the namespace defined
1090-
elem_plaintext = "#{node_header}#{elem_plaintext}</node>"
1091-
doc = REXML::Document.new(elem_plaintext)
1092-
doc.root[0]
1098+
noko = RubySaml::XML::Decryptor.decrypt_document(document.to_s, settings&.get_sp_decryption_keys)
1099+
RubySaml::XML::SignedDocument.new(noko.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML))
10931100
end
10941101

10951102
# Parse the attribute of a given node in Time format

0 commit comments

Comments
 (0)