ruby-saml 1.4.0 → 1.4.1

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 268a395d4ed90393709bec460cd818d644bd7269
4
- data.tar.gz: 0507a8928e591569b9be618d2dac54b8c83a1847
3
+ metadata.gz: 785edd651b4a713d7a01a4841676034300a5465b
4
+ data.tar.gz: 33e3be90b834541836dad199a3843f0f50dc1d73
5
5
  SHA512:
6
- metadata.gz: 59e8e5300dcd4f521c19216686753cd2ce1a2480383bf6c873601a1031d96ef59241e13076a6b2e89cee61cc43f64c2afc383d6c985a8186cee0a7efe433aabd
7
- data.tar.gz: 00dfd3b12d124c41282b7edce5ae4214f96d696314120776f0cbeadc6c86c35cfac1279b8d3ef0b23ff6559ffec6365681254da2784d86b8fa659a825989ba02
6
+ metadata.gz: 7f0297c376b1b4ae225cd8b93a5541ff06829e89dcb8ee796b1ad1256a401532f1bf96ae257a8e63114f18de72333769d89f83bb4ac188e2659710dd75c9d3d0
7
+ data.tar.gz: ca1e2d0aefbdf122c4c214736c3c3d704184387f5becff71f4f1ebfdb1dc6b4d83a63dd1b267c12decdd69b1a8ada3db5886f4c6b64d46904a254e9dca85f260
data/changelog.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # RubySaml Changelog
2
2
 
3
+ ### 1.4.1 (October 19, 2016)
4
+ * [#357](https://github.com/onelogin/ruby-saml/pull/357) Add EncryptedAttribute support. Improve decrypt method
5
+ * Allow multiple authn_context_decl_ref in settings
6
+ * Allow options[:settings] to be an hash for Settings overrides in IdpMetadataParser#parse
7
+ * Recover issuers method
8
+
3
9
  ### 1.4.0 (October 13, 2016)
4
10
  * Several security improvements:
5
11
  * Conditions element required and unique.
@@ -136,16 +136,19 @@ module OneLogin
136
136
  }
137
137
 
138
138
  if settings.authn_context != nil
139
- authn_contexts = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context]
140
- authn_contexts.each do |authn_context|
139
+ authn_contexts_class_ref = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context]
140
+ authn_contexts_class_ref.each do |authn_context_class_ref|
141
141
  class_ref = requested_context.add_element "saml:AuthnContextClassRef"
142
- class_ref.text = authn_context
142
+ class_ref.text = authn_context_class_ref
143
143
  end
144
144
  end
145
- # add saml:AuthnContextDeclRef element
145
+
146
146
  if settings.authn_context_decl_ref != nil
147
- class_ref = requested_context.add_element "saml:AuthnContextDeclRef"
148
- class_ref.text = settings.authn_context_decl_ref
147
+ authn_contexts_decl_refs = settings.authn_context_decl_ref.is_a?(Array) ? settings.authn_context_decl_ref : [settings.authn_context_decl_ref]
148
+ authn_contexts_decl_refs.each do |authn_context_decl_ref|
149
+ decl_ref = requested_context.add_element "saml:AuthnContextDeclRef"
150
+ decl_ref.text = authn_context_decl_ref
151
+ end
149
152
  end
150
153
  end
151
154
 
@@ -27,7 +27,7 @@ module OneLogin
27
27
  # IdP values
28
28
  #
29
29
  # @param (see IdpMetadataParser#get_idp_metadata)
30
- # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
30
+ # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
31
31
  # @return (see IdpMetadataParser#get_idp_metadata)
32
32
  # @raise (see IdpMetadataParser#get_idp_metadata)
33
33
  def parse_remote(url, validate_cert = true, options = {})
@@ -37,12 +37,17 @@ module OneLogin
37
37
 
38
38
  # Parse the Identity Provider metadata and update the settings with the IdP values
39
39
  # @param idp_metadata [String]
40
- # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
40
+ # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
41
41
  #
42
42
  def parse(idp_metadata, options = {})
43
43
  @document = REXML::Document.new(idp_metadata)
44
44
 
45
- (options[:settings] || OneLogin::RubySaml::Settings.new).tap do |settings|
45
+ settings = options[:settings]
46
+ if settings.nil? || settings.is_a?(Hash)
47
+ settings = OneLogin::RubySaml::Settings.new(settings || {})
48
+ end
49
+
50
+ settings.tap do |settings|
46
51
  settings.idp_entity_id = idp_entity_id
