ruby-saml 0.8.18 → 0.9

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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -6
  4. data/Gemfile +2 -12
  5. data/README.md +363 -35
  6. data/Rakefile +14 -0
  7. data/changelog.md +22 -9
  8. data/lib/onelogin/ruby-saml/attribute_service.rb +34 -0
  9. data/lib/onelogin/ruby-saml/attributes.rb +26 -64
  10. data/lib/onelogin/ruby-saml/authrequest.rb +47 -93
  11. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +87 -0
  12. data/lib/onelogin/ruby-saml/logoutrequest.rb +36 -100
  13. data/lib/onelogin/ruby-saml/logoutresponse.rb +25 -35
  14. data/lib/onelogin/ruby-saml/metadata.rb +46 -16
  15. data/lib/onelogin/ruby-saml/response.rb +63 -373
  16. data/lib/onelogin/ruby-saml/saml_message.rb +78 -0
  17. data/lib/onelogin/ruby-saml/settings.rb +54 -122
  18. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +25 -71
  19. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +37 -102
  20. data/lib/onelogin/ruby-saml/utils.rb +32 -199
  21. data/lib/onelogin/ruby-saml/version.rb +1 -1
  22. data/lib/ruby-saml.rb +5 -2
  23. data/lib/schemas/{saml20assertion_schema.xsd → saml-schema-assertion-2.0.xsd} +283 -283
  24. data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
  25. data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
  26. data/lib/schemas/saml-schema-metadata-2.0.xsd +339 -0
  27. data/lib/schemas/{saml20protocol_schema.xsd → saml-schema-protocol-2.0.xsd} +302 -302
  28. data/lib/schemas/sstc-metadata-attr.xsd +35 -0
  29. data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
  30. data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
  31. data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
  32. data/lib/schemas/{xenc_schema.xsd → xenc-schema.xsd} +1 -11
  33. data/lib/schemas/xml.xsd +287 -0
  34. data/lib/schemas/{xmldsig_schema.xsd → xmldsig-core-schema.xsd} +0 -9
  35. data/lib/xml_security.rb +83 -235
  36. data/ruby-saml.gemspec +1 -0
  37. data/test/idp_metadata_parser_test.rb +54 -0
  38. data/test/logoutrequest_test.rb +68 -155
  39. data/test/logoutresponse_test.rb +43 -32
  40. data/test/metadata_test.rb +87 -0
  41. data/test/request_test.rb +102 -99
  42. data/test/response_test.rb +181 -495
  43. data/test/responses/idp_descriptor.xml +3 -0
  44. data/test/responses/logoutresponse_fixtures.rb +7 -8
  45. data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
  46. data/test/responses/response_with_multiple_attribute_values.xml +1 -1
  47. data/test/responses/slo_request.xml +4 -0
  48. data/test/settings_test.rb +25 -112
  49. data/test/slo_logoutrequest_test.rb +40 -50
  50. data/test/slo_logoutresponse_test.rb +86 -185
  51. data/test/test_helper.rb +27 -102
  52. data/test/xml_security_test.rb +114 -337
  53. metadata +30 -81
  54. data/lib/onelogin/ruby-saml/setting_error.rb +0 -6
  55. data/test/certificates/certificate.der +0 -0
  56. data/test/certificates/formatted_certificate +0 -14
  57. data/test/certificates/formatted_chained_certificate +0 -42
  58. data/test/certificates/formatted_private_key +0 -12
  59. data/test/certificates/formatted_rsa_private_key +0 -12
  60. data/test/certificates/invalid_certificate1 +0 -1
  61. data/test/certificates/invalid_certificate2 +0 -1
  62. data/test/certificates/invalid_certificate3 +0 -12
  63. data/test/certificates/invalid_chained_certificate1 +0 -1
  64. data/test/certificates/invalid_private_key1 +0 -1
  65. data/test/certificates/invalid_private_key2 +0 -1
  66. data/test/certificates/invalid_private_key3 +0 -10
  67. data/test/certificates/invalid_rsa_private_key1 +0 -1
  68. data/test/certificates/invalid_rsa_private_key2 +0 -1
  69. data/test/certificates/invalid_rsa_private_key3 +0 -10
  70. data/test/certificates/ruby-saml-2.crt +0 -15
  71. data/test/requests/logoutrequest_fixtures.rb +0 -47
  72. data/test/responses/encrypted_new_attack.xml.base64 +0 -1
  73. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  74. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  75. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  76. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  77. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  78. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  79. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  80. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  81. data/test/responses/response_with_concealed_signed_assertion.xml +0 -51
  82. data/test/responses/response_with_doubled_signed_assertion.xml +0 -49
  83. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  84. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  85. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  86. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  87. data/test/responses/response_wrapped.xml.base64 +0 -150
  88. data/test/responses/valid_response.xml.base64 +0 -1
  89. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  90. data/test/utils_test.rb +0 -231
