ruby-saml 0.8.10 → 0.8.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/Rakefile +0 -14
  4. data/lib/onelogin/ruby-saml/authrequest.rb +3 -2
  5. data/lib/onelogin/ruby-saml/logoutrequest.rb +3 -0
  6. data/lib/onelogin/ruby-saml/logoutresponse.rb +1 -24
  7. data/lib/onelogin/ruby-saml/response.rb +206 -20
  8. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  9. data/lib/onelogin/ruby-saml/settings.rb +26 -0
  10. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +3 -2
  11. data/lib/onelogin/ruby-saml/utils.rb +90 -0
  12. data/lib/onelogin/ruby-saml/version.rb +1 -1
  13. data/lib/xml_security.rb +222 -87
  14. data/test/certificates/ruby-saml-2.crt +15 -0
  15. data/test/logoutrequest_test.rb +124 -126
  16. data/test/logoutresponse_test.rb +22 -28
  17. data/test/response_test.rb +348 -129
  18. data/test/responses/adfs_response_xmlns.xml +45 -0
  19. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  20. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  21. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  22. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  23. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  24. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  25. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  26. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  27. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  28. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  29. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  30. data/test/responses/response_wrapped.xml.base64 +150 -0
  31. data/test/responses/valid_response.xml.base64 +1 -0
  32. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  33. data/test/settings_test.rb +5 -5
  34. data/test/test_helper.rb +110 -41
  35. data/test/utils_test.rb +10 -10
  36. data/test/xml_security_test.rb +359 -68
  37. metadata +38 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a564ad5fdd002f4648b22638bf51dd7ad029ea633484289c7ce2e8759a3c5d6
4
- data.tar.gz: 15d0f1c459006f4c65fc9cb916b29b55098e68d9e3c5123080737b41b4e5e449
3
+ metadata.gz: 3039afb4b2668c3859e51ae9eff0c1b423d7dda319a7d646b26702de315047af
4
+ data.tar.gz: 2ef188024bd8030c659b499db22b3b28f2ae24930954f8c45cc69c175b8fc4e3
5
5
  SHA512:
6
- metadata.gz: 943d65750b9895285e60b24d612c1cce77bef3a262387121ccf9b7f8536ebfea27874d3a68ff095dc1a36e2bc3c53f509de07d2e4e31c02a20437e18015af561
7
- data.tar.gz: ccf1223639a43235641ba2533e61473e4ff248624571cd05177ccd1dd0611f7d8df16f405fd74af2987112a988f5fcb0f2f6c54fe3830f89d3688964780ea333
6
+ metadata.gz: 0ab896476c0de2ebcd71b060dc305d091e3b6c3be7abd81ce702389cb8e8409cbd5730a4a73a71dfdfedbeb7a0081f448ed4aec60737a8ce6c1b6fb966ce9c4f
7
+ data.tar.gz: 203b1fd9b1fa4d23ab66cac8a034d3154a48e243bff21040ae2e873d191e90c0d8b8ccdfee368ca542c306fd58ddcdbeec700047893ae6044b82b406af43de18
data/Gemfile CHANGED
@@ -7,10 +7,13 @@ gemspec
7
7
 
8
8
  if RUBY_VERSION < '1.9'
9
9
  gem 'nokogiri', '~> 1.5.0'
10
+ gem 'minitest', '~> 5.5', '<= 5.11.3'
10
11
  elsif RUBY_VERSION < '2.1'
11
12
  gem 'nokogiri', '>= 1.5.0', '<= 1.6.8.1'
13
+ gem 'minitest', '~> 5.5'
12
14
  else
13
15
  gem 'nokogiri', '>= 1.5.0'
16
+ gem 'minitest', '~> 5.5'
14
17
  end
15
18
 
16
19
  group :test do
@@ -30,6 +33,5 @@ group :test do
30
33
  gem 'shoulda', '~> 2.11'
31
34
  gem 'systemu', '~> 2'
32
35
  gem 'test-unit', '~> 3.0.9'
33
- gem 'minitest', '~> 5.5'
34
36
  gem 'timecop', '<= 0.6.0'
35
37
  end
data/Rakefile CHANGED
@@ -25,17 +25,3 @@ end
25
25
  task :test
26
26
 
27
27
  task :default => :test
