ruby-saml 0.8.11 → 0.8.16

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 (55) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +3 -1
  3. data/Rakefile +0 -14
  4. data/lib/onelogin/ruby-saml/logoutresponse.rb +9 -51
  5. data/lib/onelogin/ruby-saml/response.rb +121 -30
  6. data/lib/onelogin/ruby-saml/settings.rb +27 -10
  7. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +101 -0
  8. data/lib/onelogin/ruby-saml/utils.rb +92 -0
  9. data/lib/onelogin/ruby-saml/version.rb +1 -1
  10. data/lib/ruby-saml.rb +1 -0
  11. data/lib/xml_security.rb +222 -87
  12. data/test/certificates/certificate.der +0 -0
  13. data/test/certificates/formatted_certificate +14 -0
  14. data/test/certificates/formatted_chained_certificate +42 -0
  15. data/test/certificates/formatted_private_key +12 -0
  16. data/test/certificates/formatted_rsa_private_key +12 -0
  17. data/test/certificates/invalid_certificate1 +1 -0
  18. data/test/certificates/invalid_certificate2 +1 -0
  19. data/test/certificates/invalid_certificate3 +12 -0
  20. data/test/certificates/invalid_chained_certificate1 +1 -0
  21. data/test/certificates/invalid_private_key1 +1 -0
  22. data/test/certificates/invalid_private_key2 +1 -0
  23. data/test/certificates/invalid_private_key3 +10 -0
  24. data/test/certificates/invalid_rsa_private_key1 +1 -0
  25. data/test/certificates/invalid_rsa_private_key2 +1 -0
  26. data/test/certificates/invalid_rsa_private_key3 +10 -0
  27. data/test/certificates/ruby-saml-2.crt +15 -0
  28. data/test/logoutrequest_test.rb +124 -126
  29. data/test/logoutresponse_test.rb +22 -42
  30. data/test/requests/logoutrequest_fixtures.rb +47 -0
  31. data/test/response_test.rb +373 -129
  32. data/test/responses/adfs_response_xmlns.xml +45 -0
  33. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  34. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  35. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  36. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  37. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  38. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  39. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  40. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  41. data/test/responses/logoutresponse_fixtures.rb +4 -4
  42. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  43. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  44. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  45. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  46. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  47. data/test/responses/response_wrapped.xml.base64 +150 -0
  48. data/test/responses/valid_response.xml.base64 +1 -0
  49. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  50. data/test/settings_test.rb +111 -5
  51. data/test/slo_logoutrequest_test.rb +66 -0
  52. data/test/test_helper.rb +110 -41
  53. data/test/utils_test.rb +201 -11
  54. data/test/xml_security_test.rb +359 -68
  55. metadata +77 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 10ec0e5a4a9fc6a7f65613599597cef7ae8b8293
4
- data.tar.gz: 0b63798d5d6b78e3073ccb899e355e6aa0134648
2
+ SHA256:
3
+ metadata.gz: 68cb87ca6e3a580cea96b7784b71f60afe0f0982fc9b3c7b2de4fdda0ea1af31
4
+ data.tar.gz: 216f43c0d0a179f3a9c506f198c31e2a01a08a8661bfaafbe3cb50811b1acf88
5
5
  SHA512:
6
- metadata.gz: 1883986a5fd1b3925e6745bf7d259a907c656d36958efe50dfb760192c8273a3723b76344ee542a3a07b84da677808fad850b8bd272d949c5ce7fcd8c171a881
7
- data.tar.gz: 8538b7c75961e99186b320ff1425fb82fa0c759e3e7267eaad76659adf9f0ba98249207122092dbc383f464c1c600b03a37f010d5a3e8bfa7fbb6fba0aaa8915
6
+ metadata.gz: 35ba610649dbff55acae0612782ab7e81947907212b9c454494f9baa5c3926a126430eed919356049261a9cb40767d7079874f56f1b4cb1bd7efb637f4f6ba4f
7
+ data.tar.gz: a3e9ce681547c0e648f477198a134749c9febb1f86e042d2bb3266e01a740767672a667d0c85a162d0f11489e87ee4c1a53cbd15d45e6aeefeb31fc30a2fe99f
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
@@ -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,8 +28,8 @@ module OneLogin
30
28
  self.settings = settings
