ruby-saml 1.9.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 (159) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +25 -0
  3. data/{changelog.md → CHANGELOG.md} +64 -1
  4. data/README.md +394 -211
  5. data/UPGRADING.md +149 -0
  6. data/lib/onelogin/ruby-saml/attributes.rb +24 -1
  7. data/lib/onelogin/ruby-saml/authrequest.rb +26 -10
  8. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +285 -184
  9. data/lib/onelogin/ruby-saml/logging.rb +3 -3
  10. data/lib/onelogin/ruby-saml/logoutrequest.rb +26 -11
  11. data/lib/onelogin/ruby-saml/logoutresponse.rb +27 -11
  12. data/lib/onelogin/ruby-saml/metadata.rb +62 -17
  13. data/lib/onelogin/ruby-saml/response.rb +86 -37
  14. data/lib/onelogin/ruby-saml/saml_message.rb +14 -5
  15. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  16. data/lib/onelogin/ruby-saml/settings.rb +117 -41
  17. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +33 -31
  18. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +43 -20
  19. data/lib/onelogin/ruby-saml/utils.rb +101 -9
  20. data/lib/onelogin/ruby-saml/version.rb +1 -1
  21. data/lib/xml_security.rb +39 -13
  22. data/ruby-saml.gemspec +21 -8
  23. metadata +43 -284
  24. data/.travis.yml +0 -32
  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 -579
  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 -67
  53. data/test/logoutrequest_test.rb +0 -226
  54. data/test/logoutresponse_test.rb +0 -402
  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 -53
  64. data/test/metadata/no_idp_descriptor.xml +0 -21
  65. data/test/metadata_test.rb +0 -331
  66. data/test/request_test.rb +0 -323
  67. data/test/response_test.rb +0 -1619
  68. data/test/responses/adfs_response_sha1.xml +0 -46
  69. data/test/responses/adfs_response_sha256.xml +0 -46
  70. data/test/responses/adfs_response_sha384.xml +0 -46
  71. data/test/responses/adfs_response_sha512.xml +0 -46
  72. data/test/responses/adfs_response_xmlns.xml +0 -45
  73. data/test/responses/attackxee.xml +0 -13
  74. data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
  75. data/test/responses/invalids/empty_destination.xml.base64 +0 -1
  76. data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
  77. data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
  78. data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
  79. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  80. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  81. data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
  82. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
  83. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
  84. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
  85. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
  86. data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
  87. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  88. data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
  89. data/test/responses/invalids/no_conditions.xml.base64 +0 -1
  90. data/test/responses/invalids/no_id.xml.base64 +0 -1
  91. data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
  92. data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
  93. data/test/responses/invalids/no_nameid.xml.base64 +0 -1
  94. data/test/responses/invalids/no_saml2.xml.base64 +0 -1
  95. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  96. data/test/responses/invalids/no_status.xml.base64 +0 -1
  97. data/test/responses/invalids/no_status_code.xml.base64 +0 -1
  98. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
  99. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
  100. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
  101. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  102. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  103. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  104. data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
  105. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
  106. data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
  107. data/test/responses/no_signature_ns.xml +0 -48
  108. data/test/responses/open_saml_response.xml +0 -56
  109. data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
  110. data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
  111. data/test/responses/response_double_status_code.xml.base64 +0 -1
  112. data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
  113. data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
  114. data/test/responses/response_eval.xml +0 -7
  115. data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
  116. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  117. data/test/responses/response_node_text_attack2.xml.base64 +0 -1
  118. data/test/responses/response_node_text_attack3.xml.base64 +0 -1
  119. data/test/responses/response_unsigned_xml_base64 +0 -1
  120. data/test/responses/response_with_ampersands.xml +0 -139
  121. data/test/responses/response_with_ampersands.xml.base64 +0 -93
  122. data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
  123. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  124. data/test/responses/response_with_multiple_attribute_values.xml +0 -67
  125. data/test/responses/response_with_retrieval_method.xml +0 -26
  126. data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
  127. data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
  128. data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
  129. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  130. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  131. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  132. data/test/responses/response_without_attributes.xml.base64 +0 -79
  133. data/test/responses/response_without_reference_uri.xml.base64 +0 -1
  134. data/test/responses/response_wrapped.xml.base64 +0 -150
  135. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
  136. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  137. data/test/responses/signed_nameid_in_atts.xml +0 -47
  138. data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
  139. data/test/responses/simple_saml_php.xml +0 -71
  140. data/test/responses/starfield_response.xml.base64 +0 -1
  141. data/test/responses/test_sign.xml +0 -43
  142. data/test/responses/unsigned_encrypted_adfs.xml +0 -23
  143. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
  144. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
  145. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
  146. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
  147. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
  148. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
  149. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  150. data/test/responses/valid_response.xml.base64 +0 -1
  151. data/test/responses/valid_response_with_formatted_x509certificate.xml.base64 +0 -1
  152. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  153. data/test/saml_message_test.rb +0 -56
  154. data/test/settings_test.rb +0 -329
  155. data/test/slo_logoutrequest_test.rb +0 -448
  156. data/test/slo_logoutresponse_test.rb +0 -199
  157. data/test/test_helper.rb +0 -327
  158. data/test/utils_test.rb +0 -254
  159. data/test/xml_security_test.rb +0 -421