47
52
  settings.name_identifier_format = idp_name_id_format
48
53
  settings.idp_sso_target_url = single_signon_service_url(options)
@@ -132,6 +132,7 @@ module OneLogin
132
132
  # attributes['name']
133
133
  #
134
134
  # @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
135
+ # @raise [ValidationError] if there are 2+ Attribute with the same Name
135
136
  #
136
137
  def attributes
137
138
  @attr_statements ||= begin
@@ -140,8 +141,19 @@ module OneLogin
140
141
  stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
141
142
  stmt_elements.each do |stmt_element|
142
143
  stmt_element.elements.each do |attr_element|
143
- name = attr_element.attributes["Name"]
144
- values = attr_element.elements.collect{|e|
144
+ if attr_element.name == "EncryptedAttribute"
145
+ node = decrypt_attribute(attr_element.dup)
146
+ else
147
+ node = attr_element
148
+ end
149
+
150
+ name = node.attributes["Name"]
151
+
152
+ if options[:check_duplicated_attributes] && attributes.include?(name)
153
+ raise ValidationError.new("Found an Attribute element with duplicated Name")
154
+ end
155
+
156
+ values = node.elements.collect{|e|
145
157
  if (e.elements.nil? || e.elements.size == 0)
146
158
  # SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
147
159
  # otherwise the value is to be regarded as empty.
@@ -236,6 +248,39 @@ module OneLogin
236
248
  @not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
237
249
  end
238
250
 
251
+ # Gets the Issuers (from Response and Assertion).
252
+ # (returns the first node that matches the supplied xpath from the Response and from the Assertion)
253
+ # @return [Array] Array with the Issuers (REXML::Element)
254
+ #
255
+ def issuers
256
+ @issuers ||= begin
257
+ issuers = []
258
+ issuer_response_nodes = REXML::XPath.match(
259
+ document,
260
+ "/p:Response/a:Issuer",
261
+ { "p" => PROTOCOL, "a" => ASSERTION }
262
+ )
263
+
264
+ unless issuer_response_nodes.size == 1
265
+ error_msg = "Issuer of the Response not found or multiple."
266
+ raise ValidationError.new(error_msg)
267
+ end
268
+
269
+ doc = decrypted_document.nil? ? document : decrypted_document
270
+ issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
271
+ unless issuer_assertion_nodes.size == 1
272
+ error_msg = "Issuer of the Assertion not found or multiple."
273
+ raise ValidationError.new(error_msg)
274
+ end
275
+
276
+ nodes = issuer_response_nodes + issuer_assertion_nodes
277
+ nodes.each do |node|
278
+ issuers << node.text if node.text
279
+ end
280
+ issuers.uniq
281
+ end
282
+ end
283
+
239
284
  # @return [String|nil] The InResponseTo attribute from the SAML Response.
240
285
  #
241
286
  def in_response_to
@@ -300,7 +345,6 @@ module OneLogin
300
345
  :validate_id,
301
346
  :validate_success_status,
302
347
  :validate_num_assertion,
303
- :validate_no_encrypted_attributes,
304
348
  :validate_no_duplicated_attributes,
305
349
  :validate_signed_elements,
306
350
  :validate_structure,
@@ -432,20 +476,6 @@ module OneLogin
432
476
  true
433
477
  end
434
478
 
435
- # Validates that there are not EncryptedAttribute (not supported)
436
- # If fails, the error is added to the errors array
437
- # @return [Boolean] True if there are no EncryptedAttribute elements, otherwise False if soft=True
438
- # @raise [ValidationError] if soft == false and validation fails
439
- #
440
- def validate_no_encrypted_attributes
441
- nodes = xpath_from_signed_assertion("/a:AttributeStatement/a:EncryptedAttribute")
442
- if nodes && nodes.length > 0
443
- return append_error("There is an EncryptedAttribute in the Response and this SP not support them")
444
- end
445
-
446
- true
447
- end
448
-
449
479
  # Validates that there are not duplicated attributes
450
480
  # If fails, the error is added to the errors array
451
481
  # @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True
@@ -453,16 +483,10 @@ module OneLogin
453
483
  #
454
484
  def validate_no_duplicated_attributes
455
485
  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
486
+ begin
487
+ attributes
488
+ rescue ValidationError => e
489
+ return append_error(e.message)
466
490
  end
467
491
  end
468
492
 
@@ -644,32 +668,13 @@ module OneLogin
644
668
  def validate_issuer
645
669
  return true if settings.idp_entity_id.nil?
646
670
 
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)
671
+ begin
672
+ obtained_issuers = issuers
673
+ rescue ValidationError => e
674
+ return append_error(e.message)
664
675
  end
665
676
 
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
-
672
- issuers.each do |issuer|
677
+ obtained_issuers.each do |issuer|
673
678
  unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
674
679
  error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
675
680
  return append_error(error_msg)
@@ -928,8 +933,17 @@ module OneLogin
928
933
  decrypt_element(encryptedid_node, /(.*<\/(\w+:)?NameID>)/m)
929
934
  end
930
935
 
936
+ # Decrypts an EncryptedID element
937
+ # @param encryptedid_node [REXML::Element] The EncryptedID element
938
+ # @return [REXML::Document] The decrypted EncrypedtID element
939
+ #
940
+ def decrypt_attribute(encryptedattribute_node)
941
+ decrypt_element(encryptedattribute_node, /(.*<\/(\w+:)?Attribute>)/m)
942
+ end
943
+
931
944
  # Decrypt an element
932
945
  # @param encryptedid_node [REXML::Element] The encrypted element
946
+ # @param rgrex string Regex
933
947
  # @return [REXML::Document] The decrypted element
934
948
  #
935
949
  def decrypt_element(encrypt_node, rgrex)
@@ -937,13 +951,21 @@ module OneLogin
937
951
  raise ValidationError.new('An ' + encrypt_node.name + ' found and no SP private key found on the settings to decrypt it')
938
952
  end
939
953
 
954
+
955
+ if encrypt_node.name == 'EncryptedAttribute'
956
+ node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
957
+ else
958
+ node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
959
+ end
960
+
940
961
  elem_plaintext = OneLogin::RubySaml::Utils.decrypt_data(encrypt_node, settings.get_sp_key)
941
962
  # If we get some problematic noise in the plaintext after decrypting.
942
963
  # This quick regexp parse will grab only the Element and discard the noise.
943
964
  elem_plaintext = elem_plaintext.match(rgrex)[0]
944
- # To avoid namespace errors if saml namespace is not defined at assertion_plaintext
945
- # create a parent node first with the saml namespace defined
946
- elem_plaintext = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + elem_plaintext + '</node>'
965
+
966
+ # To avoid namespace errors if saml namespace is not defined
967
+ # create a parent node first with the namespace defined
968
+ elem_plaintext = node_header + elem_plaintext + '</node>'
947
969
  doc = REXML::Document.new(elem_plaintext)
948
970
  doc.root[0]
949
971
  end
@@ -111,13 +111,13 @@ module OneLogin
111
111
  symmetric_key = retrieve_symmetric_key(encrypt_data, private_key)
112
112
  cipher_value = REXML::XPath.first(
113
113
  encrypt_data,
114
- "//xenc:EncryptedData/xenc:CipherData/xenc:CipherValue",
114
+ "./xenc:CipherData/xenc:CipherValue",
115
115
  { 'xenc' => XENC }
116
116
  )
117
117
  node = Base64.decode64(cipher_value.text)
118
118
  encrypt_method = REXML::XPath.first(
119
119
  encrypt_data,
120
- "//xenc:EncryptedData/xenc:EncryptionMethod",
120
+ "./xenc:EncryptionMethod",
121
121
  { 'xenc' => XENC }
122
122
  )
123
123
  algorithm = encrypt_method.attributes['Algorithm']
@@ -131,10 +131,12 @@ module OneLogin
131
131
  def self.retrieve_symmetric_key(encrypt_data, private_key)
132
132
  encrypted_key = REXML::XPath.first(
133
133
  encrypt_data,
134
- "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey or \
135
- //xenc:EncryptedKey[@Id=substring-after(//xenc:EncryptedData/ds:KeyInfo/ds:RetrievalMethod/@URI, '#')]",
136
- { "ds" => DSIG, "xenc" => XENC }
134
+ "./ds:KeyInfo/xenc:EncryptedKey or \
135
+ //xenc:EncryptedKey[@Id=$id]",
136
+ { "ds" => DSIG, "xenc" => XENC },
137
+ { "id" => self.retrieve_symetric_key_reference(encrypt_data) }
137
138
  )
139
+
138
140
  encrypted_symmetric_key_element = REXML::XPath.first(
139
141
  encrypted_key,
140
142
  "./xenc:CipherData/xenc:CipherValue",
@@ -150,6 +152,14 @@ module OneLogin
150
152
  retrieve_plaintext(cipher_text, private_key, algorithm)
151
153
  end
152
154
 
155
+ def self.retrieve_symetric_key_reference(encrypt_data)
156
+ REXML::XPath.first(
157
+ encrypt_data,
158
+ "substring-after(./ds:KeyInfo/ds:RetrievalMethod/@URI, '#')",
159
+ { "ds" => DSIG }
160
+ )
161
+ end
162
+
153
163
  # Obtains the deciphered text
154
164
  # @param cipher_text [String] The ciphered text
155
165
  # @param symmetric_key [String] The symetric key used to encrypt the text
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.4.0'
3
+ VERSION = '1.4.1'
4
4
  end
5
5
  end
@@ -56,6 +56,22 @@ class IdpMetadataParserTest < Minitest::Test
56
56
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
57
57
  end
58
58
 
59
+ it "uses settings options as hash for overrides" do
60
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
61
+ idp_metadata = read_response("idp_descriptor.xml")
62
+ settings = idp_metadata_parser.parse(idp_metadata, {
63
+ :settings => {
64
+ :security => {
65
+ :digest_method => XMLSecurity::Document::SHA256,
66
+ :signature_method => XMLSecurity::Document::RSA_SHA256
67
+ }
68
+ }
69
+ })
70
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
71
+ assert_equal XMLSecurity::Document::SHA256, settings.security[:digest_method]
72
+ assert_equal XMLSecurity::Document::RSA_SHA256, settings.security[:signature_method]
73
+ end
74
+
59
75
  end
60
76
 
61
77
  describe "download and parse IdP descriptor file" do
data/test/request_test.rb CHANGED
@@ -285,5 +285,12 @@ class RequestTest < Minitest::Test
285
285
  auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings)
286
286
  assert auth_doc.to_s =~ /<saml:AuthnContextDeclRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport<\/saml:AuthnContextDeclRef>/
287
287
  end
288
+
289
+ it "create multiple saml:AuthnContextDeclRef elements correctly " do
290
+ settings.authn_context_decl_ref = ['name/password/uri', 'example/decl/ref']
291
+ auth_doc = OneLogin::RubySaml::Authrequest.new.create_authentication_xml_doc(settings)
292
+ assert auth_doc.to_s =~ /<saml:AuthnContextDeclRef>name\/password\/uri<\/saml:AuthnContextDeclRef>/
293
+ assert auth_doc.to_s =~ /<saml:AuthnContextDeclRef>example\/decl\/ref<\/saml:AuthnContextDeclRef>/
294
+ end
288
295
  end
289
296
  end
@@ -28,7 +28,7 @@ class RubySamlTest < Minitest::Test
28
28
  let(:response_no_statuscode) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status_code.xml.base64")) }
29
29
  let(:response_statuscode_responder) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responder.xml.base64")) }
30
30
  let(:response_statuscode_responder_and_msg) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responer_and_msg.xml.base64")) }
31
- let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(read_invalid_response("response_encrypted_attrs.xml.base64")) }
31
+ let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(response_document_encrypted_attrs) }
32
32
  let(:response_no_signed_elements) { OneLogin::RubySaml::Response.new(read_invalid_response("no_signature.xml.base64")) }
