ruby-saml 1.11.0 → 1.14.0

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 (160) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +25 -0
  3. data/{changelog.md → CHANGELOG.md} +49 -1
  4. data/README.md +363 -218
  5. data/UPGRADING.md +149 -0
  6. data/lib/onelogin/ruby-saml/attributes.rb +24 -1
  7. data/lib/onelogin/ruby-saml/authrequest.rb +12 -8
  8. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +154 -83
  9. data/lib/onelogin/ruby-saml/logoutrequest.rb +13 -7
  10. data/lib/onelogin/ruby-saml/logoutresponse.rb +6 -2
  11. data/lib/onelogin/ruby-saml/metadata.rb +62 -17
  12. data/lib/onelogin/ruby-saml/response.rb +57 -32
  13. data/lib/onelogin/ruby-saml/saml_message.rb +8 -3
  14. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  15. data/lib/onelogin/ruby-saml/settings.rb +92 -50
  16. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +17 -30
  17. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +32 -18
  18. data/lib/onelogin/ruby-saml/utils.rb +83 -8
  19. data/lib/onelogin/ruby-saml/version.rb +1 -1
  20. data/lib/xml_security.rb +39 -13
  21. data/ruby-saml.gemspec +14 -5
  22. metadata +29 -288
  23. data/.travis.yml +0 -46
  24. data/test/certificates/certificate.der +0 -0
  25. data/test/certificates/certificate1 +0 -12
  26. data/test/certificates/certificate_without_head_foot +0 -1
  27. data/test/certificates/formatted_certificate +0 -14
  28. data/test/certificates/formatted_chained_certificate +0 -42
  29. data/test/certificates/formatted_private_key +0 -12
  30. data/test/certificates/formatted_rsa_private_key +0 -12
  31. data/test/certificates/invalid_certificate1 +0 -1
  32. data/test/certificates/invalid_certificate2 +0 -1
  33. data/test/certificates/invalid_certificate3 +0 -12
  34. data/test/certificates/invalid_chained_certificate1 +0 -1
  35. data/test/certificates/invalid_private_key1 +0 -1
  36. data/test/certificates/invalid_private_key2 +0 -1
  37. data/test/certificates/invalid_private_key3 +0 -10
  38. data/test/certificates/invalid_rsa_private_key1 +0 -1
  39. data/test/certificates/invalid_rsa_private_key2 +0 -1
  40. data/test/certificates/invalid_rsa_private_key3 +0 -10
  41. data/test/certificates/ruby-saml-2.crt +0 -15
  42. data/test/certificates/ruby-saml.crt +0 -14
  43. data/test/certificates/ruby-saml.key +0 -15
  44. data/test/idp_metadata_parser_test.rb +0 -594
  45. data/test/logging_test.rb +0 -62
  46. data/test/logout_requests/invalid_slo_request.xml +0 -6
  47. data/test/logout_requests/slo_request.xml +0 -4
  48. data/test/logout_requests/slo_request.xml.base64 +0 -1
  49. data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
  50. data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
  51. data/test/logout_requests/slo_request_with_session_index.xml +0 -5
  52. data/test/logout_responses/logoutresponse_fixtures.rb +0 -86
  53. data/test/logoutrequest_test.rb +0 -260
  54. data/test/logoutresponse_test.rb +0 -427
  55. data/test/metadata/idp_descriptor.xml +0 -26
  56. data/test/metadata/idp_descriptor_2.xml +0 -56
  57. data/test/metadata/idp_descriptor_3.xml +0 -14
  58. data/test/metadata/idp_descriptor_4.xml +0 -72
  59. data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
  60. data/test/metadata/idp_metadata_multi_certs.xml +0 -75
  61. data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
  62. data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
  63. data/test/metadata/idp_multiple_descriptors.xml +0 -59
  64. data/test/metadata/idp_multiple_descriptors_2.xml +0 -59
  65. data/test/metadata/no_idp_descriptor.xml +0 -21
  66. data/test/metadata_test.rb +0 -331
  67. data/test/request_test.rb +0 -340
  68. data/test/response_test.rb +0 -1629
  69. data/test/responses/adfs_response_sha1.xml +0 -46
  70. data/test/responses/adfs_response_sha256.xml +0 -46
  71. data/test/responses/adfs_response_sha384.xml +0 -46
  72. data/test/responses/adfs_response_sha512.xml +0 -46
  73. data/test/responses/adfs_response_xmlns.xml +0 -45
  74. data/test/responses/attackxee.xml +0 -13
  75. data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
  76. data/test/responses/invalids/empty_destination.xml.base64 +0 -1
  77. data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
  78. data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
  79. data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
  80. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  81. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  82. data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
  83. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
  84. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
  85. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
  86. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
  87. data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
  88. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  89. data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
  90. data/test/responses/invalids/no_conditions.xml.base64 +0 -1
  91. data/test/responses/invalids/no_id.xml.base64 +0 -1
  92. data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
  93. data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
  94. data/test/responses/invalids/no_nameid.xml.base64 +0 -1
  95. data/test/responses/invalids/no_saml2.xml.base64 +0 -1
  96. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  97. data/test/responses/invalids/no_status.xml.base64 +0 -1
  98. data/test/responses/invalids/no_status_code.xml.base64 +0 -1
  99. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
  100. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
  101. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
  102. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  103. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  104. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  105. data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
  106. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
  107. data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
  108. data/test/responses/no_signature_ns.xml +0 -48
  109. data/test/responses/open_saml_response.xml +0 -56
  110. data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
  111. data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
  112. data/test/responses/response_double_status_code.xml.base64 +0 -1
  113. data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
  114. data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
  115. data/test/responses/response_eval.xml +0 -7
  116. data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
  117. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  118. data/test/responses/response_node_text_attack2.xml.base64 +0 -1
  119. data/test/responses/response_node_text_attack3.xml.base64 +0 -1
  120. data/test/responses/response_unsigned_xml_base64 +0 -1
  121. data/test/responses/response_with_ampersands.xml +0 -139
  122. data/test/responses/response_with_ampersands.xml.base64 +0 -93
  123. data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
  124. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  125. data/test/responses/response_with_multiple_attribute_values.xml +0 -67
  126. data/test/responses/response_with_retrieval_method.xml +0 -26
  127. data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
  128. data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
  129. data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
  130. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  131. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  132. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  133. data/test/responses/response_without_attributes.xml.base64 +0 -79
  134. data/test/responses/response_without_reference_uri.xml.base64 +0 -1
  135. data/test/responses/response_wrapped.xml.base64 +0 -150
  136. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
  137. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  138. data/test/responses/signed_nameid_in_atts.xml +0 -47
  139. data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
  140. data/test/responses/simple_saml_php.xml +0 -71
  141. data/test/responses/starfield_response.xml.base64 +0 -1
  142. data/test/responses/test_sign.xml +0 -43
  143. data/test/responses/unsigned_encrypted_adfs.xml +0 -23
  144. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
  145. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
  146. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
  147. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
  148. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
  149. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
  150. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  151. data/test/responses/valid_response.xml.base64 +0 -1
  152. data/test/responses/valid_response_with_formatted_x509certificate.xml.base64 +0 -1
  153. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  154. data/test/saml_message_test.rb +0 -56
  155. data/test/settings_test.rb +0 -338
  156. data/test/slo_logoutrequest_test.rb +0 -467
  157. data/test/slo_logoutresponse_test.rb +0 -233
  158. data/test/test_helper.rb +0 -333
  159. data/test/utils_test.rb +0 -259
  160. data/test/xml_security_test.rb +0 -421