@@ -1,6 +1,7 @@
1
1
  require "onelogin/ruby-saml/logging"
2
2
  require "onelogin/ruby-saml/saml_message"
3
3
  require "onelogin/ruby-saml/utils"
4
+ require "onelogin/ruby-saml/setting_error"
4
5
 
5
6
  # Only supports SAML 2.0
6
7
  module OneLogin
@@ -11,7 +12,7 @@ module OneLogin
11
12
  class Logoutrequest < SamlMessage
12
13
 
13
14
  # Logout Request ID
14
- attr_reader :uuid
15
+ attr_accessor :uuid
15
16
 
16
17
  # Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class.
17
18
  # Asigns an ID, a random uuid.
@@ -20,6 +21,10 @@ module OneLogin
20
21
  @uuid = OneLogin::RubySaml::Utils.uuid
21
22
  end
22
23
 
24
+ def request_id
25
+ @uuid
26
+ end
27
+
23
28
  # Creates the Logout Request string.
24
29
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
25
30
  # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
@@ -27,13 +32,14 @@ module OneLogin
27
32
  #
28
33
  def create(settings, params={})
29
34
  params = create_params(settings, params)
30
- params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
35
+ params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?'
31
36
  saml_request = CGI.escape(params.delete("SAMLRequest"))
32
37
  request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
33
38
  params.each_pair do |key, value|
34
39
  request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
35
40
  end
36
- @logout_url = settings.idp_slo_target_url + request_params
41
+ raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty?
42
+ @logout_url = settings.idp_slo_service_url + request_params
37
43
  end
38
44
 
39
45
  # Creates the Get parameters for the logout request.
@@ -64,8 +70,8 @@ module OneLogin
64
70
  base64_request = encode(request)
65
71
  request_params = {"SAMLRequest" => base64_request}
66
72
 
