@@ -88,6 +88,23 @@ def name_id_format
88
88
89
89
alias_method :nameid_format , :name_id_format
90
90
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
91
108
92
109
# Gets the SessionIndex from the AuthnStatement.
93
110
# Could be used to be stored in the local session in order
@@ -115,7 +132,8 @@ def sessionindex
115
132
# attributes['name']
116
133
#
117
134
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
118
- #
135
+ # @raise [ValidationError] if there are 2+ Attribute with the same Name
136
+ #
119
137
def attributes
120
138
@attr_statements ||= begin
121
139
attributes = Attributes . new
@@ -130,6 +148,11 @@ def attributes
130
148
end
131
149
132
150
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
+
133
156
values = node . elements . collect { |e |
134
157
if ( e . elements . nil? || e . elements . size == 0 )
135
158
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
@@ -175,25 +198,31 @@ def success?
175
198
#
176
199
def status_code
177
200
@status_code ||= begin
178
- node = REXML ::XPath . first (
201
+ nodes = REXML ::XPath . match (
179
202
document ,
180
203
"/p:Response/p:Status/p:StatusCode" ,
181
204
{ "p" => PROTOCOL }
182
205
)
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
184
210
end
185
211
end
186
212
187
213
# @return [String] the StatusMessage value from a SAML Response.
188
214
#
189
215
def status_message
190
216
@status_message ||= begin
191
- node = REXML ::XPath . first (
217
+ nodes = REXML ::XPath . match (
192
218
document ,
193
219
"/p:Response/p:Status/p:StatusMessage" ,
194
220
{ "p" => PROTOCOL }
195
221
)
196
- node . text if node
222
+ if nodes . size == 1
223
+ node = nodes [ 0 ]
224
+ node . text if node
225
+ end
197
226
end
198
227
end
199
228
@@ -219,26 +248,6 @@ def not_on_or_after
219
248
@not_on_or_after ||= parse_time ( conditions , "NotOnOrAfter" )
220
249
end
221
250
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
-
242
251
# @return [String|nil] The InResponseTo attribute from the SAML Response.
243
252
#
244
253
def in_response_to
@@ -303,15 +312,19 @@ def validate(collect_errors = false)
303
312
:validate_id ,
304
313
:validate_success_status ,
305
314
:validate_num_assertion ,
315
+ :validate_no_duplicated_attributes ,
306
316
:validate_signed_elements ,
307
317
:validate_structure ,
308
318
:validate_in_response_to ,
319
+ :validate_one_conditions ,
309
320
:validate_conditions ,
321
+ :validate_one_authnstatement ,
310
322
:validate_audience ,
311
323
:validate_destination ,
312
324
:validate_issuer ,
313
325
:validate_session_expiration ,
314
326
:validate_subject_confirmation ,
327
+ :validate_name_id ,
315
328
:validate_signature
316
329
]
317
330
@@ -400,6 +413,7 @@ def validate_version
400
413
# @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
401
414
#
402
415
def validate_num_assertion
416
+ error_msg = "SAML Response must contain 1 assertion"
403
417
assertions = REXML ::XPath . match (
404
418
document ,
405
419
"//a:Assertion" ,
@@ -412,7 +426,35 @@ def validate_num_assertion
412
426
)
413
427
414
428
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
416
458
end
417
459
418
460
true
@@ -516,7 +558,14 @@ def validate_audience
516
558
# @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
517
559
#
518
560
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?
520
569
521
570
unless destination == settings . assertion_consumer_service_url
522
571
error_msg = "The response was received at #{ destination } instead of #{ settings . assertion_consumer_service_url } "
@@ -526,6 +575,34 @@ def validate_destination
526
575
true
527
576
end
528
577
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
+
529
606
# Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
530
607
# If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
531
608
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
@@ -558,6 +635,31 @@ def validate_conditions
558
635
def validate_issuer
559
636
return true if settings . idp_entity_id . nil?
560
637
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
+
561
663
issuers . each do |issuer |
562
664
unless URI . parse ( issuer ) == URI . parse ( settings . idp_entity_id )
563
665
error_msg = "Doesn't match the issuer, expected: <#{ settings . idp_entity_id } >, but was: <#{ issuer } >"
@@ -631,6 +733,27 @@ def validate_subject_confirmation
631
733
true
632
734
end
633
735
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
+
634
757
# Validates the Signature
635
758
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
636
759
# @raise [ValidationError] if soft == false and validation fails
0 commit comments