33
33
  let(:response_multiple_signed) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_signed.xml.base64")) }
34
34
  let(:response_invalid_audience) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64")) }
@@ -198,17 +198,6 @@ class RubySamlTest < Minitest::Test
198
198
  assert_includes response_valid_signed.errors, error_msg
199
199
  end
200
200
 
201
- it "raise when the assertion contains encrypted attributes" do
202
- settings.idp_cert_fingerprint = signature_fingerprint_1
203
- response_encrypted_attrs.settings = settings
204
- response_encrypted_attrs.soft = false
205
- error_msg = "There is an EncryptedAttribute in the Response and this SP not support them"
206
- assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
207
- response_encrypted_attrs.is_valid?
208
- end
209
- assert_includes response_encrypted_attrs.errors, error_msg
210
- end
211
-
212
201
  it "raise when there is no valid audience" do
213
202
  settings.idp_cert_fingerprint = signature_fingerprint_1
214
203
  settings.issuer = 'invalid'
@@ -365,14 +354,6 @@ class RubySamlTest < Minitest::Test
365
354
  assert_includes response_valid_signed.errors, "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id"
366
355
  end
367
356
 
368
- it "return false when the assertion contains encrypted attributes" do
369
- settings.idp_cert_fingerprint = signature_fingerprint_1
370
- response_encrypted_attrs.settings = settings
371
- response_encrypted_attrs.soft = true
372
- response_encrypted_attrs.is_valid?
373
- assert_includes response_encrypted_attrs.errors, "There is an EncryptedAttribute in the Response and this SP not support them"
374
- end
375
-
376
357
  it "return false when there is no valid audience" do