31
29
 
32
30
  @options = options
33
- @response = decode_raw_response(response)
34
- @document = XMLSecurity::SignedDocument.new(response)
31
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
32
+ @document = XMLSecurity::SignedDocument.new(@response)
35
33
  end
36
34
 
37
35
  def validate!
@@ -39,7 +37,7 @@ module OneLogin
39
37
  end
40
38
 
41
39
  def validate(soft = true)
42
- return false unless valid_saml?(soft) && valid_state?(soft)
40
+ return false unless validate_structure(soft)
43
41
 
44
42
  valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
45
43
  end
@@ -53,7 +51,7 @@ module OneLogin
53
51
 
54
52
  def in_response_to
55
53
  @in_response_to ||= begin
56
- node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL, "a" => ASSERTION })
54
+ node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL })
57
55
  node.nil? ? nil : node.attributes['InResponseTo']
58
56
  end
59
57
  end
@@ -61,7 +59,6 @@ module OneLogin
61
59
  def issuer
62
60
  @issuer ||= begin
63
61
  node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
64
- node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
65
62
  Utils.element_text(node)
66
63
  end
67
64
  end
@@ -75,28 +72,7 @@ module OneLogin
75
72
 
76
73
  private
77
74
 
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
- def valid_saml?(soft = true)
75
+ def validate_structure(soft = true)
100
76
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
101
77
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
102
78
  @xml = Nokogiri::XML(self.document.to_s)
@@ -108,26 +84,6 @@ module OneLogin
108
84
  end
109
85
  end
110
86
 
111
- def valid_state?(soft = true)
112
- if response.empty?
113
- return soft ? false : validation_error("Blank response")
114
- end
115
-
116
- if settings.nil?
117
- return soft ? false : validation_error("No settings on response")
118
- end
119
-
120
- if settings.sp_entity_id.nil?
121
- return soft ? false : validation_error("No sp_entity_id in settings")
122
- end
123
-
124
- if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
125
- return soft ? false : validation_error("No fingerprint or certificate on settings")
126
- end
127
-
128
- true
129
- end
130
-
131
87
  def valid_in_response_to?(soft = true)
132
88
  return true unless self.options.has_key? :matches_request_id
133
89
 
@@ -139,8 +95,10 @@ module OneLogin
139
95
  end
140
96
 
141
97
  def valid_issuer?(soft = true)
142
- unless URI.parse(issuer) == URI.parse(self.settings.sp_entity_id)
143
- return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.sp_entity_id}>, but was: <#{issuer}>")
98
+ return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?
99
+
100
+ unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
101
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.idp_entity_id}>, but was: <#{issuer}>")
144
102
  end
145
103
  true
146
104
  end
@@ -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
 
@@ -133,6 +134,34 @@ module OneLogin
133
134
  end
134
135
  end
135
136
 
137
+ # Gets the Issuers (from Response and Assertion).
138
+ # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
139
+ # @return [Array] Array with the Issuers (REXML::Element)
140
+ #
141
+ def issuers
142
+ @issuers ||= begin
143
+ issuer_response_nodes = REXML::XPath.match(
144
+ document,
145
+ "/p:Response/a:Issuer",
146
+ { "p" => PROTOCOL, "a" => ASSERTION }
147
+ )
148
+
149
+ unless issuer_response_nodes.size == 1
150
+ error_msg = "Issuer of the Response not found or multiple."
151
+ raise ValidationError.new(error_msg)
152
+ end
153
+
154
+ issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
155
+ unless issuer_assertion_nodes.size == 1
156
+ error_msg = "Issuer of the Assertion not found or multiple."
157
+ raise ValidationError.new(error_msg)
158
+ end
159
+
160
+ nodes = issuer_response_nodes + issuer_assertion_nodes
161
+ nodes.map { |node| Utils.element_text(node) }.compact.uniq
162
+ end
163
+ end
164
+
136
165
  # @return [Array] The Audience elements from the Contitions of the SAML Response.
