@@ -88,6 +88,23 @@ def name_id_format
8888
8989 alias_method :nameid_format , :name_id_format
9090
91+ # @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
92+ #
93+ def name_id_spnamequalifier
94+ @name_id_spnamequalifier ||=
95+ if name_id_node && name_id_node . attribute ( "SPNameQualifier" )
96+ name_id_node . attribute ( "SPNameQualifier" ) . value
97+ end
98+ end
99+
100+ # @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
101+ #
102+ def name_id_namequalifier
103+ @name_id_namequalifier ||=
104+ if name_id_node && name_id_node . attribute ( "NameQualifier" )
105+ name_id_node . attribute ( "NameQualifier" ) . value
106+ end
107+ end
91108
92109 # Gets the SessionIndex from the AuthnStatement.
93110 # Could be used to be stored in the local session in order
@@ -115,15 +132,15 @@ def sessionindex
115132 # attributes['name']
116133 #
117134 # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
118- #
135+ #
119136 def attributes
120137 @attr_statements ||= begin
121138 attributes = Attributes . new
122139
123140 stmt_elements = xpath_from_signed_assertion ( '/a:AttributeStatement' )
124141 stmt_elements . each do |stmt_element |
125142 stmt_element . elements . each do |attr_element |
126- name = attr_element . attributes [ "Name" ]
143+ name = attr_element . attributes [ "Name" ]
127144 values = attr_element . elements . collect { |e |
128145 if ( e . elements . nil? || e . elements . size == 0 )
129146 # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
@@ -169,25 +186,31 @@ def success?
169186 #
170187 def status_code
171188 @status_code ||= begin
172- node = REXML ::XPath . first (
189+ nodes = REXML ::XPath . match (
173190 document ,
174191 "/p:Response/p:Status/p:StatusCode" ,
175192 { "p" => PROTOCOL }
176193 )
177- node . attributes [ "Value" ] if node && node . attributes
194+ if nodes . size == 1
195+ node = nodes [ 0 ]
196+ node . attributes [ "Value" ] if node && node . attributes
197+ end
178198 end
179199 end
180200
181201 # @return [String] the StatusMessage value from a SAML Response.
182202 #
183203 def status_message
184204 @status_message ||= begin
185- node = REXML ::XPath . first (
205+ nodes = REXML ::XPath . match (
186206 document ,
187207 "/p:Response/p:Status/p:StatusMessage" ,
188208 { "p" => PROTOCOL }
189209 )
190- node . text if node
210+ if nodes . size == 1
211+ node = nodes [ 0 ]
212+ node . text if node
213+ end
191214 end
192215 end
193216
@@ -213,26 +236,6 @@ def not_on_or_after
213236 @not_on_or_after ||= parse_time ( conditions , "NotOnOrAfter" )
214237 end
215238
216- # Gets the Issuers (from Response and Assertion).
217- # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
218- # @return [Array] Array with the Issuers (REXML::Element)
219- #
220- def issuers
221- @issuers ||= begin
222- issuers = [ ]
223- nodes = REXML ::XPath . match (
224- document ,
225- "/p:Response/a:Issuer" ,
226- { "p" => PROTOCOL , "a" => ASSERTION }
227- )
228- nodes += xpath_from_signed_assertion ( "/a:Issuer" )
229- nodes . each do |node |
230- issuers << node . text if node . text
231- end
232- issuers . uniq
233- end
234- end
235-
236239 # @return [String|nil] The InResponseTo attribute from the SAML Response.
237240 #
238241 def in_response_to
@@ -298,15 +301,19 @@ def validate(collect_errors = false)
298301 :validate_success_status ,
299302 :validate_num_assertion ,
300303 :validate_no_encrypted_attributes ,
304+ :validate_no_duplicated_attributes ,
301305 :validate_signed_elements ,
302306 :validate_structure ,
303307 :validate_in_response_to ,
308+ :validate_one_conditions ,
304309 :validate_conditions ,
310+ :validate_one_authnstatement ,
305311 :validate_audience ,
306312 :validate_destination ,
307313 :validate_issuer ,
308314 :validate_session_expiration ,
309315 :validate_subject_confirmation ,
316+ :validate_name_id ,
310317 :validate_signature
311318 ]
312319
@@ -395,6 +402,7 @@ def validate_version
395402 # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
396403 #
397404 def validate_num_assertion
405+ error_msg = "SAML Response must contain 1 assertion"
398406 assertions = REXML ::XPath . match (
399407 document ,
400408 "//a:Assertion" ,
@@ -407,7 +415,18 @@ def validate_num_assertion
407415 )
408416
409417 unless assertions . size + encrypted_assertions . size == 1
410- return append_error ( "SAML Response must contain 1 assertion" )
418+ return append_error ( error_msg )
419+ end
420+
421+ unless decrypted_document . nil?
422+ assertions = REXML ::XPath . match (
423+ decrypted_document ,
424+ "//a:Assertion" ,
425+ { "a" => ASSERTION }
426+ )
427+ unless assertions . size == 1
428+ return append_error ( error_msg )
429+ end
411430 end
412431
413432 true
@@ -427,6 +446,28 @@ def validate_no_encrypted_attributes
427446 true
428447 end
429448
449+ # Validates that there are not duplicated attributes
450+ # If fails, the error is added to the errors array
451+ # @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True
452+ # @raise [ValidationError] if soft == false and validation fails
453+ #
454+ def validate_no_duplicated_attributes
455+ if options [ :check_duplicated_attributes ]
456+ processed_names = [ ]
457+ stmt_elements = xpath_from_signed_assertion ( '/a:AttributeStatement' )
458+ stmt_elements . each do |stmt_element |
459+ stmt_element . elements . each do |attr_element |
460+ name = attr_element . attributes [ "Name" ]
461+ if attributes . include? ( name )
462+ return append_error ( "Found an Attribute element with duplicated Name" )
463+ end
464+ processed_names . add ( name )
465+ end
466+ end
467+ end
468+
469+ true
470+ end
430471
431472 # Validates the Signed elements
432473 # If fails, the error is added to the errors array
@@ -526,7 +567,14 @@ def validate_audience
526567 # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
527568 #
528569 def validate_destination
529- return true if destination . nil? || destination . empty? || settings . assertion_consumer_service_url . nil? || settings . assertion_consumer_service_url . empty?
570+ return true if destination . nil?
571+
572+ if destination . empty?
573+ error_msg = "The response has an empty Destination value"
574+ return append_error ( error_msg )
575+ end
576+
577+ return true if settings . assertion_consumer_service_url . nil? || settings . assertion_consumer_service_url . empty?
530578
531579 unless destination == settings . assertion_consumer_service_url
532580 error_msg = "The response was received at #{ destination } instead of #{ settings . assertion_consumer_service_url } "
@@ -536,6 +584,34 @@ def validate_destination
536584 true
537585 end
538586
587+ # Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
588+ # If fails, the error is added to the errors array
589+ # @return [Boolean] True if there is a conditions element and is unique
590+ #
591+ def validate_one_conditions
592+ conditions_nodes = xpath_from_signed_assertion ( '/a:Conditions' )
593+ unless conditions_nodes . size == 1
594+ error_msg = "The Assertion must include one Conditions element"
595+ return append_error ( error_msg )
596+ end
597+
598+ true
599+ end
600+
601+ # Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
602+ # If fails, the error is added to the errors array
603+ # @return [Boolean] True if there is a authnstatement element and is unique
604+ #
605+ def validate_one_authnstatement
606+ authnstatement_nodes = xpath_from_signed_assertion ( '/a:AuthnStatement' )
607+ unless authnstatement_nodes . size == 1
608+ error_msg = "The Assertion must include one AuthnStatement element"
609+ return append_error ( error_msg )
610+ end
611+
612+ true
613+ end
614+
539615 # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
540616 # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
541617 # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
@@ -568,6 +644,31 @@ def validate_conditions
568644 def validate_issuer
569645 return true if settings . idp_entity_id . nil?
570646
647+ issuers = [ ]
648+ issuer_response_nodes = REXML ::XPath . match (
649+ document ,
650+ "/p:Response/a:Issuer" ,
651+ { "p" => PROTOCOL , "a" => ASSERTION }
652+ )
653+
654+ unless issuer_response_nodes . size == 1
655+ error_msg = "Issuer of the Response not found or multiple."
656+ return append_error ( error_msg )
657+ end
658+
659+ doc = decrypted_document . nil? ? document : decrypted_document
660+ issuer_assertion_nodes = xpath_from_signed_assertion ( "/a:Issuer" )
661+ unless issuer_assertion_nodes . size == 1
662+ error_msg = "Issuer of the Assertion not found or multiple."
663+ return append_error ( error_msg )
664+ end
665+
666+ nodes = issuer_response_nodes + issuer_assertion_nodes
667+ nodes . each do |node |
668+ issuers << node . text if node . text
669+ end
670+ issuers . uniq
671+
571672 issuers . each do |issuer |
572673 unless URI . parse ( issuer ) == URI . parse ( settings . idp_entity_id )
573674 error_msg = "Doesn't match the issuer, expected: <#{ settings . idp_entity_id } >, but was: <#{ issuer } >"
@@ -641,6 +742,27 @@ def validate_subject_confirmation
641742 true
642743 end
643744
745+ # Validates the NameID element
746+ def validate_name_id
747+ if name_id_node . nil?
748+ if settings . security [ :want_name_id ]
749+ return append_error ( "No NameID element found in the assertion of the Response" )
750+ end
751+ else
752+ if name_id . nil? || name_id . empty?
753+ return append_error ( "An empty NameID value found" )
754+ end
755+
756+ unless settings . issuer . nil? || settings . issuer . empty? || name_id_spnamequalifier . nil? || name_id_spnamequalifier . empty?
757+ if name_id_spnamequalifier != settings . issuer
758+ return append_error ( "The SPNameQualifier value mistmatch the SP entityID value." )
759+ end
760+ end
761+ end
762+
763+ true
764+ end
765+
644766 # Validates the Signature
645767 # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
646768 # @raise [ValidationError] if soft == false and validation fails
0 commit comments