377
358
  settings.idp_cert_fingerprint = signature_fingerprint_1
378
359
  settings.issuer = 'invalid'
@@ -559,20 +540,6 @@ class RubySamlTest < Minitest::Test
559
540
  end
560
541
  end
561
542
 
562
- describe "#validate_no_encrypted_attributes" do
563
- it "return true when the assertion does not contain encrypted attributes" do
564
- response_valid_signed.settings = settings
565
- assert response_valid_signed.send(:validate_no_encrypted_attributes)
566
- assert_empty response_valid_signed.errors
567
- end
568
-
569
- it "return false when the assertion contains encrypted attributes" do
570
- response_encrypted_attrs.settings = settings
571
- assert !response_encrypted_attrs.send(:validate_no_encrypted_attributes)
572
- assert_includes response_encrypted_attrs.errors, "There is an EncryptedAttribute in the Response and this SP not support them"
573
- end
574
- end
575
-
576
543
  describe "#validate_audience" do
577
544
  it "return true when the audience is valid" do
578
545
  response_valid_signed.settings = settings
@@ -953,15 +920,29 @@ class RubySamlTest < Minitest::Test
953
920
  assert_equal "bob", response_with_multiple_attribute_statements.attributes[:firstname]
954
921
  end
955
922
 
956
- it "not raise errors about nil/empty attributes for EncryptedAttributes" do
957
- response_no_cert_and_encrypted_attrs = OneLogin::RubySaml::Response.new(response_document_no_cert_and_encrypted_attrs)
958
- assert_equal 'Demo', response_no_cert_and_encrypted_attrs.attributes["first_name"]
959
- end
960
-
961
923
  it "not raise on responses without attributes" do