67
- if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
68
- params['SigAlg'] = settings.security[:signature_method]
73
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key
74
+ params['SigAlg'] = settings.security[:signature_method]
69
75
  url_string = OneLogin::RubySaml::Utils.build_query(
70
76
  :type => 'SAMLRequest',
71
77
  :data => base64_request,
@@ -89,6 +95,11 @@ module OneLogin
89
95
  # @return [String] The SAMLRequest String.
90
96
  #
91
97
  def create_logout_request_xml_doc(settings)
98
+ document = create_xml_document(settings)
99
+ sign_document(document, settings)
100
+ end
101
+
102
+ def create_xml_document(settings)
92
103
  time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
93
104
 
94
105
  request_doc = XMLSecurity::Document.new
@@ -98,11 +109,11 @@ module OneLogin
98
109
  root.attributes['ID'] = uuid
99
110
  root.attributes['IssueInstant'] = time
100
111
  root.attributes['Version'] = "2.0"
101
- root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
112
+ root.attributes['Destination'] = settings.idp_slo_service_url unless settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty?
102
113
 
103
- if settings.issuer
114
+ if settings.sp_entity_id
104
115
  issuer = root.add_element "saml:Issuer"
105
- issuer.text = settings.issuer
116
+ issuer.text = settings.sp_entity_id
106
117
  end
107
118
 
108
119
  nameid = root.add_element "saml:NameID"
@@ -122,14 +133,18 @@ module OneLogin
122
133
  sessionindex.text = settings.sessionindex
123
134
  end
124
135
 
136
+ request_doc
137
+ end
138
+
139
+ def sign_document(document, settings)
125
140
  # embed signature
126
- if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
141
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate
127
142
  private_key = settings.get_sp_key
128
143
  cert = settings.get_sp_cert
129
- request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
144
+ document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
130
145
  end
131
146
 
132
- request_doc
147
+ document
133
148
  end
134
149
  end
135
150
  end
@@ -43,19 +43,20 @@ module OneLogin
43
43
  end
44
44
 
45
45
  @options = options
46
- @response = decode_raw_saml(response)
46
+ @response = decode_raw_saml(response, settings)
47
47
  @document = XMLSecurity::SignedDocument.new(@response)
48
48
  end
49
49
 
50
+ def response_id
51
+ id(document)
52
+ end
53
+
50
54
  # Checks if the Status has the "Success" code
51
55
  # @return [Boolean] True if the StatusCode is Sucess
52
56
  # @raise [ValidationError] if soft == false and validation fails
53
57
  #
54
58
  def success?
55
- unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
56
- return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>")
57
- end
58
- true
59
+ return status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
59
60
  end
60
61
 
61
62
  # @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
@@ -65,7 +66,7 @@ module OneLogin
65
66
  node = REXML::XPath.first(
66
67
  document,
67
68
  "/p:LogoutResponse",
68
- { "p" => PROTOCOL, "a" => ASSERTION }
69
+ { "p" => PROTOCOL }
69
70
  )
70
71
  node.nil? ? nil : node.attributes['InResponseTo']
71
72
  end
@@ -88,7 +89,7 @@ module OneLogin
88
89
  #
89
90
  def status_code
90
91
  @status_code ||= begin
91
- node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
92
+ node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL })
92
93
  node.nil? ? nil : node.attributes["Value"]
93
94
  end
94
95
  end
@@ -98,7 +99,7 @@ module OneLogin
98
99
  node = REXML::XPath.first(
99
100
  document,
100
101
  "/p:LogoutResponse/p:Status/p:StatusMessage",
101
- { "p" => PROTOCOL, "a" => ASSERTION }
102
+ { "p" => PROTOCOL }
102
103
  )
103
104
  Utils.element_text(node)
104
105
  end
@@ -166,7 +167,7 @@ module OneLogin
166
167
 
167
168
  return append_error("No settings on logout response") if settings.nil?
168
169
 
169
- return append_error("No issuer in settings of the logout response") if settings.issuer.nil?
170
+ return append_error("No sp_entity_id in settings of the logout response") if settings.sp_entity_id.nil?
170
171
 
171
172
  if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil?
172
173
  return append_error("No fingerprint or certificate on settings of the logout response")
@@ -211,7 +212,7 @@ module OneLogin
211
212
  return true unless options.has_key? :get_params
212
213
  return true unless options[:get_params].has_key? 'Signature'
213
214
 
214
- options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params])
215
+ options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding])
215
216
 
216
217
  if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil?
217
218
  options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg'])
@@ -231,13 +232,19 @@ module OneLogin
231
232
  :raw_sig_alg => options[:raw_get_params]['SigAlg']
232
233
  )
233
234
 
235
+ expired = false
234
236
  if idp_certs.nil? || idp_certs[:signing].empty?
235
237
  valid = OneLogin::RubySaml::Utils.verify_signature(
236
- :cert => settings.get_idp_cert,
238
+ :cert => idp_cert,
237
239
  :sig_alg => options[:get_params]['SigAlg'],
238
240
  :signature => options[:get_params]['Signature'],
239
241
  :query_string => query_string
240
242
  )
243
+ if valid && settings.security[:check_idp_cert_expiration]
244
+ if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
245
+ expired = true
246
+ end
247
+ end
241
248
  else
242
249
  valid = false
243
250
  idp_certs[:signing].each do |signing_idp_cert|
@@ -248,11 +255,20 @@ module OneLogin
248
255
  :query_string => query_string
249
256
  )
250
257
  if valid
258
+ if settings.security[:check_idp_cert_expiration]
259
+ if OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert)
260
+ expired = true
261
+ end
262
+ end
251
263
  break
252
264
  end
253
265
  end
254
266
  end
255
267
 