28
-
29
- # require 'rake/rdoctask'
30
- # Rake::RDocTask.new do |rdoc|
31
- # if File.exist?('VERSION')
32
- # version = File.read('VERSION')
33
- # else
34
- # version = ""
35
- # end
36
-
37
- # rdoc.rdoc_dir = 'rdoc'
38
- # rdoc.title = "ruby-saml #{version}"
39
- # rdoc.rdoc_files.include('README*')
40
- # rdoc.rdoc_files.include('lib/**/*.rb')
41
- #end
@@ -2,6 +2,7 @@ require "base64"
2
2
  require "zlib"
3
3
  require "cgi"
4
4
  require "onelogin/ruby-saml/utils"
5
+ require "onelogin/ruby-saml/setting_error"
5
6
 
6
7
  module OneLogin
7
8
  module RubySaml
@@ -25,7 +26,7 @@ module OneLogin
25
26
  params.each_pair do |key, value|
26
27
  request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
27
28
  end
28
- raise "Invalid settings, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil?
29
+ raise SettingError.new "Invalid settings, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
29
30
  @login_url = settings.idp_sso_target_url + request_params
30
31
  end
31
32
 
@@ -101,7 +102,7 @@ module OneLogin
101
102
  root.attributes['ID'] = uuid
102
103
  root.attributes['IssueInstant'] = time
103
104
  root.attributes['Version'] = "2.0"
104
- root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil?
105
+ root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
105
106
  root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
106
107
  root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
107
108
  root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil?
@@ -3,6 +3,7 @@ require "zlib"
3
3
  require "cgi"
4
4
  require 'rexml/document'
5
5
  require "onelogin/ruby-saml/utils"
6
+ require "onelogin/ruby-saml/setting_error"
6
7
 
7
8
  module OneLogin
8
9
  module RubySaml
@@ -23,6 +24,7 @@ module OneLogin
23
24
  params.each_pair do |key, value|
24
25
  request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
25
26
  end
27
+ raise SettingError.new "Invalid settings, idp_slo_target_url is not set!" if settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
26
28
  @logout_url = settings.idp_slo_target_url + request_params
27
29
  end
28
30
 
@@ -103,6 +105,7 @@ module OneLogin
103
105
  root.attributes['ID'] = uuid
104
106
  root.attributes['IssueInstant'] = time
105
107
  root.attributes['Version'] = "2.0"
108
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
106
109
 
107
110
  if settings.sp_entity_id
108
111
  issuer = root.add_element "saml:Issuer", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
@@ -1,7 +1,5 @@
1
1
  require "xml_security"
2
2
  require "time"
3
- require "base64"
4
- require "zlib"
5
3
 
6
4
  module OneLogin
7
5
  module RubySaml
@@ -30,7 +28,7 @@ module OneLogin
30
28
  self.settings = settings
31
29
 
32
30
  @options = options
33
- @response = decode_raw_response(response)
31
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
34
32
  @document = XMLSecurity::SignedDocument.new(response)
35
33
  end
36
34
 
@@ -75,27 +73,6 @@ module OneLogin
75
73
 
76
74
  private
77
75
 
78
- def decode(encoded)
79
- Base64.decode64(encoded)
80
- end
81
-
82
- def inflate(deflated)
83
- zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
84
- zlib.inflate(deflated)
85
- end
86
-
87
- def decode_raw_response(response)
88
- if response =~ /^</
89
- return response
90
- elsif (decoded = decode(response)) =~ /^</
91
- return decoded
92
- elsif (inflated = inflate(decoded)) =~ /^</
93
- return inflated
94
- end
95
-
96
- raise "Couldn't decode SAMLResponse"
97
- end
98
-
99
76
  def valid_saml?(soft = true)
100
77
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
101
78
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -1,6 +1,7 @@
1
1
  require "xml_security"
2
2
  require "time"
3
3
  require "nokogiri"
4
+ require "onelogin/ruby-saml/utils"
4
5
  require 'onelogin/ruby-saml/attributes'
5
6
 
6
7
  # Only supports SAML 2.0
@@ -22,7 +23,7 @@ module OneLogin
22
23
  def initialize(response, options = {})
23
24
  raise ArgumentError.new("Response cannot be nil") if response.nil?
24
25
  @options = options
25
- @response = (response =~ /^</) ? response : Base64.decode64(response)
26
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
26
27
  @document = XMLSecurity::SignedDocument.new(@response)
27
28
  end
28
29
 
@@ -42,6 +43,8 @@ module OneLogin
42
43
  end
43
44
  end
44
45
 
46
+ alias nameid name_id
47
+
45
48
  def sessionindex
46
49
  @sessionindex ||= begin
47
50
  node = xpath_first_from_signed_assertion('/a:AuthnStatement')