962
924
  assert_equal OneLogin::RubySaml::Attributes.new, response_unsigned.attributes
963
925
  end
964
926
 
927
+ describe "#encrypted attributes" do
928
+ it "raise error when the assertion contains encrypted attributes but no private key to decrypt" do
929
+ settings.private_key = nil
930
+ response_encrypted_attrs.settings = settings
931
+ assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedAttribute found and no SP private key found on the settings to decrypt it") do
932
+ attrs = response_encrypted_attrs.attributes
933
+ end
934
+ end
935
+
936
+ it "extract attributes when the assertion contains encrypted attributes and the private key is provided" do
937
+ settings.certificate = ruby_saml_cert_text
938
+ settings.private_key = ruby_saml_key_text
939
+ response_encrypted_attrs.settings = settings
940
+ attributes = response_encrypted_attrs.attributes
941
+ assert_equal "test", attributes[:uid]
942
+ assert_equal "test@example.com", attributes[:mail]
943
+ end
944
+ end
945
+
965
946
  it "return false when validating a response with duplicate attributes" do
966
947
  response_duplicated_attributes.settings = settings
967
948
  response_duplicated_attributes.options[:check_duplicated_attributes] = true
@@ -0,0 +1 @@
1
+ PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgRGVzdGluYXRpb249Imh0dHA6Ly9zdHVmZi5jb20vZW5kcG9pbnRzL2VuZHBvaW50cy9hY3MucGhwIiBJRD0icGZ4ODM2M2M2YTctZmQ1MC0yNjA0LTdmZTctZTZhNjgxYWE3ZmMxIiBJblJlc3BvbnNlVG89Il81N2JjYmY3MC03YjFmLTAxMmUtYzgyMS03ODJiY2IxM2JiMzgiIElzc3VlSW5zdGFudD0iMjAxMS0wNi0xN1QxNDo1NDoxNFoiIFZlcnNpb249IjIuMCI+DQogIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDgzNjNjNmE3LWZkNTAtMjYwNC03ZmU3LWU2YTY4MWFhN2ZjMSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+cEJ6cTlYdzBMeFBoK0x0M3VuTmdaQ081UzJjPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5wcmZlQWRzRE9mUUZUTEwwYk01WEFOQXh3cmNZZE9RTjF3U1g2eEtoSkRHU2I5V2NPUE1Pa3ZOczdRSUZyaE1nNEZpdXJtc0VnMlE1OTFRYkRVUStoNW5SbFo1NHRVcFh1eDBTOHFYSmxnd0JHWjJUWCtKeDk3TTB4NkJBS3I0cm05K0RuOGk1QW1zdU5QRUVieFhBdm5JRWJVOTNVem5wd3IyNG1OUllpdFU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNHekNDQVlRQ0NRQ05OY1FYb20zMlZEQU5CZ2txaGtpRzl3MEJBUVVGQURCU01Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQk1DU1U0eEZUQVRCZ05WQkFjVERFbHVaR2xoYm1Gd2IyeHBjekVSTUE4R0ExVUVDaE1JVDI1bFRHOW5hVzR4RERBS0JnTlZCQXNUQTBWdVp6QWVGdzB4TkRBME1qTXhPRFF4TURGYUZ3MHhOVEEwTWpNeE9EUXhNREZhTUZJeEN6QUpCZ05WQkFZVEFsVlRNUXN3Q1FZRFZRUUlFd0pKVGpFVk1CTUdBMVVFQnhNTVNXNWthV0Z1WVhCdmJHbHpNUkV3RHdZRFZRUUtFd2hQYm1WTWIyZHBiakVNTUFvR0ExVUVDeE1EUlc1bk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRG82bStRWnZZUS94TDBFbExndXBLMVFEY1lMNGY1UGNrd3NOZ1M5cFV2VjdmelRxQ0hrOFRoTHhUazQyTVEyTWNKc09lVUpWUDcyOEtoeW1qRkNxeGdQNFZ1d1JrOXJwQWwwK21oeTZNUGR5anlBNkcxNGpyRFdTNjV5c0xjaEs0dC92d3BFRHowU1FsRW9HMWtNemxsU203elpTM1hyZWdBN0RqTmFVWVFxd0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFMTTJ2R0NpUS92bSthNnY0MCtWWDJ6ZHFIQTJRLzF2RjFpYlF6SjU0TUpDT1ZXdnMrdlFYZlpGaGRtME9QTTJJckRVN29xdktQcVA2eE9BZUpLNkgweVA3TTRZTDNmYXRTdklZbW1meVhDOWt0M1N2ei9OeXJIelBoVW5KMHllL3NVU1h4bnpReHdjbS85UHdBcXJRYUEzUXBRa0g1N3liRi9Pb3J5UGUrMmg8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+DQogIDwvc2FtbHA6U3RhdHVzPg0KICA8c2FtbDpBc3NlcnRpb24geG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiBJRD0icGZ4Nzg0MTk5MWMtYzczZi00MDM1LWUyZWUtYzE3MGMwZTFkM2U0IiBJc3N1ZUluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MTRaIiBWZXJzaW9uPSIyLjAiPg0KICAgIDxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+ICAgIA0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiIFNQTmFtZVF1YWxpZmllcj0iaGVsbG8uY29tIj5zb21lb25lQGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD4NCiAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4NCiAgICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfNTdiY2JmNzAtN2IxZi0wMTJlLWM4MjEtNzgyYmNiMTNiYjM4IiBOb3RPbk9yQWZ0ZXI9IjIwNTEtMDYtMTdUMTQ6NTk6MTRaIiBSZWNpcGllbnQ9ImhodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHAiLz4NCiAgICAgIDwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPg0KICAgIDwvc2FtbDpTdWJqZWN0Pg0KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDEwLTA2LTE3VDE0OjUzOjQ0WiIgTm90T25PckFmdGVyPSIyMDUxLTA2LTE3VDE0OjU5OjE0WiI+DQogICAgICA8c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgICAgICA8c2FtbDpBdWRpZW5jZT5odHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+DQogICAgICA8L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICA8L3NhbWw6Q29uZGl0aW9ucz4NCiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTEtMDYtMTdUMTQ6NTQ6MDdaIiBTZXNzaW9uSW5kZXg9Il81MWJlMzc5NjVmZWI1NTc5ZDgwMzE0MTA3NjkzNmRjMmU5ZDFkOThlYmYiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTEtMDYtMTdUMjI6NTQ6MTRaIj4NCiAgICAgIDxzYW1sOkF1dGhuQ29udGV4dD4NCiAgICAgICAgPHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+DQogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0Pg0KICAgIDwvc2FtbDpBdXRoblN0YXRlbWVudD4NCiAgICA8c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgICANCiAgICA8c2FtbDpFbmNyeXB0ZWRBdHRyaWJ1dGU+PHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIi8+PGRzaWc6S2V5SW5mbyB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXk+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS0xXzUiLz48eGVuYzpDaXBoZXJEYXRhPjx4ZW5jOkNpcGhlclZhbHVlPmdZYnoxVm5Bcml1bXpqTnoweGNwc0o2QnRkOGtxNzlnb0dqNm5pcWlHY3dMV1NYTTBwaTRQNUd1K2tPejFubWRtSHlnUGlXN3RqalFOTWxBYVFvN1IvNVFURjh6a3F6SXltdGY3a0VpMkhQN29FbHMya1VoOWJ5WkR0akhqdTlueVY4ajlacXlydnZyaFVXNS90RmJHZkJ5UXdYOGhiVGl1TXV5eXcxeTltVT08L3hlbmM6Q2lwaGVyVmFsdWU+PC94ZW5jOkNpcGhlckRhdGE+PC94ZW5jOkVuY3J5cHRlZEtleT48L2RzaWc6S2V5SW5mbz4NCiAgIDx4ZW5jOkNpcGhlckRhdGE+DQogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5YWVhaQzNwUDArekFsazZYSGM4aERqQTk4R3MxNWRCaU5SZC9nU3U2MEw1blkzS3NpSkd0c0gzcGlmU2dLN2ZIWisyTzJrYUV3SmszY0djSThYc0VEcnk1YWp0NnRQT0RIRkZGeDFmV1o1SEQ5L1I4RG01U3pQSG9QMmxET2lBZWgwd3pUQjVWdUdrbU5nUmYyNjBkNkpDVndxY0RVc3FrZWJkMWkvTzd6V3VoUjFDRmZZK0l3c1l2bUdXMUlqdUQxcXVJUmdzNnFTSEt1SnhrWlM1NGp0RHhFaUR0VGN0aXhVaytlMHhHRXZUVUxKVFA5eDN5anZEV09FNGJpelZsSFJpVnVmMzRaVWJuV2hURlFiKzBIUT09PC94ZW5jOkNpcGhlclZhbHVlPg0KICAgPC94ZW5jOkNpcGhlckRhdGE+DQo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkQXR0cmlidXRlPg0KICAgICAgDQo8c2FtbDpFbmNyeXB0ZWRBdHRyaWJ1dGU+PHhlbmM6RW5jcnlwdGVkRGF0YSB4bWxuczp4ZW5jPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyMiIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiIFR5cGU9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI0VsZW1lbnQiPjx4ZW5jOkVuY3J5cHRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNhZXMxMjgtY2JjIi8+PGRzaWc6S2V5SW5mbyB4bWxuczpkc2lnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48eGVuYzpFbmNyeXB0ZWRLZXk+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3JzYS0xXzUiLz48eGVuYzpDaXBoZXJEYXRhPjx4ZW5jOkNpcGhlclZhbHVlPndqOVRBbTE4aGNkTGpqdnZ6bzdnbFF0djI3ME5FWS9wR1NpeVhIN0dXNGFWRllWWG5EbEVhSFRadFlmUnFXVXFJMCtENG5FZ1l2dzZzMEIxNzZpcWtNME1QS0ZUdUx5cDBxU2tuVDhYVmVJSmVHQ0xON1NPRFFBTE8ySlNCVk15aFNhM0MrSDN2MExITWhOVzdRMU5VeDA2WEFMYXRnTFBqc3RFaVhqYU41ST08L3hlbmM6Q2lwaGVyVmFsdWU+PC94ZW5jOkNpcGhlckRhdGE+PC94ZW5jOkVuY3J5cHRlZEtleT48L2RzaWc6S2V5SW5mbz4NCiAgIDx4ZW5jOkNpcGhlckRhdGE+DQogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT4xSy9SSHFjMHN1ekc5ZUo2ditHUXljTjh5RWtTc2lFdHFFRGZvYnZ6Qkx2UFQrYno4SVRNaDlCL3FJZno2TnhUREdPejQ1WW12WC92aHhoclZiYVcwRUpLemFsQXVRN0ZpMTlOSEl1NkllcW16a29JMGlQMzNsamZjRy91N1lPNmhJeDl5OU1qeWF5MjNFcEh5akIvcVNLaTF1emJzV2JLV21rWU8yUS9TM0NVZ242Mm40b01UQkowcFdKdEY5Y0NzcWxhZ0owZXBpYkl6VksrWDFJRm1aOE85cjF2d040ZzdZNmovNUZGbjNBQnhKT0hhZVNucU95dUU2N1RhSzdmdk9tMHNNU1RBVkV1NWlqWk9Fdk5LRVB0enRHZituNlFiSXF2bmxSTllNST08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICA8L3hlbmM6Q2lwaGVyRGF0YT4NCjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+
data/test/test_helper.rb CHANGED
@@ -2,6 +2,7 @@ require 'simplecov'
2
2
 