@@ -2,6 +2,7 @@ require "onelogin/ruby-saml/logging"
2
2
 
3
3
  require "onelogin/ruby-saml/saml_message"
4
4
  require "onelogin/ruby-saml/utils"
5
+ require "onelogin/ruby-saml/setting_error"
5
6
 
6
7
  # Only supports SAML 2.0
7
8
  module OneLogin
@@ -12,7 +13,7 @@ module OneLogin
12
13
  class SloLogoutresponse < SamlMessage
13
14
 
14
15
  # Logout Response ID
15
- attr_reader :uuid
16
+ attr_accessor :uuid
16
17
 
17
18
  # Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class.
18
19
  # Asigns an ID, a random uuid.
@@ -21,23 +22,30 @@ module OneLogin
21
22
  @uuid = OneLogin::RubySaml::Utils.uuid
22
23
  end
23
24
 
25
+ def response_id
26
+ @uuid
27
+ end
28
+
24
29
  # Creates the Logout Response string.
25
30
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
26
31
  # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
27
32
  # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
28
33
  # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
34
+ # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
29
35
  # @return [String] Logout Request string that includes the SAMLRequest
30
36
  #
31
- def create(settings, request_id = nil, logout_message = nil, params = {})
32
- params = create_params(settings, request_id, logout_message, params)
33
- params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
37
+ def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
38
+ params = create_params(settings, request_id, logout_message, params, logout_status_code)
39
+ params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?'
40
+ url = settings.idp_slo_response_service_url || settings.idp_slo_service_url
34
41
  saml_response = CGI.escape(params.delete("SAMLResponse"))
