ruby-saml 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of ruby-saml might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ebddaf61706d9ea22f5dfe8f8857d38633bd8b34
4
- data.tar.gz: 17e92a96f7ad6fde5764d5238f6c7abe165b2f9d
3
+ metadata.gz: 268a395d4ed90393709bec460cd818d644bd7269
4
+ data.tar.gz: 0507a8928e591569b9be618d2dac54b8c83a1847
5
5
  SHA512:
6
- metadata.gz: 340ffba316ae676dc6b12049f3e4936c74e6b296ab0fbbdc86906dcb322c5eb6430eabbe930bb3dc114bb0e8cfe7b0220e0c1ddbb31b98178a9f71318cfdf812
7
- data.tar.gz: e690b29654a0d1389278f7feee2d65d6ebba3118a634e727486845607c46a9460a546935f4fc78f89c4ce6cc339af555f35aa447f3f74ebab62714de0f917693
6
+ metadata.gz: 59e8e5300dcd4f521c19216686753cd2ce1a2480383bf6c873601a1031d96ef59241e13076a6b2e89cee61cc43f64c2afc383d6c985a8186cee0a7efe433aabd
7
+ data.tar.gz: 00dfd3b12d124c41282b7edce5ae4214f96d696314120776f0cbeadc6c86c35cfac1279b8d3ef0b23ff6559ffec6365681254da2784d86b8fa659a825989ba02
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Ruby SAML [![Build Status](https://secure.travis-ci.org/onelogin/ruby-saml.png)](http://travis-ci.org/onelogin/ruby-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/ruby-saml/badge.svg?branch=master%0A)](https://coveralls.io/r/onelogin/ruby-saml?branch=master%0A) [![Gem Version](https://badge.fury.io/rb/ruby-saml.svg)](http://badge.fury.io/rb/ruby-saml)
2
2
 
3
+ ## Updating from 1.3.x to 1.4.X
4
+
5
+ Version `1.4.0` is a recommended update for all Ruby SAML users as it includes security improvements.
6
+
3
7
  ## Updating from 1.2.x to 1.3.X
4
8
 
5
9
  Version `1.3.0` is a recommended update for all Ruby SAML users as it includes security fixes. It adds security improvements in order to prevent Signature wrapping attacks. [CVE-2016-5697](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5697)
@@ -92,7 +96,7 @@ require 'onelogin/ruby-saml/authrequest'
92
96
 
93
97
  ### Installation on Ruby 1.8.7
94
98
 
95
- This gem has a dependency on Nokogiri, which dropped support for Ruby 1.8.x in Nokogiri 1.6. When installing this gem on Ruby 1.8.7, you will need to make sure a version of Nokogiri prior to 1.6 is installed or specified if it hasn't been already.
99
+ This gem uses Nokogiri as a dependency, which dropped support for Ruby 1.8.x in Nokogiri 1.6. When installing this gem on Ruby 1.8.7, you will need to make sure a version of Nokogiri prior to 1.6 is installed or specified if it hasn't been already.
96
100
 
97
101
  Using `Gemfile`
98
102
 
@@ -121,7 +125,7 @@ OneLogin::RubySaml::Logging.logger = Logger.new(File.open('/var/log/ruby-saml.lo
121
125
 
122
126
  ## The Initialization Phase
123
127
 
124
- This is the first request you will get from the identity provider. It will hit your application at a specific URL (that you've announced as being your SAML initialization point). The response to this initialization, is a redirect back to the identity provider, which can look something like this (ignore the saml_settings method call for now):
128
+ This is the first request you will get from the identity provider. It will hit your application at a specific URL that you've announced as your SAML initialization point. The response to this initialization is a redirect back to the identity provider, which can look something like this (ignore the saml_settings method call for now):
125
129
 
126
130
  ```ruby
127
131
  def init
@@ -130,7 +134,7 @@ def init
130
134
  end
131
135
  ```
132
136
 
133
- Once you've redirected back to the identity provider, it will ensure that the user has been authorized and redirect back to your application for final consumption, this is can look something like this (the authorize_success and authorize_failure methods are specific to your application):
137
+ Once you've redirected back to the identity provider, it will ensure that the user has been authorized and redirect back to your application for final consumption. This can look something like this (the `authorize_success` and `authorize_failure` methods are specific to your application):
134
138
 
135
139
  ```ruby
136
140
  def consume
@@ -147,17 +151,17 @@ def consume
147
151
  end
148
152
  ```
149
153
 
150
- In the above there are a few assumptions in place, one being that the response.nameid is an email address. This is all handled with how you specify the settings that are in play via the saml_settings method. That could be implemented along the lines of this:
151
-
152
- If the assertion of the SAMLResponse is not encrypted, you can initialize the Response without the :settings parameter and set it later,
154
+ In the above there are a few assumptions, one being that `response.nameid` is an email address. This is all handled with how you specify the settings that are in play via the `saml_settings` method. That could be implemented along the lines of this:
153
155
 
154
156
  ```
155
157
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
156
158
  response.settings = saml_settings
157
159
  ```
158
- but if the SAMLResponse contains an encrypted assertion, you need to provide the settings in the
159
- initialize method in order to be able to obtain the decrypted assertion, using the service provider private key in order to decrypt.
160
- If you don't know what expect, use always the first proposed way (always set the settings on the initialize method).
160
+
161
+ If the assertion of the SAMLResponse is not encrypted, you can initialize the Response without the `:settings` parameter and set it later.
162
+ If the SAMLResponse contains an encrypted assertion, you need to provide the settings in the
163
+ initialize method in order to obtain the decrypted assertion, using the service provider private key in order to decrypt.
164
+ If you don't know what expect, always use the former (set the settings on initialize).
161
165
 
162
166
  ```ruby
163
167
  def saml_settings
@@ -175,6 +179,11 @@ def saml_settings
175
179
 
176
180
  # Optional for most SAML IdPs
177
181
  settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
182
+ # or as an array
183
+ settings.authn_context = [
184
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
185
+ "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
186
+ ]
178
187
 
179
188
  # Optional bindings (defaults to Redirect for logout POST for acs)
180
189
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
@@ -184,14 +193,14 @@ def saml_settings
184
193
  end
185
194
  ```
186
195
 
187
- Some assertion validations can be skipped by passing parameters to OneLogin::RubySaml::Response.new(). For example, you can skip the Conditions validation or the SubjectConfirmation validations by initializing the response with different options:
196
+ Some assertion validations can be skipped by passing parameters to `OneLogin::RubySaml::Response.new()`. For example, you can skip the `Conditions` validation or the `SubjectConfirmation` validations by initializing the response with different options:
188
197
 
189
198
  ```ruby
190
199
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions
191
200
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation
192
201
  ```
193
202
 
194
- What's left at this point, is to wrap it all up in a controller and point the initialization and consumption URLs in OneLogin at that. A full controller example could look like this:
203
+ All that's left is to wrap everything in a controller and reference it in the initialization and consumption URLs in OneLogin. A full controller example could look like this:
195
204
 
196
205
  ```ruby
197
206
  # This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.
@@ -270,8 +279,10 @@ The following attributes are set:
270
279
  * idp_slo_target_url
271
280
  * idp_cert_fingerprint
272
281
 
273
- If you are using saml:AttributeStatement to transfer metadata, like the user name, you can access all the attributes through response.attributes. It contains all the saml:AttributeStatement with its 'Name' as a indifferent key the one/more saml:AttributeValue as value. The value returned depends on the value of the
274
- `single_value_compatibility` (when activate, only one value returned, the first one)
282
+ ## Retrieving Attributes
283
+
284
+ If you are using `saml:AttributeStatement` to transfer data like the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the
285
+ `single_value_compatibility` (when activated, only the first value is returned)
275
286
 
276
287
  ```ruby
277
288
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
@@ -280,7 +291,7 @@ response.settings = saml_settings
280
291
  response.attributes[:username]
281
292
  ```
282
293
 
283
- Imagine this saml:AttributeStatement
294
+ Imagine this `saml:AttributeStatement`
284
295
 
285
296
  ```xml
286
297
  <saml:AttributeStatement>
@@ -380,15 +391,15 @@ pp(response.attributes.multi(:not_exists))
380
391
  # => nil
381
392
  ```
382
393
 
383
- The saml:AuthnContextClassRef of the AuthNRequest can be provided by `settings.authn_context` , possible values are described at [SAMLAuthnCxt]. The comparison method can be set using the parameter `settings.authn_context_comparison` (the possible values are: 'exact', 'better', 'maximum' and 'minimum'), 'exact' is the default value.
384
- If we want to add a saml:AuthnContextDeclRef, define a `settings.authn_context_decl_ref`.
394
+ The `saml:AuthnContextClassRef` of the AuthNRequest can be provided by `settings.authn_context`; possible values are described at [SAMLAuthnCxt]. The comparison method can be set using `settings.authn_context_comparison` parameter. Possible values include: 'exact', 'better', 'maximum' and 'minimum' (default value is 'exact').
395
+ To add a `saml:AuthnContextDeclRef`, define `settings.authn_context_decl_ref`.
385
396
 
386
397
 
387
398
  ## Signing
388
399
 
389
- The Ruby Toolkit supports 2 different kinds of signature: Embeded and as GET parameter
400
+ The Ruby Toolkit supports 2 different kinds of signature: Embeded and `GET` parameters
390
401
 
391
- In order to be able to sign we need first to define the private key and the public cert of the service provider
402
+ In order to be able to sign, define the private key and the public cert of the service provider:
392
403
 
393
404
  ```ruby
394
405
  settings.certificate = "CERTIFICATE TEXT WITH HEAD AND FOOT"
@@ -414,23 +425,23 @@ The settings related to sign are stored in the `security` attribute of the setti
414
425
  settings.security[:embed_sign] = false
415
426
  ```
416
427
 
417
- Notice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding,
418
- remember to provide it to the Signature builder if you are sending a GET RelayState parameter or
419
- Signature validation process will fail at the Identity Provider.
428
+ Notice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding.
429
+ Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
430
+ signature validation process will fail at the Identity Provider.
420
431
 
421
432
  The Service Provider will sign the request/responses with its private key.
422
433
  The Identity Provider will validate the sign of the received request/responses with the public x500 cert of the
423
434
  Service Provider.
424
435
 
425
- Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and the decrypt process.
436
+ Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and decrypt processes.
426
437
 
427
- Enable/disable the soft mode by the settings.soft parameter. When is set false, the saml validations errors will raise an exception.
438
+ Enable/disable the soft mode with the `settings.soft` parameter. When set to `false`, saml validations errors will raise an exception.
428
439
 
429
440
  ## Decrypting
430
441
 
431
442
  The Ruby Toolkit supports EncryptedAssertion.
432
443
 
433
- In order to be able to decrypt a SAML Response that contains a EncryptedAssertion we need first to define the private key and the public cert of the service provider, and share this with the Identity Provider.
444
+ In order to be able to decrypt a SAML Response that contains a EncryptedAssertion you need define the private key and the public cert of the service provider, then share this with the Identity Provider.
434
445
 
435
446
  ```ruby
436
447
  settings.certificate = "CERTIFICATE TEXT WITH HEAD AND FOOT"
@@ -440,13 +451,13 @@ In order to be able to decrypt a SAML Response that contains a EncryptedAssertio
440
451
  The Identity Provider will encrypt the Assertion with the public cert of the Service Provider.
441
452
  The Service Provider will decrypt the EncryptedAssertion with its private key.
442
453
 
443
- Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and the decrypt process.
454
+ Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and decrypt processes.
444
455
 
445
456
  ## Single Log Out
446
457
 
447
458
  The Ruby Toolkit supports SP-initiated Single Logout and IdP-Initiated Single Logout.
448
459
 
449
- Here is an example that we could add to our previous controller to generate and send a SAML Logout Request to the IdP
460
+ Here is an example that we could add to our previous controller to generate and send a SAML Logout Request to the IdP:
450
461
 
451
462
  ```ruby
452
463
  # Create a SP initiated SLO
@@ -475,7 +486,7 @@ def sp_logout_request
475
486
  end
476
487
  ```
477
488
 
478
- and this method process the SAML Logout Response sent by the IdP as reply of the SAML Logout Request
489
+ This method processes the SAML Logout Response sent by the IdP as the reply of the SAML Logout Request:
479
490
 
480
491
  ```ruby
481
492
  # After sending an SP initiated LogoutRequest to the IdP, we need to accept
@@ -510,7 +521,7 @@ def delete_session
510
521
  end
511
522
  ```
512
523
 
513
- Here is an example that we could add to our previous controller to process a SAML Logout Request from the IdP and reply a SAML Logout Response to the IdP
524
+ Here is an example that we could add to our previous controller to process a SAML Logout Request from the IdP and reply with a SAML Logout Response to the IdP:
514
525
 
515
526
  ```ruby
516
527
  # Method to handle IdP initiated logouts
@@ -576,11 +587,11 @@ end
576
587
 
577
588
  ## Clock Drift
578
589
 
579
- Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition" then this may be due to clock differences between your system and that of the Identity Provider.
590
+ Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition", this may be due to clock differences between your system and that of the Identity Provider.
580
591
 
581
592
  First, ensure that both systems synchronize their clocks, using for example the industry standard [Network Time Protocol (NTP)](http://en.wikipedia.org/wiki/Network_Time_Protocol).
582
593
 
583
- Even then you may experience intermittent issues though, because the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift you can initialize the response passing in an option named `:allowed_clock_drift`. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the `NotBefore` assertion. For example:
594
+ Even then you may experience intermittent issues, as the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift, you can initialize the response by passing in an option named `:allowed_clock_drift`. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the `NotBefore` assertion. For example:
584
595
 
585
596
  ```ruby
586
597
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1.second)
@@ -1,5 +1,29 @@
1
1
  # RubySaml Changelog
2
2
 
3
+ ### 1.4.0 (October 13, 2016)
4
+ * Several security improvements:
5
+ * Conditions element required and unique.
6
+ * AuthnStatement element required and unique.
7
+ * SPNameQualifier must math the SP EntityID
8
+ * Reject saml:Attribute element with same “Name” attribute
9
+ * Reject empty nameID
10
+ * Require Issuer element. (Must match IdP EntityID).
11
+ * Destination value can't be blank (if present must match ACS URL).
12
+ * Check that the EncryptedAssertion element only contains 1 Assertion element.
13
+
14
+ * [#335](https://github.com/onelogin/ruby-saml/pull/335) Explicitly parse as XML and fix setting of Nokogiri options.
15
+ * [#345](https://github.com/onelogin/ruby-saml/pull/345)Support multiple settings.auth_context
16
+ * More tests to prevent XML Signature Wrapping
17
+ * [#342](https://github.com/onelogin/ruby-saml/pull/342) Correct the usage of Mutex
18
+ * [352](https://github.com/onelogin/ruby-saml/pull/352) Support multiple AttributeStatement tags
19
+
20
+
21
+ ### 1.3.1 (July 10, 2016)
22
+ * Fix response_test.rb of gem 1.3.0
23
+ * Add reference to Security Guidelines
24
+ * Update License
25
+ * [#334](https://github.com/onelogin/ruby-saml/pull/334) Keep API backward-compatibility on IdpMetadataParser fingerprint method.
26
+
3
27
  ### 1.3.0 (June 24, 2016)
4
28
  * [Security Fix](https://github.com/onelogin/ruby-saml/commit/a571f52171e6bfd87db59822d1d9e8c38fb3b995) Add extra validations to prevent Signature wrapping attacks
5
29
  * Fix XMLSecurity SHA256 and SHA512 uris
@@ -136,8 +136,11 @@ module OneLogin
136
136
  }
137
137
 
138
138
  if settings.authn_context != nil
139
- class_ref = requested_context.add_element "saml:AuthnContextClassRef"
140
- class_ref.text = settings.authn_context
139
+ authn_contexts = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context]
140
+ authn_contexts.each do |authn_context|
141
+ class_ref = requested_context.add_element "saml:AuthnContextClassRef"
142
+ class_ref.text = authn_context
143
+ end
141
144
  end
142
145
  # add saml:AuthnContextDeclRef element
143
146
  if settings.authn_context_decl_ref != nil
@@ -88,6 +88,23 @@ module OneLogin
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,35 +132,34 @@ module OneLogin
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
- stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
124
- return attributes if stmt_element.nil?
125
-
126
- stmt_element.elements.each do |attr_element|
127
- name = attr_element.attributes["Name"]
128
- values = attr_element.elements.collect{|e|
129
- if (e.elements.nil? || e.elements.size == 0)
130
- # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
131
- # otherwise the value is to be regarded as empty.
132
- ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
133
- # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
134
- # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
135
- # identify the subject in an SP rather than email or other less opaque attributes
136
- # NameQualifier, if present is prefixed with a "/" to the value
137
- else
138
- REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
139
- (n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + n.text.to_s
140
- }
141
- end
142
- }
143
-
144
- attributes.add(name, values.flatten)
140
+ stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
141
+ stmt_elements.each do |stmt_element|
142
+ stmt_element.elements.each do |attr_element|
143
+ name = attr_element.attributes["Name"]
144
+ values = attr_element.elements.collect{|e|
145
+ if (e.elements.nil? || e.elements.size == 0)
146
+ # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
147
+ # otherwise the value is to be regarded as empty.
148
+ ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
149
+ # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
150
+ # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
151
+ # identify the subject in an SP rather than email or other less opaque attributes
152
+ # NameQualifier, if present is prefixed with a "/" to the value
153
+ else
154
+ REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
155
+ (n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + n.text.to_s
156
+ }
157
+ end
158
+ }
159
+
160
+ attributes.add(name, values.flatten)
161
+ end
145
162
  end
146
-
147
163
  attributes
148
164
  end
149
165
  end
@@ -170,12 +186,15 @@ module OneLogin
170
186
  #
171
187
  def status_code
172
188
  @status_code ||= begin
173
- node = REXML::XPath.first(
189
+ nodes = REXML::XPath.match(
174
190
  document,
175
191
  "/p:Response/p:Status/p:StatusCode",
176
192
  { "p" => PROTOCOL }
177
193
  )
178
- 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
179
198
  end
180
199
  end
181
200
 
@@ -183,12 +202,15 @@ module OneLogin
183
202
  #
184
203
  def status_message
185
204
  @status_message ||= begin
186
- node = REXML::XPath.first(
205
+ nodes = REXML::XPath.match(
187
206
  document,
188
207
  "/p:Response/p:Status/p:StatusMessage",
189
208
  { "p" => PROTOCOL }
190
209
  )
191
- node.text if node
210
+ if nodes.size == 1
211
+ node = nodes[0]
212
+ node.text if node
213
+ end
192
214
  end
193
215
  end
194
216
 
@@ -214,26 +236,6 @@ module OneLogin
214
236
  @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
215
237
  end
216
238
 
217
- # Gets the Issuers (from Response and Assertion).
218
- # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
219
- # @return [Array] Array with the Issuers (REXML::Element)
220
- #
221
- def issuers
222
- @issuers ||= begin
223
- issuers = []
224
- nodes = REXML::XPath.match(
225
- document,
226
- "/p:Response/a:Issuer",
227
- { "p" => PROTOCOL, "a" => ASSERTION }
228
- )
229
- nodes += xpath_from_signed_assertion("/a:Issuer")
230
- nodes.each do |node|
231
- issuers << node.text if node.text
232
- end
233
- issuers.uniq
234
- end
235
- end
236
-
237
239
  # @return [String|nil] The InResponseTo attribute from the SAML Response.
238
240
  #
239
241
  def in_response_to
@@ -299,15 +301,19 @@ module OneLogin
299
301
  :validate_success_status,
300
302
  :validate_num_assertion,
301
303
  :validate_no_encrypted_attributes,
304
+ :validate_no_duplicated_attributes,
302
305
  :validate_signed_elements,
303
306
  :validate_structure,
304
307
  :validate_in_response_to,
308
+ :validate_one_conditions,
305
309
  :validate_conditions,
310
+ :validate_one_authnstatement,
306
311
  :validate_audience,
307
312
  :validate_destination,
308
313
  :validate_issuer,
309
314
  :validate_session_expiration,
310
315
  :validate_subject_confirmation,
316
+ :validate_name_id,
311
317
  :validate_signature
312
318
  ]
313
319
 
@@ -396,6 +402,7 @@ module OneLogin
396
402
  # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
397
403
  #
398
404
  def validate_num_assertion
405
+ error_msg = "SAML Response must contain 1 assertion"
399
406
  assertions = REXML::XPath.match(
400
407
  document,
401
408
  "//a:Assertion",
@@ -408,7 +415,18 @@ module OneLogin
408
415
  )
409
416
 
410
417
  unless assertions.size + encrypted_assertions.size == 1
411
- 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
412
430
  end
413
431
 
414
432
  true
@@ -428,6 +446,28 @@ module OneLogin
428
446
  true
429
447
  end
430
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
431
471
 
432
472
  # Validates the Signed elements
433
473
  # If fails, the error is added to the errors array
@@ -527,7 +567,14 @@ module OneLogin
527
567
  # @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
528
568
  #
529
569
  def validate_destination
530
- 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?
531
578
 
532
579
  unless destination == settings.assertion_consumer_service_url
533
580
  error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}"
@@ -537,6 +584,34 @@ module OneLogin
537
584
  true
538
585
  end
539
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
+
540
615
  # Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
541
616
  # If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
542
617
  # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
@@ -569,6 +644,31 @@ module OneLogin
569
644
  def validate_issuer
570
645
  return true if settings.idp_entity_id.nil?
571
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
+
572
672
  issuers.each do |issuer|
573
673
  unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
574
674
  error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
@@ -642,6 +742,27 @@ module OneLogin
642
742
  true
643
743
  end
644
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
+
645
766
  # Validates the Signature
646
767
  # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
647
768
  # @raise [ValidationError] if soft == false and validation fails