@@ -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