268
+ if expired
269
+ error_msg = "IdP x509 certificate expired"
270
+ return append_error(error_msg)
271
+ end
256
272
  unless valid
257
273
  error_msg = "Invalid Signature on Logout Response"
258
274
  return append_error(error_msg)
@@ -15,25 +15,56 @@ module OneLogin
15
15
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
16
16
  # @param pretty_print [Boolean] Pretty print or not the response
17
17
  # (No pretty print if you gonna validate the signature)
18
+ # @param valid_until [DateTime] Metadata's valid time
19
+ # @param cache_duration [Integer] Duration of the cache in seconds
18
20
  # @return [String] XML Metadata of the Service Provider
19
21
  #
20
- def generate(settings, pretty_print=false)
22
+ def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
21
23
  meta_doc = XMLSecurity::Document.new
24
+ add_xml_declaration(meta_doc)
25
+ root = add_root_element(meta_doc, settings, valid_until, cache_duration)
26
+ sp_sso = add_sp_sso_element(root, settings)
27
+ add_sp_certificates(sp_sso, settings)
28
+ add_sp_service_elements(sp_sso, settings)
29
+ add_extras(root, settings)
30
+ embed_signature(meta_doc, settings)
31
+ output_xml(meta_doc, pretty_print)
32
+ end
33
+
34
+ protected
35
+
36
+ def add_xml_declaration(meta_doc)
37
+ meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8')
38
+ end
39
+
40
+ def add_root_element(meta_doc, settings, valid_until, cache_duration)
22
41
  namespaces = {
23
42
  "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
24
43
  }
44
+
25
45
  if settings.attribute_consuming_service.configured?
26
46
  namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion"
27
47
  end