35
42
  response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
36
43
  params.each_pair do |key, value|
37
44
  response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
38
45
  end
39
46
 
40
- @logout_url = settings.idp_slo_target_url + response_params
47
+ raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty?
48
+ @logout_url = url + response_params
41
49
  end
42
50
 
43
51
  # Creates the Get parameters for the logout response.
@@ -45,9 +53,10 @@ module OneLogin
45
53
  # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
46
54
  # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
47
55
  # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
56
+ # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
48
57
  # @return [Hash] Parameters
49
58
  #
50
- def create_params(settings, request_id = nil, logout_message = nil, params = {})
59
+ def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
51
60
  # The method expects :RelayState but sometimes we get 'RelayState' instead.
52
61
  # Based on the HashWithIndifferentAccess value in Rails we could experience
53
62
  # conflicts so this line will solve them.
@@ -58,7 +67,7 @@ module OneLogin
58
67
  params.delete('RelayState')
59
68
  end
60
69
 
61
- response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
70
+ response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
62
71
  response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
63
72
 
64
73
  response = ""
@@ -70,7 +79,7 @@ module OneLogin
70
79
  base64_response = encode(response)
71
80
  response_params = {"SAMLResponse" => base64_response}
72
81
 
73
- if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
82
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && settings.private_key
74
83
  params['SigAlg'] = settings.security[:signature_method]
75
84
  url_string = OneLogin::RubySaml::Utils.build_query(
76
85
  :type => 'SAMLResponse',
@@ -94,39 +103,44 @@ module OneLogin
94
103
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
95
104
  # @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
96
105
  # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
106
+ # @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
97
107
  # @return [String] The SAMLResponse String.
98
108
  #
99
- def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
100
- document = create_xml_document(settings, request_id, logout_message)
109
+ def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil)
110
+ document = create_xml_document(settings, request_id, logout_message, logout_status_code)
101
111
  sign_document(document, settings)
102
112
  end
103
113
 
104
- def create_xml_document(settings, request_id = nil, logout_message = nil)
114
+ def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil)
105
115
  time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
106
116
 
107
117
  response_doc = XMLSecurity::Document.new
108
118
  response_doc.uuid = uuid
109
119
 
120
+ destination = settings.idp_slo_response_service_url || settings.idp_slo_service_url
121
+
122
+
110
123
  root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
111
124
  root.attributes['ID'] = uuid
112
125
  root.attributes['IssueInstant'] = time
113
126
  root.attributes['Version'] = '2.0'
