@@ -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,7 +132,8 @@ def sessionindex
115132 # attributes['name']
116133 #
117134 # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
118- #
135+ # @raise [ValidationError] if there are 2+ Attribute with the same Name
136+ #
119137 def attributes
120138 @attr_statements ||= begin
121139 attributes = Attributes . new
@@ -130,6 +148,11 @@ def attributes
130148 end
131149
132150 name = node . attributes [ "Name" ]
151+
152+ if options [ :check_duplicated_attributes ] && attributes . include? ( name )
153+ raise ValidationError . new ( "Found an Attribute element with duplicated Name" )
154+ end
155+
133156 values = node . elements . collect { |e |
134157 if ( e . elements . nil? || e . elements . size == 0 )
135158 # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
@@ -175,25 +198,31 @@ def success?
175198 #
176199 def status_code
177200 @status_code ||= begin
178- node = REXML ::XPath . first (
201+ nodes = REXML ::XPath . match (
179202 document ,
180203 "/p:Response/p:Status/p:StatusCode" ,
181204 { "p" => PROTOCOL }
182205 )
183- node . attributes [ "Value" ] if node && node . attributes
206+ if nodes . size == 1
207+ node = nodes [ 0 ]
208+ node . attributes [ "Value" ] if node && node . attributes
209+ end
184210 end
185211 end
186212
187213 # @return [String] the StatusMessage value from a SAML Response.
188214 #
189215 def status_message
190216 @status_message ||= begin
191- node = REXML ::XPath . first (
217+ nodes = REXML ::XPath . match (
192218 document ,
193219 "/p:Response/p:Status/p:StatusMessage" ,
194220 { "p" => PROTOCOL }
195221 )
196- node . text if node
222+ if nodes . size == 1
223+ node = nodes [ 0 ]
224+ node . text if node
225+ end
197226 end
198227 end
199228
@@ -219,26 +248,6 @@ def not_on_or_after
219248 @not_on_or_after ||= parse_time ( conditions , "NotOnOrAfter" )
220249 end
221250
222- # Gets the Issuers (from Response and Assertion).
223- # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
224- # @return [Array] Array with the Issuers (REXML::Element)
225- #
226- def issuers
227- @issuers ||= begin
228- issuers = [ ]
229- nodes = REXML ::XPath . match (
230- document ,
231- "/p:Response/a:Issuer" ,
232- { "p" => PROTOCOL , "a" => ASSERTION }
233- )
234- nodes += xpath_from_signed_assertion ( "/a:Issuer" )
235- nodes . each do |node |
236- issuers << node . text if node . text
237- end
238- issuers . uniq
239- end
240- end
241-
242251 # @return [String|nil] The InResponseTo attribute from the SAML Response.
243252 #
244253 def in_response_to
@@ -303,15 +312,19 @@ def validate(collect_errors = false)
303312 :validate_id ,
304313 :validate_success_status ,
305314 :validate_num_assertion ,
315+ :validate_no_duplicated_attributes ,
306316 :validate_signed_elements ,
307317 :validate_structure ,
308318 :validate_in_response_to ,
319+ :validate_one_conditions ,
309320 :validate_conditions ,
321+ :validate_one_authnstatement ,
310322 :validate_audience ,
311323 :validate_destination ,
312324 :validate_issuer ,
313325 :validate_session_expiration ,
314326 :validate_subject_confirmation ,
327+ :validate_name_id ,
315328 :validate_signature
316329 ]
317330
@@ -400,6 +413,7 @@ def validate_version
400413 # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
401414 #
402415 def validate_num_assertion
416+ error_msg = "SAML Response must contain 1 assertion"
403417 assertions = REXML ::XPath . match (
404418 document ,
405419 "//a:Assertion" ,
@@ -412,7 +426,35 @@ def validate_num_assertion
412426 )
413427
414428 unless assertions . size + encrypted_assertions . size == 1
415- return append_error ( "SAML Response must contain 1 assertion" )
429+ return append_error ( error_msg )
430+ end
431+
432+ unless decrypted_document . nil?
433+ assertions = REXML ::XPath . match (
434+ decrypted_document ,
435+ "//a:Assertion" ,
436+ { "a" => ASSERTION }
437+ )
438+ unless assertions . size == 1
439+ return append_error ( error_msg )
440+ end
441+ end
442+
443+ true
444+ end
445+
446+ # Validates that there are not duplicated attributes
447+ # If fails, the error is added to the errors array
448+ # @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True
449+ # @raise [ValidationError] if soft == false and validation fails
450+ #
451+ def validate_no_duplicated_attributes
452+ if options [ :check_duplicated_attributes ]
453+ begin
454+ attributes
455+ rescue ValidationError => e
456+ return append_error ( e . message )
457+ end
416458 end
417459
418460 true
@@ -516,7 +558,14 @@ def validate_audience
516558 # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
517559 #
518560 def validate_destination
519- return true if destination . nil? || destination . empty? || settings . assertion_consumer_service_url . nil? || settings . assertion_consumer_service_url . empty?
561+ return true if destination . nil?
562+
563+ if destination . empty?
564+ error_msg = "The response has an empty Destination value"
565+ return append_error ( error_msg )
566+ end
567+
568+ return true if settings . assertion_consumer_service_url . nil? || settings . assertion_consumer_service_url . empty?
520569
521570 unless destination == settings . assertion_consumer_service_url
522571 error_msg = "The response was received at #{ destination } instead of #{ settings . assertion_consumer_service_url } "
@@ -526,6 +575,34 @@ def validate_destination
526575 true
527576 end
528577
578+ # Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
579+ # If fails, the error is added to the errors array
580+ # @return [Boolean] True if there is a conditions element and is unique
581+ #
582+ def validate_one_conditions
583+ conditions_nodes = xpath_from_signed_assertion ( '/a:Conditions' )
584+ unless conditions_nodes . size == 1
585+ error_msg = "The Assertion must include one Conditions element"
586+ return append_error ( error_msg )
587+ end
588+
589+ true
590+ end
591+
592+ # Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
593+ # If fails, the error is added to the errors array
594+ # @return [Boolean] True if there is a authnstatement element and is unique
595+ #
596+ def validate_one_authnstatement
597+ authnstatement_nodes = xpath_from_signed_assertion ( '/a:AuthnStatement' )
598+ unless authnstatement_nodes . size == 1
599+ error_msg = "The Assertion must include one AuthnStatement element"
600+ return append_error ( error_msg )
601+ end
602+
603+ true
604+ end
605+
529606 # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
530607 # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
531608 # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
@@ -558,6 +635,31 @@ def validate_conditions
558635 def validate_issuer
559636 return true if settings . idp_entity_id . nil?
560637
638+ issuers = [ ]
639+ issuer_response_nodes = REXML ::XPath . match (
640+ document ,
641+ "/p:Response/a:Issuer" ,
642+ { "p" => PROTOCOL , "a" => ASSERTION }
643+ )
644+
645+ unless issuer_response_nodes . size == 1
646+ error_msg = "Issuer of the Response not found or multiple."
647+ return append_error ( error_msg )
648+ end
649+
650+ doc = decrypted_document . nil? ? document : decrypted_document
651+ issuer_assertion_nodes = xpath_from_signed_assertion ( "/a:Issuer" )
652+ unless issuer_assertion_nodes . size == 1
653+ error_msg = "Issuer of the Assertion not found or multiple."
654+ return append_error ( error_msg )
655+ end
656+
657+ nodes = issuer_response_nodes + issuer_assertion_nodes
658+ nodes . each do |node |
659+ issuers << node . text if node . text
660+ end
661+ issuers . uniq
662+
561663 issuers . each do |issuer |
562664 unless URI . parse ( issuer ) == URI . parse ( settings . idp_entity_id )
563665 error_msg = "Doesn't match the issuer, expected: <#{ settings . idp_entity_id } >, but was: <#{ issuer } >"
@@ -631,6 +733,27 @@ def validate_subject_confirmation
631733 true
632734 end
633735
736+ # Validates the NameID element
737+ def validate_name_id
738+ if name_id_node . nil?
739+ if settings . security [ :want_name_id ]
740+ return append_error ( "No NameID element found in the assertion of the Response" )
741+ end
742+ else
743+ if name_id . nil? || name_id . empty?
744+ return append_error ( "An empty NameID value found" )
745+ end
746+
747+ unless settings . issuer . nil? || settings . issuer . empty? || name_id_spnamequalifier . nil? || name_id_spnamequalifier . empty?
748+ if name_id_spnamequalifier != settings . issuer
749+ return append_error ( "The SPNameQualifier value mistmatch the SP entityID value." )
750+ end
751+ end
752+ end
753+
754+ true
755+ end
756+
634757 # Validates the Signature
635758 # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
636759 # @raise [ValidationError] if soft == false and validation fails
0 commit comments