28
- root = meta_doc.add_element "md:EntityDescriptor", namespaces
29
- sp_sso = root.add_element "md:SPSSODescriptor", {
48
+
49
+ root = meta_doc.add_element("md:EntityDescriptor", namespaces)
50
+ root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
51
+ root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id
52
+ root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z') if valid_until
53
+ root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration
54
+ root
55
+ end
56
+
57
+ def add_sp_sso_element(root, settings)
58
+ root.add_element "md:SPSSODescriptor", {
30
59
  "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
31
60
  "AuthnRequestsSigned" => settings.security[:authn_requests_signed],
32
61
  "WantAssertionsSigned" => settings.security[:want_assertions_signed],
33
62
  }
63
+ end
34
64
 
35
- # Add KeyDescriptor if messages will be signed / encrypted
36
- # with SP certificate, and new SP certificate if any
65
+ # Add KeyDescriptor if messages will be signed / encrypted
66
+ # with SP certificate, and new SP certificate if any
67
+ def add_sp_certificates(sp_sso, settings)
37
68
  cert = settings.get_sp_cert
38
69
  cert_new = settings.get_sp_cert_new
39
70
 
@@ -56,10 +87,10 @@ module OneLogin
56
87
  end
57
88
  end
58
89
 
59
- root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
60
- if settings.issuer
61
- root.attributes["entityID"] = settings.issuer
62
- end
90
+ sp_sso
91
+ end
92
+
93
+ def add_sp_service_elements(sp_sso, settings)
63
94
  if settings.single_logout_service_url
64
95
  sp_sso.add_element "md:SingleLogoutService", {
65
96
  "Binding" => settings.single_logout_service_binding,
@@ -67,10 +98,12 @@ module OneLogin
67
98
  "ResponseLocation" => settings.single_logout_service_url
68
99
  }
69
100
  end
101
+
70
102
  if settings.name_identifier_format
71
103
  nameid = sp_sso.add_element "md:NameIDFormat"
72
104
  nameid.text = settings.name_identifier_format
73
105
  end
106
+
74
107
  if settings.assertion_consumer_service_url
75
108
  sp_sso.add_element "md:AssertionConsumerService", {
76
109
  "Binding" => settings.assertion_consumer_service_binding,
@@ -109,15 +142,27 @@ module OneLogin
109
142
  # <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
110
143
  # <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
111
144
 
112
- meta_doc << REXML::XMLDecl.new("1.0", "UTF-8")
145
+ sp_sso
146
+ end
113
147
 
114
- # embed signature
115
- if settings.security[:metadata_signed] && settings.private_key && settings.certificate
116
- private_key = settings.get_sp_key
117
- meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
118
- end
148
+ # can be overridden in subclass
149
+ def add_extras(root, _settings)
150
+ root
151
+ end
152
+
153
+ def embed_signature(meta_doc, settings)
154
+ return unless settings.security[:metadata_signed]
155
+
156
+ private_key = settings.get_sp_key
157
+ cert = settings.get_sp_cert
158
+ return unless private_key && cert
159
+
160
+ meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
161
+ end
162
+
163
+ def output_xml(meta_doc, pretty_print)
164
+ ret = ''
119
165
 
120
- ret = ""
121
166
  # pretty print the XML so IdP administrators can easily see what the SP supports
122
167
  if pretty_print
123
168
  meta_doc.write(ret, 1)
@@ -125,7 +170,7 @@ module OneLogin
125
170
  ret = meta_doc.to_s
126
171
  end
127
172
 
128
- return ret
173
+ ret
129
174
  end
130
175
  end
131
176
  end
@@ -34,7 +34,7 @@ module OneLogin
34
34
  # This is not a whitelist to allow people extending OneLogin::RubySaml:Response
35
35
  # and pass custom options
36
36
  AVAILABLE_OPTIONS = [
37
- :allowed_clock_drift, :check_duplicated_attributes, :matches_request_id, :settings, :skip_authnstatement, :skip_conditions,
37
+ :allowed_clock_drift, :check_duplicated_attributes, :matches_request_id, :settings, :skip_audience, :skip_authnstatement, :skip_conditions,
38
38
  :skip_destination, :skip_recipient_check, :skip_subject_confirmation
39
39
  ]
40
40
  # TODO: Update the comment on initialize to describe every option
@@ -47,6 +47,8 @@ module OneLogin
47
47
  # or :matches_request_id that will validate that the response matches the ID of the request,
48
48
  # or skip the subject confirmation validation with the :skip_subject_confirmation option
49
49
  # or skip the recipient validation of the subject confirmation element with :skip_recipient_check option
50
+ # or skip the audience validation with :skip_audience option
51
+ #
50
52
  def initialize(response, options = {})
51
53
  raise ArgumentError.new("Response cannot be nil") if response.nil?
52
54
 
@@ -61,7 +63,7 @@ module OneLogin
61
63
  end
62
64
  end
63
65
 
64
- @response = decode_raw_saml(response)
66
+ @response = decode_raw_saml(response, settings)
65
67
  @document = XMLSecurity::SignedDocument.new(@response, @errors)
66
68
 
67
69
  if assertion_encrypted?
@@ -225,11 +227,10 @@ module OneLogin
225
227
  statuses = nodes.collect do |inner_node|
226
228
  inner_node.attributes["Value"]
227
229
  end
228
- extra_code = statuses.join(" | ")
229
- if extra_code
230
- code = "#{code} | #{extra_code}"
231
- end
230
+
231
+ code = [code, statuses].flatten.join(" | ")
232
232
  end
233
+
233
234
  code
234
235
  end
235
236
  end
@@ -336,9 +337,31 @@ module OneLogin
336
337
  end
337
338
 
338
339
  # returns the allowed clock drift on timing validation
339
- # @return [Integer]
340
+ # @return [Float]
340
341
  def allowed_clock_drift
341
- return options[:allowed_clock_drift].to_f
342
+ options[:allowed_clock_drift].to_f.abs + Float::EPSILON
343
+ end
344
+
345
+ # Checks if the SAML Response contains or not an EncryptedAssertion element
346
+ # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
347
+ #
348
+ def assertion_encrypted?
349
+ ! REXML::XPath.first(
350
+ document,
351
+ "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
352
+ { "p" => PROTOCOL, "a" => ASSERTION }
353
+ ).nil?
354
+ end
355
+
356
+ def response_id
357
+ id(document)
358
+ end
359
+
360
+ def assertion_id
361
+ @assertion_id ||= begin
362
+ node = xpath_first_from_signed_assertion("")
363
+ node.nil? ? nil : node.attributes['ID']
364
+ end
342
365
  end
343
366
 
344
367
  private
@@ -353,7 +376,6 @@ module OneLogin
353
376
  return false unless validate_response_state
354
377
 
355
378
  validations = [
356
- :validate_response_state,
357
379
  :validate_version,
358
380
  :validate_id,
359
381
  :validate_success_status,
@@ -435,7 +457,7 @@ module OneLogin
435
457
  # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
436
458
  #
437
459
  def validate_id
438
- unless id(document)
460
+ unless response_id
439
461
  return append_error("Missing ID attribute on SAML Response")
440
462
  end
441
463
 
@@ -584,16 +606,23 @@ module OneLogin
584
606
  end
585
607
 
586
608
  # Validates the Audience, (If the Audience match the Service Provider EntityID)
609
+ # If the response was initialized with the :skip_audience option, this validation is skipped,
587
610
  # If fails, the error is added to the errors array
588
611
  # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
589
612
  # @raise [ValidationError] if soft == false and validation fails
590
613
  #
591
614
  def validate_audience
592
- return true if audiences.empty? || settings.issuer.nil? || settings.issuer.empty?
615
+ return true if options[:skip_audience]
616
+ return true if settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
617
+
618
+ if audiences.empty?
619
+ return true unless settings.security[:strict_audience_validation]
620
+ return append_error("Invalid Audiences. The <AudienceRestriction> element contained only empty <Audience> elements. Expected audience #{settings.sp_entity_id}.")
621
+ end
593
622
 
594
- unless audiences.include? settings.issuer
623
+ unless audiences.include? settings.sp_entity_id
595
624
  s = audiences.count > 1 ? 's' : '';
596
- error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.issuer}"
625
+ error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
597
626
  return append_error(error_msg)
598
627
  end
599
628
 
@@ -668,13 +697,13 @@ module OneLogin
668
697
 
669
698
  now = Time.now.utc
670
699
 
671
- if not_before && (now_with_drift = now + allowed_clock_drift) < not_before
672
- error_msg = "Current time is earlier than NotBefore condition (#{now_with_drift} < #{not_before})"
700
+ if not_before && now < (not_before - allowed_clock_drift)
701
+ error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
673
702
  return append_error(error_msg)
674
703
  end
675
704
 
676
- if not_on_or_after && now >= (not_on_or_after_with_drift = not_on_or_after + allowed_clock_drift)
677
- error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after_with_drift})"
705
+ if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
706
+ error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
678
707
  return append_error(error_msg)