137
166
  #
138
167
  def audiences
@@ -149,14 +178,15 @@ module OneLogin
149
178
  end
150
179
 
151
180
  def validate(soft = true)
152
- validate_success_status &&
153
- validate_num_assertion &&
154
- validate_signed_elements &&
155
- validate_structure(soft) &&
156
- validate_response_state(soft) &&
157
- validate_conditions(soft) &&
158
- validate_audience(soft) &&
159
- document.validate_document(get_fingerprint, soft) &&
181
+ validate_structure(soft) &&
182
+ validate_success_status(soft) &&
183
+ validate_num_assertion &&
184
+ validate_signed_elements(soft) &&
185
+ validate_response_state(soft) &&
186
+ validate_conditions(soft) &&
187
+ validate_audience(soft) &&
188
+ validate_issuer(soft) &&
189
+ validate_signature(soft) &&
160
190
  success?
161
191
  end
162
192
 
@@ -175,10 +205,6 @@ module OneLogin
175
205
  { "a" => ASSERTION }
176
206
  )
177
207
 
178
- unless assertions.size != 0
179
- return soft ? false : validation_error("Encrypted assertion is not supported")
180
- end
181
-
182
208
  unless assertions.size + encrypted_assertions.size == 1
183
209
  return soft ? false : validation_error("SAML Response must contain 1 assertion")
184
210
  end
@@ -190,7 +216,7 @@ module OneLogin
190
216
  # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
191
217
  # an are a Response or an Assertion Element, otherwise False if soft=True
192
218
  #
193
- def validate_signed_elements
219
+ def validate_signed_elements(soft)
194
220
  signature_nodes = REXML::XPath.match(
195
221
  document,
196
222
  "//ds:Signature",
@@ -228,7 +254,6 @@ module OneLogin
228
254
 
229
255
  if verified_seis.include?(sei)
230
256
  return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
231
- return append_error("Duplicated Reference URI. SAML Response rejected")
232
257
  end
233
258
 
234
259
  verified_seis.push(sei)
@@ -249,7 +274,7 @@ module OneLogin
249
274
  # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
250
275
  # @raise [ValidationError] if soft == false and validation fails
251
276
  #
252
- def validate_success_status
277
+ def validate_success_status(soft = true)
253
278
  return true if success?
254
279
 
255
280
  return false unless soft
@@ -298,6 +323,21 @@ module OneLogin
298
323
  end
299
324
  end
300
325
 
326
+ # @return [String] the StatusMessage value from a SAML Response.
327
+ #
328
+ def status_message
329
+ @status_message ||= begin
330
+ nodes = REXML::XPath.match(
331
+ document,
332
+ "/p:Response/p:Status/p:StatusMessage",
333
+ { "p" => PROTOCOL }
334
+ )
335
+ if nodes.size == 1
336
+ Utils.element_text(nodes.first)
337
+ end
338
+ end
339
+ end
340
+
301
341
  def validate_structure(soft = true)
302
342
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
303
343
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -362,15 +402,6 @@ module OneLogin
362
402
  ))
363
403
  end
364
404
 
365
- def get_fingerprint
366
- if settings.idp_cert
367
- cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
368
- Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
369
- else
370
- settings.idp_cert_fingerprint
371
- end
372
- end
373
-
374
405
  def validate_conditions(soft = true)
375
406
  return true if conditions.nil?
376
407
  return true if options[:skip_conditions]
@@ -388,17 +419,76 @@ module OneLogin
388
419
  true
389
420
  end
390
421
 
