@@ -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,15 +132,15 @@ def sessionindex
115
132
# attributes['name']
116
133
#
117
134
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
118
- #
135
+ #
119
136
def attributes
120
137
@attr_statements ||= begin
121
138
attributes = Attributes . new
122
139
123
140
stmt_elements = xpath_from_signed_assertion ( '/a:AttributeStatement' )
124
141
stmt_elements . each do |stmt_element |
125
142
stmt_element . elements . each do |attr_element |
126
- name = attr_element . attributes [ "Name" ]
143
+ name = attr_element . attributes [ "Name" ]
127
144
values = attr_element . elements . collect { |e |
128
145
if ( e . elements . nil? || e . elements . size == 0 )
129
146
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
@@ -169,25 +186,31 @@ def success?
169
186
#
170
187
def status_code
171
188
@status_code ||= begin
172
- node = REXML ::XPath . first (
189
+ nodes = REXML ::XPath . match (
173
190
document ,
174
191
"/p:Response/p:Status/p:StatusCode" ,
175
192
{ "p" => PROTOCOL }
176
193
)
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
178
198
end
179
199
end
180
200
181
201
# @return [String] the StatusMessage value from a SAML Response.
182
202
#
183
203
def status_message
184
204
@status_message ||= begin
185
- node = REXML ::XPath . first (
205
+ nodes = REXML ::XPath . match (
186
206
document ,
187
207
"/p:Response/p:Status/p:StatusMessage" ,
188
208
{ "p" => PROTOCOL }
189
209
)
190
- node . text if node
210
+ if nodes . size == 1
211
+ node = nodes [ 0 ]
212
+ node . text if node
213
+ end
191
214
end
192
215
end
193
216
@@ -213,26 +236,6 @@ def not_on_or_after
213
236
@not_on_or_after ||= parse_time ( conditions , "NotOnOrAfter" )
214
237
end
215
238
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
-
236
239
# @return [String|nil] The InResponseTo attribute from the SAML Response.
237
240
#
238
241
def in_response_to
@@ -298,15 +301,19 @@ def validate(collect_errors = false)
298
301
:validate_success_status ,
299
302
:validate_num_assertion ,
300
303
:validate_no_encrypted_attributes ,
304
+ :validate_no_duplicated_attributes ,
301
305
:validate_signed_elements ,
302
306
:validate_structure ,
303
307
:validate_in_response_to ,
308
+ :validate_one_conditions ,
304
309
:validate_conditions ,
310
+ :validate_one_authnstatement ,
305
311
:validate_audience ,
306
312
:validate_destination ,
307
313
:validate_issuer ,
308
314
:validate_session_expiration ,
309
315
:validate_subject_confirmation ,
316
+ :validate_name_id ,
310
317
:validate_signature
311
318
]
312
319
@@ -395,6 +402,7 @@ def validate_version
395
402
# @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
396
403
#
397
404
def validate_num_assertion
405
+ error_msg = "SAML Response must contain 1 assertion"
398
406
assertions = REXML ::XPath . match (
399
407
document ,
400
408
"//a:Assertion" ,
@@ -407,7 +415,18 @@ def validate_num_assertion
407
415
)
408
416
409
417
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
411
430
end
412
431
413
432
true
@@ -427,6 +446,28 @@ def validate_no_encrypted_attributes
427
446
true
428
447
end
429
448
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
430
471
431
472
# Validates the Signed elements
432
473
# If fails, the error is added to the errors array
@@ -526,7 +567,14 @@ def validate_audience
526
567
# @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
527
568
#
528
569
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?
530
578
531
579
unless destination == settings . assertion_consumer_service_url
532
580
error_msg = "The response was received at #{ destination } instead of #{ settings . assertion_consumer_service_url } "
@@ -536,6 +584,34 @@ def validate_destination
536
584
true
537
585
end
538
586
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
+
539
615
# Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
540
616
# If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
541
617
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
@@ -568,6 +644,31 @@ def validate_conditions
568
644
def validate_issuer
569
645
return true if settings . idp_entity_id . nil?
570
646
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
+
571
672
issuers . each do |issuer |
572
673
unless URI . parse ( issuer ) == URI . parse ( settings . idp_entity_id )
573
674
error_msg = "Doesn't match the issuer, expected: <#{ settings . idp_entity_id } >, but was: <#{ issuer } >"
@@ -641,6 +742,27 @@ def validate_subject_confirmation
641
742
true
642
743
end
643
744
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
+
644
766
# Validates the Signature
645
767
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
646
768
# @raise [ValidationError] if soft == false and validation fails
0 commit comments