ruby-saml 0.8.10 → 0.8.15

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.

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