422
+ def validate_issuer(soft = true)
423
+ return true if settings.idp_entity_id.nil?
424
+
425
+ begin
426
+ obtained_issuers = issuers
427
+ rescue ValidationError => e
428
+ return soft ? false : validation_error("Error while extracting issuers")
429
+ end
430
+
431
+ obtained_issuers.each do |issuer|
432
+ unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
433
+ error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
434
+ return soft ? false : validation_error(error_msg)
435
+ end
436
+ end
437
+
438
+ true
439
+ end
440
+
441
+ def validate_signature(soft = true)
442
+ error_msg = "Invalid Signature on SAML Response"
443
+
444
+ sig_elements = REXML::XPath.match(
445
+ document,
446
+ "/p:Response[@ID=$id]/ds:Signature]",
447
+ { "p" => PROTOCOL, "ds" => DSIG },
448
+ { 'id' => document.signed_element_id }
449
+ )
450
+
451
+ # Check signature nodes
452
+ if sig_elements.nil? || sig_elements.size == 0
453
+ sig_elements = REXML::XPath.match(
454
+ document,
455
+ "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
456
+ {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
457
+ { 'id' => document.signed_element_id }
458
+ )
459
+ end
460
+
461
+ if sig_elements.size != 1
462
+ if sig_elements.size == 0
463
+ error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
464
+ else
465
+ error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
466
+ end
467
+ return soft ? false : validation_error(error_msg)
468
+ end
469
+
470
+ opts = {}
471
+ opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
472
+ opts[:cert] = settings.get_idp_cert
473
+ fingerprint = settings.get_fingerprint
474
+
475
+ unless fingerprint
476
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
477
+ end
478
+
479
+ unless document.validate_document(fingerprint, soft, opts)
480
+ return soft ? false : validation_error(error_msg)
481
+ end
482
+
483
+ true
484
+ end
485
+
391
486
  def parse_time(node, attribute)
392
487
  if node && node.attributes[attribute]
393
488
  Time.parse(node.attributes[attribute])
394
489
  end
395
490
  end
396
491
 
397
- # Validates the Audience, (If the Audience match the Service Provider EntityID)
398
- # If fails, the error is added to the errors array
399
- # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
400
- # @raise [ValidationError] if soft == false and validation fails
401
- #
402
492
  def validate_audience(soft = true)
403
493
  return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
404
494
 
@@ -410,6 +500,7 @@ module OneLogin
410
500
 
411
501
  true
412
502
  end
503
+
413
504
  end
414
505
  end
415
506
  end
@@ -27,6 +27,7 @@ module OneLogin
27
27
  attr_accessor :idp_cert_fingerprint
28
28
  attr_accessor :idp_cert
29
29
  attr_accessor :idp_slo_target_url
30
+ attr_accessor :idp_entity_id
30
31
  #sp data
31
32
  attr_accessor :sp_entity_id
32
33
  attr_accessor :assertion_consumer_service_url
@@ -36,7 +37,6 @@ module OneLogin
36
37
  attr_accessor :name_identifier_value
37
38
  attr_accessor :name_identifier_value_requested
38
39
  attr_accessor :sessionindex
39
- attr_accessor :assertion_consumer_logout_service_url
40
40
  attr_accessor :compress_request
41
41
  attr_accessor :compress_response
42
42
  attr_accessor :double_quote_xml_attribute_values
@@ -117,21 +117,38 @@ module OneLogin
117
117
  @single_logout_service_binding = url
118
118
  end
119
119
 
120
- # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
120
+ # Calculates the fingerprint of the IdP x509 certificate.
121
+ # @return [String] The fingerprint
121
122
  #
122
- def get_sp_cert
123
- return nil if certificate.nil? || certificate.empty?
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
124
131
 
125
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
126
- OpenSSL::X509::Certificate.new(formatted_cert)
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
127
144
  end
128
145
 
129
- # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
146
+ # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
130
147
  #
131
- def get_sp_cert_new
132
- return nil if certificate_new.nil? || certificate_new.empty?
148
+ def get_sp_cert
149
+ return nil if certificate.nil? || certificate.empty?
133
150
 
134
- formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
151
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
135
152
  OpenSSL::X509::Certificate.new(formatted_cert)
136
153
  end
137
154