114
127
  root.attributes['InResponseTo'] = request_id unless request_id.nil?
115
- root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
128
+ root.attributes['Destination'] = destination unless destination.nil? or destination.empty?
116
129
 
117
130
  if settings.sp_entity_id != nil
118
131
  issuer = root.add_element "saml:Issuer"
119
132
  issuer.text = settings.sp_entity_id
120
133
  end
121
134
 
122
- # add success message
135
+ # add status
123
136
  status = root.add_element 'samlp:Status'
124
137
 
125
- # success status code
126
- status_code = status.add_element 'samlp:StatusCode'
127
- status_code.attributes['Value'] = 'urn:oasis:names:tc:SAML:2.0:status:Success'
138
+ # status code
139
+ status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success'
140
+ status_code_elem = status.add_element 'samlp:StatusCode'
141
+ status_code_elem.attributes['Value'] = status_code
128
142
 
129
- # success status message
143
+ # status message
130
144
  logout_message ||= 'Successfully Signed Out'
131
145
  status_message = status.add_element 'samlp:StatusMessage'
132
146
  status_message.text = logout_message
@@ -136,7 +150,7 @@ module OneLogin
136
150
 
137
151
  def sign_document(document, settings)
138
152
  # embed signature
139
- if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
153
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.private_key && settings.certificate
140
154
  private_key = settings.get_sp_key
141
155
  cert = settings.get_sp_cert
142
156
  document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
@@ -13,8 +13,26 @@ module OneLogin
13
13
  class Utils
14
14
  @@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
15
15
 
16
- DSIG = "http://www.w3.org/2000/09/xmldsig#"
17
- XENC = "http://www.w3.org/2001/04/xmlenc#"
16
+ BINDINGS = { :post => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
17
+ :redirect => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze }.freeze
18
+ DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze
19
+ XENC = "http://www.w3.org/2001/04/xmlenc#".freeze
20
+ DURATION_FORMAT = %r(^
21
+ (-?)P # 1: Duration sign
22
+ (?:
23
+ (?:(\d+)Y)? # 2: Years
24
+ (?:(\d+)M)? # 3: Months
25
+ (?:(\d+)D)? # 4: Days
26
+ (?:T
27
+ (?:(\d+)H)? # 5: Hours
28
+ (?:(\d+)M)? # 6: Minutes
29
+ (?:(\d+(?:[.,]\d+)?)S)? # 7: Seconds
30
+ )?
31
+ |
32
+ (\d+)W # 8: Weeks
33
+ )
34
+ $)x.freeze
35
+ UUID_PREFIX = '_'
18
36
 
19
37
  # Checks if the x509 cert provided is expired
20
38
  #
@@ -28,6 +46,37 @@ module OneLogin
28
46
  return cert.not_after < Time.now
29
47
  end
30
48
 
49
+ # Interprets a ISO8601 duration value relative to a given timestamp.
50
+ #
51
+ # @param duration [String] The duration, as a string.
52
+ # @param timestamp [Integer] The unix timestamp we should apply the
53
+ # duration to. Optional, default to the
54
+ # current time.
55
+ #
56
+ # @return [Integer] The new timestamp, after the duration is applied.
57
+ #
58
+ def self.parse_duration(duration, timestamp=Time.now.utc)
59
+ return nil if RUBY_VERSION < '1.9' # 1.8.7 not supported
60
+
61
+ matches = duration.match(DURATION_FORMAT)
62
+
63
+ if matches.nil?
64
+ raise Exception.new("Invalid ISO 8601 duration")
65
+ end
66
+
67
+ sign = matches[1] == '-' ? -1 : 1
68
+
69
+ durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks =
70
+ matches[2..8].map { |match| match ? sign * match.tr(',', '.').to_f : 0.0 }
71
+
72
+ initial_datetime = Time.at(timestamp).utc.to_datetime
73
+ final_datetime = initial_datetime.next_year(durYears)
74
+ final_datetime = final_datetime.next_month(durMonths)
75
+ final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
76
+ final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
77
+ return final_timestamp
78
+ end
79
+
31
80
  # Return a properly formatted x509 certificate