@@ -1,48 +1,30 @@
1
1
  require "xml_security"
2
2
  require "time"
3
3
  require "nokogiri"
4
- require "onelogin/ruby-saml/utils"
5
- require 'onelogin/ruby-saml/attributes'
6
4
 
7
5
  # Only supports SAML 2.0
8
6
  module OneLogin
9
7
  module RubySaml
10
8
 
11
- class Response
9
+ class Response < SamlMessage
12
10
  ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
13
11
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
14
12
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
15
13
 
16
14
  # TODO: This should probably be ctor initialized too... WDYT?
17
15
  attr_accessor :settings
16
+ attr_accessor :errors
18
17
 
19
18
  attr_reader :options
20
19
  attr_reader :response
21
20
  attr_reader :document
22
21
 
23
22
  def initialize(response, options = {})
23
+ @errors = []
24
24
  raise ArgumentError.new("Response cannot be nil") if response.nil?
25
25
  @options = options
26
- @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
27
- @document = XMLSecurity::SignedDocument.new(@response)
28
- end
29
-
30
- def response_id
31
- @response_id ||= begin
32
- node = REXML::XPath.first(
33
- document,
34
- "/p:Response",
35
- { "p" => PROTOCOL }
36
- )
37
- node.nil? ? nil : node.attributes['ID']
38
- end
39
- end
40
-
41
- def assertion_id
42
- @assertion_id ||= begin
43
- node = xpath_first_from_signed_assertion("")
44
- node.nil? ? nil : node.attributes['ID']
45
- end
26
+ @response = decode_raw_saml(response)
27
+ @document = XMLSecurity::SignedDocument.new(@response, @errors)
46
28
  end
47
29
 
48
30
  def is_valid?
@@ -53,46 +35,16 @@ module OneLogin
53
35
  validate(false)
54
36
  end
55
37
 
56
- def name_id_node
57
- @name_id ||= begin
58
- xpath_first_from_signed_assertion('/a:Subject/a:NameID')
59
- end
38
+ def errors
39
+ @errors
60
40
  end
61
41
 
62
42
  # The value of the user identifier as designated by the initialization request response
63
43
  def name_id
64
- @name_id ||= Utils.element_text(name_id_node)
65
- end
66
-
67
- alias nameid name_id
68
-
69
- # @return [String] the NameID Format provided by the SAML response from the IdP.
70
- #
71
- def name_id_format
72
- @name_id_format ||=
73
- if name_id_node && name_id_node.attribute("Format")
74
- name_id_node.attribute("Format").value
75
- end
76
- end
77
-
78
- alias_method :nameid_format, :name_id_format
79
-
80
- # @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
81
- #
82
- def name_id_spnamequalifier
83
- @name_id_spnamequalifier ||=
84
- if name_id_node && name_id_node.attribute("SPNameQualifier")
85
- name_id_node.attribute("SPNameQualifier").value
86
- end
87
- end
88
-
89
- # @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
90
- #
91
- def name_id_namequalifier
92
- @name_id_namequalifier ||=
93
- if name_id_node && name_id_node.attribute("NameQualifier")
94
- name_id_node.attribute("NameQualifier").value
95
- end
44
+ @name_id ||= begin
45
+ node = xpath_first_from_signed_assertion('/a:Subject/a:NameID')
46
+ node.nil? ? nil : node.text
47
+ end
96
48
  end
97
49
 
98
50
  def sessionindex
@@ -102,9 +54,9 @@ module OneLogin
102
54
  end
103
55
  end
104
56
 