@@ -147,14 +150,165 @@ module OneLogin
147
150
  end
148
151
 
149
152
  def validate(soft = true)
150
- validate_structure(soft) &&
151
- validate_response_state(soft) &&
152
- validate_conditions(soft) &&
153
- validate_audience(soft) &&
154
- document.validate_document(get_fingerprint, soft) &&
153
+ validate_structure(soft) &&
154
+ validate_success_status(soft) &&
155
+ validate_num_assertion &&
156
+ validate_signed_elements(soft) &&
157
+ validate_response_state(soft) &&
158
+ validate_conditions(soft) &&
159
+ validate_audience(soft) &&
160
+ validate_signature(soft) &&
155
161
  success?
156
162
  end
157
163
 
164
+ # Validates that the SAML Response only contains a single Assertion (encrypted or not).
165
+ # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
166
+ #
167
+ def validate_num_assertion(soft = true)
168
+ assertions = REXML::XPath.match(
169
+ document,
170
+ "//a:Assertion",
171
+ { "a" => ASSERTION }
172
+ )
173
+ encrypted_assertions = REXML::XPath.match(
174
+ document,
175
+ "//a:EncryptedAssertion",
176
+ { "a" => ASSERTION }
177
+ )
178
+
179
+ unless assertions.size + encrypted_assertions.size == 1
180
+ return soft ? false : validation_error("SAML Response must contain 1 assertion")
181
+ end
182
+
183
+ true
184
+ end
185
+
186
+ # Validates the Signed elements
187
+ # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
188
+ # an are a Response or an Assertion Element, otherwise False if soft=True
189
+ #
190
+ def validate_signed_elements(soft)
191
+ signature_nodes = REXML::XPath.match(
192
+ document,
193
+ "//ds:Signature",
194
+ {"ds"=>DSIG}
195
+ )
196
+ signed_elements = []
197
+ verified_seis = []
198
+ verified_ids = []
199
+ signature_nodes.each do |signature_node|
200
+ signed_element = signature_node.parent.name
201
+ if signed_element != 'Response' && signed_element != 'Assertion'
202
+ return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
203
+ end
204
+
205
+ if signature_node.parent.attributes['ID'].nil?
206
+ return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
207
+ end
208
+
209
+ id = signature_node.parent.attributes.get_attribute("ID").value
210
+ if verified_ids.include?(id)
211
+ return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
212
+ end
213
+ verified_ids.push(id)
214
+
215
+ # Check that reference URI matches the parent ID and no duplicate References or IDs
216
+ ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
217
+ if ref
218
+ uri = ref.attributes.get_attribute("URI")
219
+ if uri && !uri.value.empty?
220
+ sei = uri.value[1..-1]
221
+
222
+ unless sei == id
223
+ return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
224
+ end
225
+
226
+ if verified_seis.include?(sei)
227
+ return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
228
+ end
229
+
230
+ verified_seis.push(sei)
231
+ end
232
+ end
233
+
234
+ signed_elements << signed_element
235
+ end
236
+
237
+ unless signature_nodes.length < 3 && !signed_elements.empty?
238
+ return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
239
+ end
240
+
241
+ true
242
+ end
243
+
244
+ # Validates the Status of the SAML Response
245
+ # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
246
+ # @raise [ValidationError] if soft == false and validation fails
247
+ #
248
+ def validate_success_status(soft = true)
249
+ return true if success?
250
+
251
+ return false unless soft
252
+
253
+ error_msg = 'The status code of the Response was not Success'
254
+ status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
255
+ return validation_error(status_error_msg)
256
+ end
257
+
258
+ # Checks if the Status has the "Success" code
259
+ # @return [Boolean] True if the StatusCode is Sucess
260
+ #
261
+ def success?
262
+ status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
263
+ end
264
+
265
+ # @return [String] StatusCode value from a SAML Response.
266
+ #
267
+ def status_code
268
+ @status_code ||= begin
269
+ nodes = REXML::XPath.match(
270
+ document,
271
+ "/p:Response/p:Status/p:StatusCode",
272
+ { "p" => PROTOCOL }
273
+ )
274
+ if nodes.size == 1
275
+ node = nodes[0]
276
+ code = node.attributes["Value"] if node && node.attributes
277
+
278
+ unless code == "urn:oasis:names:tc:SAML:2.0:status:Success"
279
+ nodes = REXML::XPath.match(
280
+ document,
281
+ "/p:Response/p:Status/p:StatusCode/p:StatusCode",
282
+ { "p" => PROTOCOL }
283
+ )
284
+ statuses = nodes.collect do |inner_node|
285
+ inner_node.attributes["Value"]
286
+ end
287
+ extra_code = statuses.join(" | ")
288
+ if extra_code
289
+ code = "#{code} | #{extra_code}"
290
+ end
291
+ end
292
+ code
293
+ end
294
+ end
295
+ end
296
+
297
+ # @return [String] the StatusMessage value from a SAML Response.
298
+ #
299
+ def status_message
300
+ @status_message ||= begin
301
+ nodes = REXML::XPath.match(
302
+ document,
303
+ "/p:Response/p:Status/p:StatusMessage",
304
+ { "p" => PROTOCOL }
305
+ )
306
+ if nodes.size == 1
307
+ Utils.element_text(nodes.first)
308
+ end
309
+ end
310
+ end
311
+
158
312
  def validate_structure(soft = true)