679
708
  end
680
709
 
@@ -716,7 +745,7 @@ module OneLogin
716
745
  return true if session_expires_at.nil?
717
746
 
718
747
  now = Time.now.utc
719
- unless (session_expires_at + allowed_clock_drift) > now
748
+ unless now < (session_expires_at + allowed_clock_drift)
720
749
  error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
721
750
  return append_error(error_msg)
722
751
  end
@@ -754,8 +783,8 @@ module OneLogin
754
783
 
755
784
  attrs = confirmation_data_node.attributes
756
785
  next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
757
- (attrs.include? "NotOnOrAfter" and (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift) <= now) ||
758
- (attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift)) ||
786
+ (attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
787
+ (attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
759
788
  (attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
760
789
 
761
790
  valid_subject_confirmation = true
@@ -781,8 +810,8 @@ module OneLogin
781
810
  return append_error("An empty NameID value found")
782
811
  end
783
812
 
784
- unless settings.issuer.nil? || settings.issuer.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
785
- if name_id_spnamequalifier != settings.issuer
813
+ unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
814
+ if name_id_spnamequalifier != settings.sp_entity_id
786
815
  return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
787
816
  end
788
817
  end
@@ -802,7 +831,7 @@ module OneLogin
802
831
  # otherwise, review if the decrypted assertion contains a signature
803
832
  sig_elements = REXML::XPath.match(
804
833
  document,
805
- "/p:Response[@ID=$id]/ds:Signature]",
834
+ "/p:Response[@ID=$id]/ds:Signature",
806
835
  { "p" => PROTOCOL, "ds" => DSIG },
807
836
  { 'id' => document.signed_element_id }
808
837
  )
@@ -821,28 +850,59 @@ module OneLogin
821
850
  end
822
851
 
823
852
  if sig_elements.size != 1
853
+ if sig_elements.size == 0
854
+ append_error("Signed element id ##{doc.signed_element_id} is not found")
855
+ else
856
+ append_error("Signed element id ##{doc.signed_element_id} is found more than once")
857
+ end
824
858
  return append_error(error_msg)
825
859
  end
826
860
 
861
+ old_errors = @errors.clone
862
+
827
863
  idp_certs = settings.get_idp_cert_multi
828
864
  if idp_certs.nil? || idp_certs[:signing].empty?
829
865
  opts = {}
830
866
  opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
831
- opts[:cert] = settings.get_idp_cert
867
+ idp_cert = settings.get_idp_cert
832
868
  fingerprint = settings.get_fingerprint
869
+ opts[:cert] = idp_cert
833
870
 
834
- unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
871
+ if fingerprint && doc.validate_document(fingerprint, @soft, opts)
872
+ if settings.security[:check_idp_cert_expiration]
873
+ if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
874
+ error_msg = "IdP x509 certificate expired"
875
+ return append_error(error_msg)
876
+ end
877
+ end
878
+ else
835
879
  return append_error(error_msg)
836
880
  end
837
881
  else
838
882
  valid = false
883
+ expired = false
839
884
  idp_certs[:signing].each do |idp_cert|
840
- valid = doc.validate_document_with_cert(idp_cert)
885
+ valid = doc.validate_document_with_cert(idp_cert, true)
841
886
  if valid
887
+ if settings.security[:check_idp_cert_expiration]
888
+ if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
889
+ expired = true
890
+ end
891
+ end
892
+
893
+ # At least one certificate is valid, restore the old accumulated errors
894
+ @errors = old_errors
842
895
  break
843
896
  end
897
+
898
+ end
899
+ if expired
900
+ error_msg = "IdP x509 certificate expired"
901
+ return append_error(error_msg)
844
902
  end
845
903
  unless valid
904
+ # Remove duplicated errors
905
+ @errors = @errors.uniq
846
906
  return append_error(error_msg)
847
907
  end
848
908
  end
@@ -943,17 +1003,6 @@ module OneLogin
943
1003
  XMLSecurity::SignedDocument.new(response_node.to_s)
944
1004
  end
945
1005
 
946
- # Checks if the SAML Response contains or not an EncryptedAssertion element
947
- # @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
948
- #
949
- def assertion_encrypted?
950
- ! REXML::XPath.first(
951
- document,
952
- "(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
953
- { "p" => PROTOCOL, "a" => ASSERTION }
954
- ).nil?
955
- end
956
-
957
1006
  # Decrypts an EncryptedAssertion element
958
1007
  # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
959
1008
  # @return [REXML::Document] The decrypted EncryptedAssertion element
@@ -16,8 +16,8 @@ module OneLogin
16
16
  class SamlMessage
17
17
  include REXML
18
18
 
19
- ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
20
- PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
19
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion".freeze
20
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol".freeze
21
21
 
22
22
  BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
23
23
  @@mutex = Mutex.new
@@ -86,9 +86,14 @@ module OneLogin
86
86
  # @param saml [String] The deflated and encoded SAML Message
87
87
  # @return [String] The plain SAML Message
88
88
  #
89
- def decode_raw_saml(saml)
89
+ def decode_raw_saml(saml, settings = nil)
90
90
  return saml unless base64_encoded?(saml)
91
91
 
92
+ settings = OneLogin::RubySaml::Settings.new if settings.nil?
93
+ if saml.bytesize > settings.message_max_bytesize
94
+ raise ValidationError.new("Encoded SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected")
95
+ end
96
+
92
97
  decoded = decode(saml)
93
98
  begin
94
99
  inflate(decoded)
@@ -105,7 +110,7 @@ module OneLogin
105
110
  def encode_raw_saml(saml, settings)
106
111
  saml = deflate(saml) if settings.compress_request
107
112
 
108
- CGI.escape(Base64.encode64(saml))
113
+ CGI.escape(encode(saml))
109
114
  end
110
115
 
111
116
  # Base 64 decode method
@@ -121,7 +126,11 @@ module OneLogin
121
126
  # @return [String] The encoded string
122
127
  #
123
128
  def encode(string)
124
- Base64.encode64(string).gsub(/\n/, "")
129
+ if Base64.respond_to?('strict_encode64')
130
+ Base64.strict_encode64(string)
131
+ else
132
+ Base64.encode64(string).gsub(/\n/, "")
133
+ end
125
134
  end
126
135
 
127
136
  # Check if a string is base64 encoded
@@ -0,0 +1,6 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class SettingError < StandardError
4
+ end
5
+ end
6
+ end