3
3
  SimpleCov.start do
4
4
  add_filter "test/"
5
+ add_filter "vendor/"
5
6
  add_filter "lib/onelogin/ruby-saml/logging.rb"
6
7
  end
7
8
 
@@ -131,6 +132,10 @@ class Minitest::Test
131
132
  @unsigned_message_encrypted_unsigned_assertion ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'unsigned_message_encrypted_unsigned_assertion.xml.base64'))
132
133
  end
133
134
 
135
+ def response_document_encrypted_attrs
136
+ @response_document_encrypted_attrs ||= File.read(File.join(File.dirname(__FILE__), 'responses', 'response_encrypted_attrs.xml.base64'))
137
+ end
138
+
134
139
  def signature_fingerprint_1
135
140
  @signature_fingerprint1 ||= "C5:19:85:D9:47:F1:BE:57:08:20:25:05:08:46:EB:27:F6:CA:B7:83"
136
141
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-saml
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OneLogin LLC
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-13 00:00:00.000000000 Z
11
+ date: 2016-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -249,7 +249,6 @@ files:
249
249
  - test/responses/invalids/no_status_code.xml.base64
250
250
  - test/responses/invalids/no_subjectconfirmation_data.xml.base64
251
251
  - test/responses/invalids/no_subjectconfirmation_method.xml.base64