159
313
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
160
314
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -219,15 +373,6 @@ module OneLogin
219
373
  ))
220
374
  end
221
375
 
222
- def get_fingerprint
223
- if settings.idp_cert
224
- cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
225
- Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
226
- else
227
- settings.idp_cert_fingerprint
228
- end
229
- end
230
-
231
376
  def validate_conditions(soft = true)
232
377
  return true if conditions.nil?
233
378
  return true if options[:skip_conditions]
@@ -245,17 +390,57 @@ module OneLogin
245
390
  true
246
391
  end
247
392
 
393
+ def validate_signature(soft = true)
394
+ error_msg = "Invalid Signature on SAML Response"
395
+
396
+ sig_elements = REXML::XPath.match(
397
+ document,
398
+ "/p:Response[@ID=$id]/ds:Signature]",
399
+ { "p" => PROTOCOL, "ds" => DSIG },
400
+ { 'id' => document.signed_element_id }
401
+ )
402
+
403
+ # Check signature nodes
404
+ if sig_elements.nil? || sig_elements.size == 0
405
+ sig_elements = REXML::XPath.match(
406
+ document,
407
+ "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
408
+ {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
409
+ { 'id' => document.signed_element_id }
410
+ )
411
+ end
412
+
413
+ if sig_elements.size != 1
414
+ if sig_elements.size == 0
415
+ error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
416
+ else
417
+ error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
418
+ end
419
+ return soft ? false : validation_error(error_msg)
420
+ end
421
+
422
+ opts = {}
423
+ opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
424
+ opts[:cert] = settings.get_idp_cert
425
+ fingerprint = settings.get_fingerprint
426
+
427
+ unless fingerprint
428
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
429
+ end
430
+
431
+ unless document.validate_document(fingerprint, soft, opts)
432
+ return soft ? false : validation_error(error_msg)
433
+ end
434
+
435
+ true
436
+ end
437
+
248
438
  def parse_time(node, attribute)
249
439
  if node && node.attributes[attribute]
250
440
  Time.parse(node.attributes[attribute])
251
441
  end
252
442
  end
253
443
 
254
- # Validates the Audience, (If the Audience match the Service Provider EntityID)
255
- # If fails, the error is added to the errors array
256
- # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
257
- # @raise [ValidationError] if soft == false and validation fails
258
- #
259
444
  def validate_audience(soft = true)
260
445
  return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
261
446
 
@@ -267,6 +452,7 @@ module OneLogin
267
452
 
268
453
  true
269
454
  end
455
+
270
456
  end
271
457
  end
272
458
  end
@@ -0,0 +1,6 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class SettingError < StandardError
4
+ end
5
+ end
6
+ end
@@ -117,6 +117,32 @@ module OneLogin
117
117
  @single_logout_service_binding = url
118
118
  end
119
119
 
120
+ # Calculates the fingerprint of the IdP x509 certificate.
121
+ # @return [String] The fingerprint
122
+ #
123
+ def get_fingerprint
124
+ idp_cert_fingerprint || begin
125
+ idp_cert = get_idp_cert
126
+ if idp_cert
127
+ Digest::SHA1.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
128
+ end
129
+ end
130
+ end
131
+
132
+ # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
133
+ #
134
+ def get_idp_cert
135
+ return nil if idp_cert.nil?
136
+
137
+ if idp_cert.respond_to?(:to_pem)
138
+ idp_cert
139
+ else
140
+ return nil if idp_cert.empty?
141
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
142
+ OpenSSL::X509::Certificate.new(formatted_cert)
143
+ end
144
+ end
145
+
120
146
  # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
121
147
  #
122
148
  def get_sp_cert