32
81
  #
33
82
  # @param cert [String] The original certificate
@@ -118,27 +167,36 @@ module OneLogin
118
167
  #
119
168
  # @param rawparams [Hash] Raw GET Parameters
120
169
  # @param params [Hash] GET Parameters
170
+ # @param lowercase_url_encoding [bool] Lowercase URL Encoding (For ADFS urlencode compatiblity)
121
171
  # @return [Hash] New raw parameters
122
172
  #
123
- def self.prepare_raw_get_params(rawparams, params)
173
+ def self.prepare_raw_get_params(rawparams, params, lowercase_url_encoding=false)
124
174
  rawparams ||= {}
125
175
 
126
176
  if rawparams['SAMLRequest'].nil? && !params['SAMLRequest'].nil?
127
- rawparams['SAMLRequest'] = CGI.escape(params['SAMLRequest'])
177
+ rawparams['SAMLRequest'] = escape_request_param(params['SAMLRequest'], lowercase_url_encoding)
128
178
  end
129
179
  if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil?
130
- rawparams['SAMLResponse'] = CGI.escape(params['SAMLResponse'])
180
+ rawparams['SAMLResponse'] = escape_request_param(params['SAMLResponse'], lowercase_url_encoding)
131
181
  end
132
182
  if rawparams['RelayState'].nil? && !params['RelayState'].nil?
133
- rawparams['RelayState'] = CGI.escape(params['RelayState'])
183
+ rawparams['RelayState'] = escape_request_param(params['RelayState'], lowercase_url_encoding)
134
184
  end
135
185
  if rawparams['SigAlg'].nil? && !params['SigAlg'].nil?
136
- rawparams['SigAlg'] = CGI.escape(params['SigAlg'])
186
+ rawparams['SigAlg'] = escape_request_param(params['SigAlg'], lowercase_url_encoding)
137
187
  end
138
188
 
139
189
  rawparams
140
190
  end
141
191
 
192
+ def self.escape_request_param(param, lowercase_url_encoding)
193
+ CGI.escape(param).tap do |escaped|
194
+ next unless lowercase_url_encoding
195
+
196
+ escaped.gsub!(/%[A-Fa-f0-9]{2}/) { |match| match.downcase }
197
+ end
198
+ end
199
+
142
200
  # Validate the Signature parameter sent on the HTTP-Redirect binding
143
201
  # @param params [Hash] Parameters to be used in the validation process
144
202
  # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
@@ -253,6 +311,9 @@ module OneLogin
253
311
  when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
254
312
  when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
255
313
  when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
314
+ when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(128, :GCM).decrypt
315
+ when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(192, :GCM).decrypt
316
+ when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt
256
317
  when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
257
318
  when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
258
319
  end
@@ -263,6 +324,16 @@ module OneLogin
263
324
  cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
264
325
  assertion_plaintext = cipher.update(data)
265
326
  assertion_plaintext << cipher.final
327
+ elsif auth_cipher
328
+ iv_len, text_len, tag_len = auth_cipher.iv_len, cipher_text.length, 16
329
+ data = cipher_text[iv_len..text_len-1-tag_len]
330
+ auth_cipher.padding = 0
331
+ auth_cipher.key = symmetric_key
332
+ auth_cipher.iv = cipher_text[0..iv_len-1]
333
+ auth_cipher.auth_data = ''
334
+ auth_cipher.auth_tag = cipher_text[text_len-tag_len..-1]
335
+ assertion_plaintext = auth_cipher.update(data)
336
+ assertion_plaintext << auth_cipher.final
266
337
  elsif rsa
267
338
  rsa.private_decrypt(cipher_text)
268
339
  elsif oaep
@@ -272,8 +343,12 @@ module OneLogin
272
343
  end