252
- - test/responses/invalids/response_encrypted_attrs.xml.base64
253
252
  - test/responses/invalids/response_invalid_signed_element.xml.base64
254
253
  - test/responses/invalids/response_with_concealed_signed_assertion.xml
255
254
  - test/responses/invalids/response_with_doubled_signed_assertion.xml
@@ -260,6 +259,7 @@ files:
260
259
  - test/responses/no_signature_ns.xml
261
260
  - test/responses/open_saml_response.xml
262
261
  - test/responses/response_assertion_wrapped.xml.base64
262
+ - test/responses/response_encrypted_attrs.xml.base64
263
263
  - test/responses/response_encrypted_nameid.xml.base64
264
264
  - test/responses/response_eval.xml
265
265
  - test/responses/response_no_cert_and_encrypted_attrs.xml
@@ -390,7 +390,6 @@ test_files:
390
390
  - test/responses/invalids/no_status_code.xml.base64
391
391
  - test/responses/invalids/no_subjectconfirmation_data.xml.base64
392
392
  - test/responses/invalids/no_subjectconfirmation_method.xml.base64
393
- - test/responses/invalids/response_encrypted_attrs.xml.base64
394
393
  - test/responses/invalids/response_invalid_signed_element.xml.base64
395
394
  - test/responses/invalids/response_with_concealed_signed_assertion.xml
396
395
  - test/responses/invalids/response_with_doubled_signed_assertion.xml
@@ -401,6 +400,7 @@ test_files:
401
400
  - test/responses/no_signature_ns.xml
402
401
  - test/responses/open_saml_response.xml
403
402
  - test/responses/response_assertion_wrapped.xml.base64
403
+ - test/responses/response_encrypted_attrs.xml.base64
404
404
  - test/responses/response_encrypted_nameid.xml.base64
405
405
  - test/responses/response_eval.xml
406
406
  - test/responses/response_no_cert_and_encrypted_attrs.xml
@@ -1 +0,0 @@
1
- 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