105
- # Gets the Attributes from the AttributeStatement element.
106
- #
57
+ # Returns OneLogin::RubySaml::Attributes enumerable collection.
107
58
  # All attributes can be iterated over +attributes.each+ or returned as array by +attributes.all+
59
+ #
108
60
  # For backwards compatibility ruby-saml returns by default only the first value for a given attribute with
109
61
  # attributes['name']
110
62
  # To get all of the attributes, use:
@@ -113,36 +65,24 @@ module OneLogin
113
65
  # OneLogin::RubySaml::Attributes.single_value_compatibility = false
114
66
  # Now this will return an array:
115
67
  # attributes['name']
116
- #
117
- # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
118
- #
119
68
  def attributes
120
69
  @attr_statements ||= begin
121
70
  attributes = Attributes.new
122
71
 
123
- stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
124
- stmt_elements.each do |stmt_element|
125
- stmt_element.elements.each do |attr_element|
126
- name = attr_element.attributes["Name"]
127
- values = attr_element.elements.collect{|e|
128
- if (e.elements.nil? || e.elements.size == 0)
129
- # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
130
- # otherwise the value is to be regarded as empty.
131
- ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : Utils.element_text(e)
132
- # explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
133
- # this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
134
- # identify the subject in an SP rather than email or other less opaque attributes
135
- # NameQualifier, if present is prefixed with a "/" to the value
136
- else
137
- REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
138
- (n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + Utils.element_text(n)
139
- }
140
- end
141
- }
142
-
143
- attributes.add(name, values.flatten)
144
- end
72
+ stmt_element = xpath_first_from_signed_assertion('/a:AttributeStatement')
73
+ return attributes if stmt_element.nil?
74
+
75
+ stmt_element.elements.each do |attr_element|
76
+ name = attr_element.attributes["Name"]
77
+ values = attr_element.elements.collect{|e|
78
+ # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
79
+ # otherwise the value is to be regarded as empty.
80
+ ["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
81
+ }
82
+
83
+ attributes.add(name, values)
145
84
  end
85
+
146
86
  attributes
147
87
  end
148
88
  end
@@ -163,6 +103,13 @@ module OneLogin
163
103
  end
164
104
  end
165
105
 
106
+ def status_message
107
+ @status_message ||= begin
108
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusMessage", { "p" => PROTOCOL, "a" => ASSERTION })
109
+ node.text if node
110
+ end
111
+ end
112
+
166
113
  # Conditions (if any) for the assertion to run
167
114
  def conditions
168
115
  @conditions ||= xpath_first_from_signed_assertion('/a:Conditions')
@@ -180,223 +127,43 @@ module OneLogin
180
127
  @issuer ||= begin
181
128
  node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
182
129
  node ||= xpath_first_from_signed_assertion('/a:Issuer')
183
- Utils.element_text(node)
184
- end
185
- end
186
-
187
- # Gets the Issuers (from Response and Assertion).
188
- # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
189
- # @return [Array] Array with the Issuers (REXML::Element)
190
- #
191
- def issuers
192
- @issuers ||= begin
193
- issuer_response_nodes = REXML::XPath.match(
194
- document,
195
- "/p:Response/a:Issuer",
196
- { "p" => PROTOCOL, "a" => ASSERTION }
197
- )
198
-
199
- unless issuer_response_nodes.size == 1
200
- error_msg = "Issuer of the Response not found or multiple."
201
- raise ValidationError.new(error_msg)
202
- end
203
-
204
- issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
205
- unless issuer_assertion_nodes.size == 1
206
- error_msg = "Issuer of the Assertion not found or multiple."
207
- raise ValidationError.new(error_msg)
208
- end
209
-
210
- nodes = issuer_response_nodes + issuer_assertion_nodes
211
- nodes.map { |node| Utils.element_text(node) }.compact.uniq
212
- end
213
- end
214
-
215
- # @return [Array] The Audience elements from the Contitions of the SAML Response.
216
- #
217
- def audiences
218
- @audiences ||= begin
219
- nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
220
- nodes.map { |node| Utils.element_text(node) }.reject(&:empty?)
130
+ node.nil? ? nil : node.text
221
131
  end
222
132
  end
223
133
 
224
134
  private
225
135
 
226
- def validation_error(message)
227
- raise ValidationError.new(message)
228
- end
229
-
230
136
  def validate(soft = true)
231
- validate_structure(soft) &&
232
- validate_success_status(soft) &&
233
- validate_num_assertion &&
234
- validate_signed_elements(soft) &&
235
- validate_response_state(soft) &&
236
- validate_conditions(soft) &&
237
- validate_audience(soft) &&
238
- validate_issuer(soft) &&
239
- validate_signature(soft) &&
240
- success?
241
- end
242
-
243
- # Validates that the SAML Response only contains a single Assertion (encrypted or not).
244
- # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
245
- #
246
- def validate_num_assertion(soft = true)
247
- assertions = REXML::XPath.match(
248
- document,
249
- "//a:Assertion",
250
- { "a" => ASSERTION }
251
- )
252
- encrypted_assertions = REXML::XPath.match(
253
- document,
254
- "//a:EncryptedAssertion",
255
- { "a" => ASSERTION }
256
- )
257
-
258
- unless assertions.size + encrypted_assertions.size == 1
259
- return soft ? false : validation_error("SAML Response must contain 1 assertion")
260
- end
261
-
262
- true
263
- end
264
-
265
- # Validates the Signed elements
266
- # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
267
- # an are a Response or an Assertion Element, otherwise False if soft=True
268
- #
269
- def validate_signed_elements(soft)
270
- signature_nodes = REXML::XPath.match(
271
- document,
272
- "//ds:Signature",
273
- {"ds"=>DSIG}
274
- )
275
- signed_elements = []
276
- verified_seis = []
277
- verified_ids = []
278
- signature_nodes.each do |signature_node|
279
- signed_element = signature_node.parent.name
280
- if signed_element != 'Response' && signed_element != 'Assertion'
281
- return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
282
- end
283
-
284
- if signature_node.parent.attributes['ID'].nil?
285
- return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
286
- end
287
-
288
- id = signature_node.parent.attributes.get_attribute("ID").value
289
- if verified_ids.include?(id)
290
- return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
291
- end
292
- verified_ids.push(id)
293
-
294
- # Check that reference URI matches the parent ID and no duplicate References or IDs
295
- ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
296
- if ref
297
- uri = ref.attributes.get_attribute("URI")
298
- if uri && !uri.value.empty?
299
- sei = uri.value[1..-1]
300
-
301
- unless sei == id
302
- return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
303
- end
304
-
305
- if verified_seis.include?(sei)
306
- return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
307
- end
308
-
309
- verified_seis.push(sei)
310
- end
311
- end
312
-
313
- signed_elements << signed_element
314
- end
315
-
316
- unless signature_nodes.length < 3 && !signed_elements.empty?
317
- return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
318
- end
319
-
320
- true
137
+ valid_saml?(document, soft) &&
138
+ validate_response_state(soft) &&
139
+ validate_conditions(soft) &&
140
+ validate_issuer(soft) &&
141
+ document.validate_document(get_fingerprint, soft) &&
142
+ validate_success_status(soft)
321
143
  end
322
144
 
323
- # Validates the Status of the SAML Response
324
- # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
325
- # @raise [ValidationError] if soft == false and validation fails
326
- #
327
145
  def validate_success_status(soft = true)
328
- return true if success?
329
-
330
- return false unless soft
331
-
332
- error_msg = 'The status code of the Response was not Success'
333
- status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
334
- return validation_error(status_error_msg)
335
- end
336
-
337
- # Checks if the Status has the "Success" code
338
- # @return [Boolean] True if the StatusCode is Sucess
339
- #
340
- def success?
341
- status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
342
- end
343
-
344
- # @return [String] StatusCode value from a SAML Response.
345
- #
346
- def status_code
347
- @status_code ||= begin
348
- nodes = REXML::XPath.match(
349
- document,
350
- "/p:Response/p:Status/p:StatusCode",
351
- { "p" => PROTOCOL }
352
- )
353
- if nodes.size == 1
354
- node = nodes[0]
355
- code = node.attributes["Value"] if node && node.attributes
356
-
357
- unless code == "urn:oasis:names:tc:SAML:2.0:status:Success"
358
- nodes = REXML::XPath.match(
359
- document,
360
- "/p:Response/p:Status/p:StatusCode/p:StatusCode",
361
- { "p" => PROTOCOL }
362
- )
363
- statuses = nodes.collect do |inner_node|
364
- inner_node.attributes["Value"]
365
- end
366
- extra_code = statuses.join(" | ")
367
- if extra_code
368
- code = "#{code} | #{extra_code}"
369
- end
370
- end
371
- code
372
- end
373
- end
374
- end
375
-
376
- # @return [String] the StatusMessage value from a SAML Response.
377
- #
378
- def status_message
379
- @status_message ||= begin
380
- nodes = REXML::XPath.match(
381
- document,
382
- "/p:Response/p:Status/p:StatusMessage",
383
- { "p" => PROTOCOL }
384
- )
385
- if nodes.size == 1
386
- Utils.element_text(nodes.first)
387
- end
146
+ if success?
147
+ true
148
+ else
149
+ soft ? false : validation_error(status_message)
388
150
  end
389
151
  end
390
152
 
391
153
  def validate_structure(soft = true)
392
154
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
393
- @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
155
+ @schema = Nokogiri::XML::Schema(IO.read('saml-schema-protocol-2.0.xsd'))
394
156
  @xml = Nokogiri::XML(self.document.to_s)
395
157
  end
396
158
  if soft
397
- @schema.validate(@xml).map{ return false }
159
+ @schema.validate(@xml).map{
160
+ @errors << "Schema validation failed";
161
+ return false
162
+ }
398
163
  else
399
- @schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
164
+ @schema.validate(@xml).map{ |error| @errors << "#{error.message}\n\n#{@xml.to_s}";
165
+ validation_error("#{error.message}\n\n#{@xml.to_s}")
166
+ }
400
167
  end
401
168
  end
402
169
 
@@ -432,24 +199,13 @@ module OneLogin
432
199
  node
433
200
  end
434
201
 
435
- # Extracts all the appearances that matchs the subelt (pattern)
436
- # Search on any Assertion that is signed, or has a Response parent signed
437
- # @param subelt [String] The XPath pattern
438
- # @return [Array of REXML::Element] Return all matches
439
- #
440
- def xpath_from_signed_assertion(subelt=nil)
441
- node = REXML::XPath.match(
442
- document,
443
- "/p:Response/a:Assertion[@ID=$id]#{subelt}",
444
- { "p" => PROTOCOL, "a" => ASSERTION },
445
- { 'id' => document.signed_element_id }
446
- )
447
- node.concat( REXML::XPath.match(
448
- document,
449
- "/p:Response[@ID=$id]/a:Assertion#{subelt}",
450
- { "p" => PROTOCOL, "a" => ASSERTION },
451
- { 'id' => document.signed_element_id }
452
- ))
202
+ def get_fingerprint
203
+ if settings.idp_cert
204
+ cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
205
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
206
+ else
207
+ settings.idp_cert_fingerprint
208
+ end
453
209
  end
454
210
 
455
211
  def validate_conditions(soft = true)
@@ -459,10 +215,12 @@ module OneLogin
459
215
  now = Time.now.utc
460
216
 
461
217
  if not_before && (now + (options[:allowed_clock_drift] || 0)) < not_before
218
+ @errors << "Current time is earlier than NotBefore condition #{(now + (options[:allowed_clock_drift] || 0))} < #{not_before})"
462
219
  return soft ? false : validation_error("Current time is earlier than NotBefore condition")
463
220
  end
464
221
 
465
222
  if not_on_or_after && now >= not_on_or_after
223
+ @errors << "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after})"
466
224
  return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
467
225
  end
468
226
 
@@ -472,64 +230,9 @@ module OneLogin
472
230
  def validate_issuer(soft = true)
473
231
  return true if settings.idp_entity_id.nil?
474
232
 
475
- begin
476
- obtained_issuers = issuers
477
- rescue ValidationError => e
478
- return soft ? false : validation_error("Error while extracting issuers")
479
- end
480
-
481
- obtained_issuers.each do |issuer|
482
- unless OneLogin::RubySaml::Utils.uri_match?(issuer, settings.idp_entity_id)
483
- error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
484
- return soft ? false : validation_error(error_msg)
485
- end
486
- end
487
-
488
- true
489
- end
490
-
491
- def validate_signature(soft = true)
492
- error_msg = "Invalid Signature on SAML Response"
493
-
494
- sig_elements = REXML::XPath.match(
495
- document,
496
- "/p:Response[@ID=$id]/ds:Signature]",
497
- { "p" => PROTOCOL, "ds" => DSIG },
498
- { 'id' => document.signed_element_id }
499
- )
500
-
501
- # Check signature nodes
502
- if sig_elements.nil? || sig_elements.size == 0
503
- sig_elements = REXML::XPath.match(
504
- document,
505
- "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
506
- {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
507
- { 'id' => document.signed_element_id }
508
- )
509
- end
510
-
511
- if sig_elements.size != 1
512
- if sig_elements.size == 0
513
- error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
514
- else
515
- error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
516
- end
517
- return soft ? false : validation_error(error_msg)
518
- end
519
-
520
- opts = {}
521
- opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
522
- opts[:cert] = settings.get_idp_cert
523
- fingerprint = settings.get_fingerprint
524
-
525
- unless fingerprint
526
- return soft ? false : validation_error("No fingerprint or certificate on settings")
527
- end
528
-
529
- unless document.validate_document(fingerprint, soft, opts)
530
- return soft ? false : validation_error(error_msg)
233
+ unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
234
+ return soft ? false : validation_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
531
235
  end
532
-
533
236
  true
534
237
  end
535
238
 
@@ -538,19 +241,6 @@ module OneLogin
538
241
  Time.parse(node.attributes[attribute])
539
242
  end
540
243
  end
541
-
542
- def validate_audience(soft = true)
543
- return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
544
-
545
- unless audiences.include? settings.sp_entity_id
546
- s = audiences.count > 1 ? 's' : '';
547
- error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
548
- return soft ? false : validation_error(error_msg)
549
- end
550
-
551
- true
552
- end
553
-
554
244
  end
555
245
  end
556
246
  end
@@ -0,0 +1,78 @@
1
+ require 'cgi'
2
+ require 'zlib'
3
+ require 'base64'
4
+ require "rexml/document"
5
+ require "rexml/xpath"
6
+
7
+ module OneLogin
8
+ module RubySaml
9
+ class SamlMessage
10
+ include REXML
11
+
12
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
13
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
14
+
15
+ def valid_saml?(document, soft = true)
16
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
17
+ @schema = Nokogiri::XML::Schema(IO.read('saml-schema-protocol-2.0.xsd'))
18
+ @xml = Nokogiri::XML(document.to_s)
19
+ end
20
+ if soft
21
+ @schema.validate(@xml).map{ return false }
22
+ else
23
+ @schema.validate(@xml).map{ |error| validation_error("#{error.message}\n\n#{@xml.to_s}") }
24
+ end
25
+ end
26
+
27
+ def validation_error(message)
28
+ raise ValidationError.new(message)
29
+ end
30
+
31
+ private
32
+
33
+ def decode_raw_saml(saml)
34
+ if saml =~ /^</
35
+ return saml
36
+ elsif (decoded = decode(saml)) =~ /^</
37
+ return decoded
38
+ elsif (inflated = inflate(decoded)) =~ /^</
39
+ return inflated
40
+ end
41
+
42
+ return nil
43
+ end
44
+
45
+ def encode_raw_saml(saml, settings)
46
+ saml = Zlib::Deflate.deflate(saml, 9)[2..-5] if settings.compress_request
47
+ base64_saml = Base64.encode64(saml)
48
+ return CGI.escape(base64_saml)
49
+ end
50
+
51
+ def decode(encoded)
52
+ Base64.decode64(encoded)
53
+ end
54
+
55
+ def encode(encoded)
56
+ Base64.encode64(encoded).gsub(/\n/, "")
57
+ end
58
+
59
+ def escape(unescaped)
60
+ CGI.escape(unescaped)
61
+ end
62
+
63
+ def unescape(escaped)
64
+ CGI.unescape(escaped)
65
+ end
66
+
67
+ def inflate(deflated)
68
+ zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
69
+ zlib.inflate(deflated)
70
+ end
71
+
72
+ def deflate(inflated)
73
+ Zlib::Deflate.deflate(inflated, 9)[2..-5]
74
+ end
75
+
76
+ end
77
+ end
78
+ end