273
344
  end
274
345
 
346
+ def self.set_prefix(value)
347
+ UUID_PREFIX.replace value
348
+ end
349
+
275
350
  def self.uuid
276
- RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
351
+ "#{UUID_PREFIX}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
277
352
  end
278
353
 
279
354
  # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.11.0'
3
+ VERSION = '1.14.0'
4
4
  end
5
5
  end
data/lib/xml_security.rb CHANGED
@@ -159,15 +159,13 @@ module XMLSecurity
159
159
  x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
160
160
 
161
161
  # add the signature
162
- issuer_element = self.elements["//saml:Issuer"]
162
+ issuer_element = elements["//saml:Issuer"]
163
163
  if issuer_element
164
- self.root.insert_after issuer_element, signature_element
164
+ root.insert_after(issuer_element, signature_element)
165
+ elsif first_child = root.children[0]
166
+ root.insert_before(first_child, signature_element)
165
167
  else
166
- if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
167
- self.root.insert_before sp_sso_descriptor, signature_element
168
- else
169
- self.root.add_element(signature_element)
170
- end
168
+ root.add_element(signature_element)
171
169
  end
172
170
  end
173
171
 
@@ -212,7 +210,7 @@ module XMLSecurity
212
210
  begin
213
211
  cert = OpenSSL::X509::Certificate.new(cert_text)
214
212
  rescue OpenSSL::X509::CertificateError => _e
215
- return append_error("Certificate Error", soft)
213
+ return append_error("Document Certificate Error", soft)
216
214
  end
217
215
 
218
216
  if options[:fingerprint_alg]
@@ -224,7 +222,6 @@ module XMLSecurity
224
222
 
225
223
  # check cert matches registered idp cert
226
224
  if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
227
- @errors << "Fingerprint mismatch"
228
225
  return append_error("Fingerprint mismatch", soft)
229
226
  end
230
227
  else
@@ -241,7 +238,7 @@ module XMLSecurity
241
238
  validate_signature(base64_cert, soft)
242
239
  end
243
240
 
244
- def validate_document_with_cert(idp_cert)
241
+ def validate_document_with_cert(idp_cert, soft = true)
245
242
  # get cert from response
246
243
  cert_element = REXML::XPath.first(
247
244
  self,
@@ -255,12 +252,12 @@ module XMLSecurity
255
252
  begin
256
253
  cert = OpenSSL::X509::Certificate.new(cert_text)
257
254
  rescue OpenSSL::X509::CertificateError => _e
258
- return append_error("Certificate Error", soft)
255
+ return append_error("Document Certificate Error", soft)
259
256
  end
260
257
 
261
258
  # check saml response cert matches provided idp cert
262
259
  if idp_cert.to_pem != cert.to_pem
263
- return false
260
+ return append_error("Certificate of the Signature element does not match provided certificate", soft)
264
261
  end
265
262
  else
266
263
  base64_cert = Base64.encode64(idp_cert.to_pem)
@@ -326,6 +323,9 @@ module XMLSecurity
326
323
  '//ds:CanonicalizationMethod',
327
324
  { "ds" => DSIG }
328
325
  )
326
+
327
+ canon_algorithm = process_transforms(ref, canon_algorithm)
328
+
329
329
  canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
330
330
 
331
331
  digest_algorithm = algorithm(REXML::XPath.first(
@@ -342,7 +342,6 @@ module XMLSecurity
342
342
  digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
343
343
 
344
344
  unless digests_match?(hash, digest_value)
345
- @errors << "Digest mismatch"
346
345
  return append_error("Digest mismatch", soft)
347
346
  end
348
347
 
@@ -360,6 +359,33 @@ module XMLSecurity
360
359
 
361
360
  private
362
361
 
362
+ def process_transforms(ref, canon_algorithm)
363
+ transforms = REXML::XPath.match(
364
+ ref,
365
+ "//ds:Transforms/ds:Transform",
366
+ { "ds" => DSIG }
367
+ )
368
+
369
+ transforms.each do |transform_element|
370
+ if transform_element.attributes && transform_element.attributes["Algorithm"]
371
+ algorithm = transform_element.attributes["Algorithm"]
372
+ case algorithm
373
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
374
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
375
+ canon_algorithm = Nokogiri::XML::XML_C14N_1_0
376
+ when "http://www.w3.org/2006/12/xml-c14n11",
377
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
378
+ canon_algorithm = Nokogiri::XML::XML_C14N_1_1
379
+ when "http://www.w3.org/2001/10/xml-exc-c14n#",
380
+ "http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
381
+ canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
382
+ end
383
+ end
384
+ end
385
+
386
+ canon_algorithm
387
+ end
388
+
363
389
  def digests_match?(hash, digest_value)
364
390
  hash == digest_value
365
391
  end
data/ruby-saml.gemspec CHANGED
@@ -15,14 +15,13 @@ Gem::Specification.new do |s|
15
15
  "LICENSE",
16
16
  "README.md"
17
17
  ]
18
- s.files = `git ls-files`.split("\n")
19
- s.homepage = %q{http://github.com/onelogin/ruby-saml}
18
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ s.homepage = %q{https://github.com/onelogin/ruby-saml}
20
20
  s.rdoc_options = ["--charset=UTF-8"]
21
21
  s.require_paths = ["lib"]
22
22
  s.rubygems_version = %q{1.3.7}
23
23
  s.required_ruby_version = '>= 1.8.7'
24
24
  s.summary = %q{SAML Ruby Tookit}
25
- s.test_files = `git ls-files test/*`.split("\n")
26
25
 
27
26
  # Because runtime dependencies are determined at build time, we cannot make
28
27
  # Nokogiri's version dependent on the Ruby version, even though we would
@@ -31,6 +30,7 @@ Gem::Specification.new do |s|
31
30
  if JRUBY_VERSION < '9.2.0.0'
32
31
  s.add_runtime_dependency('nokogiri', '>= 1.8.2', '<= 1.8.5')
33
32
  s.add_runtime_dependency('jruby-openssl', '>= 0.9.8')
33
+ s.add_runtime_dependency('json', '< 2.3.0')
34
34
  else
35
35
  s.add_runtime_dependency('nokogiri', '>= 1.8.2')
36
36
  end
@@ -39,8 +39,12 @@ Gem::Specification.new do |s|
39
39
  s.add_runtime_dependency('nokogiri', '<= 1.5.11')
40
40
  elsif RUBY_VERSION < '2.1'
41
41
  s.add_runtime_dependency('nokogiri', '>= 1.5.10', '<= 1.6.8.1')
42
+ s.add_runtime_dependency('json', '< 2.3.0')
43
+ elsif RUBY_VERSION < '2.3'
44
+ s.add_runtime_dependency('nokogiri', '>= 1.9.1', '< 1.10.0')
42
45
  else
43
- s.add_runtime_dependency('nokogiri', '>= 1.5.10')
46
+ s.add_runtime_dependency('nokogiri', '>= 1.10.5')
47
+ s.add_runtime_dependency('rexml')
44
48
  end
45
49
 
46
50
  s.add_development_dependency('coveralls')
@@ -50,7 +54,12 @@ Gem::Specification.new do |s|
50
54
  s.add_development_dependency('shoulda', '~> 2.11')
51
55
  s.add_development_dependency('simplecov')
52
56
  s.add_development_dependency('systemu', '~> 2')
53
- s.add_development_dependency('timecop', '<= 0.6.0')
57
+
58
+ if RUBY_VERSION < '2.1'
59
+ s.add_development_dependency('timecop', '<= 0.6.0')
60
+ else
61
+ s.add_development_dependency('timecop', '~> 0.9')
62
+ end
54
63
 
55
64
  if defined?(JRUBY_VERSION)
56
65
  # All recent versions of JRuby play well with pry