ruby-saml 1.8.0 → 1.13.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 +4 -4
  2. data/.github/workflows/test.yml +25 -0
  3. data/{changelog.md → CHANGELOG.md} +66 -1
  4. data/README.md +365 -209
  5. data/UPGRADING.md +149 -0
  6. data/lib/onelogin/ruby-saml/attribute_service.rb +1 -1
  7. data/lib/onelogin/ruby-saml/attributes.rb +24 -1
  8. data/lib/onelogin/ruby-saml/authrequest.rb +25 -9
  9. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +285 -184
  10. data/lib/onelogin/ruby-saml/logging.rb +4 -1
  11. data/lib/onelogin/ruby-saml/logoutrequest.rb +25 -10
  12. data/lib/onelogin/ruby-saml/logoutresponse.rb +33 -17
  13. data/lib/onelogin/ruby-saml/metadata.rb +62 -17
  14. data/lib/onelogin/ruby-saml/response.rb +89 -45
  15. data/lib/onelogin/ruby-saml/saml_message.rb +17 -8
  16. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  17. data/lib/onelogin/ruby-saml/settings.rb +124 -43
  18. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +38 -11
  19. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +42 -19
  20. data/lib/onelogin/ruby-saml/utils.rb +94 -12
  21. data/lib/onelogin/ruby-saml/version.rb +1 -1
  22. data/lib/xml_security.rb +44 -19
  23. data/ruby-saml.gemspec +16 -8
  24. metadata +44 -282
  25. data/.travis.yml +0 -26
  26. data/test/certificates/certificate1 +0 -12
  27. data/test/certificates/certificate_without_head_foot +0 -1
  28. data/test/certificates/formatted_certificate +0 -14
  29. data/test/certificates/formatted_chained_certificate +0 -42
  30. data/test/certificates/formatted_private_key +0 -12
  31. data/test/certificates/formatted_rsa_private_key +0 -12
  32. data/test/certificates/invalid_certificate1 +0 -1
  33. data/test/certificates/invalid_certificate2 +0 -1
  34. data/test/certificates/invalid_certificate3 +0 -12
  35. data/test/certificates/invalid_chained_certificate1 +0 -1
  36. data/test/certificates/invalid_private_key1 +0 -1
  37. data/test/certificates/invalid_private_key2 +0 -1
  38. data/test/certificates/invalid_private_key3 +0 -10
  39. data/test/certificates/invalid_rsa_private_key1 +0 -1
  40. data/test/certificates/invalid_rsa_private_key2 +0 -1
  41. data/test/certificates/invalid_rsa_private_key3 +0 -10
  42. data/test/certificates/ruby-saml-2.crt +0 -15
  43. data/test/certificates/ruby-saml.crt +0 -14
  44. data/test/certificates/ruby-saml.key +0 -15
  45. data/test/idp_metadata_parser_test.rb +0 -579
  46. data/test/logging_test.rb +0 -62
  47. data/test/logout_requests/invalid_slo_request.xml +0 -6
  48. data/test/logout_requests/slo_request.xml +0 -4
  49. data/test/logout_requests/slo_request.xml.base64 +0 -1
  50. data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
  51. data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
  52. data/test/logout_requests/slo_request_with_session_index.xml +0 -5
  53. data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
  54. data/test/logoutrequest_test.rb +0 -226
  55. data/test/logoutresponse_test.rb +0 -402
  56. data/test/metadata/idp_descriptor.xml +0 -26
  57. data/test/metadata/idp_descriptor_2.xml +0 -56
  58. data/test/metadata/idp_descriptor_3.xml +0 -14
  59. data/test/metadata/idp_descriptor_4.xml +0 -72
  60. data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
  61. data/test/metadata/idp_metadata_multi_certs.xml +0 -75
  62. data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
  63. data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
  64. data/test/metadata/idp_multiple_descriptors.xml +0 -53
  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 -323
  68. data/test/response_test.rb +0 -1586
  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_without_x509certificate.xml.base64 +0 -1
  153. data/test/saml_message_test.rb +0 -56
  154. data/test/settings_test.rb +0 -301
  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
@@ -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
@@ -24,7 +24,7 @@ module OneLogin
24
24
  # Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class.
25
25
  # @param response [String] A UUEncoded logout response from the IdP.
26
26
  # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
27
- # @param options [Hash] Extra parameters.
27
+ # @param options [Hash] Extra parameters.
28
28
  # :matches_request_id It will validate that the logout response matches the ID of the request.
29
29
  # :get_params GET Parameters, including the SAMLResponse
30
30
  # :relax_signature_validation to accept signatures if no idp certificate registered on settings
@@ -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
@@ -146,7 +147,7 @@ module OneLogin
146
147
 
147
148
  # Validates the Logout Response against the specified schema.
148
149
  # @return [Boolean] True if the XML is valid, otherwise False if soft=True
149
- # @raise [ValidationError] if soft == false and validation fails
150
+ # @raise [ValidationError] if soft == false and validation fails
150
151
  #
151
152
  def validate_structure
152
153
  unless valid_saml?(document, soft)
@@ -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")
@@ -205,7 +206,7 @@ module OneLogin
205
206
  # Validates the Signature if it exists and the GET parameters are provided
206
207
  # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
207
208
  # @raise [ValidationError] if soft == false and validation fails
208
- #
209
+ #
209
210
  def validate_signature
210
211
  return true unless !options.nil?
211
212
  return true unless options.has_key? :get_params
@@ -231,33 +232,48 @@ 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
- idp_certs[:signing].each do |idp_cert|
250
+ idp_certs[:signing].each do |signing_idp_cert|
244
251
  valid = OneLogin::RubySaml::Utils.verify_signature(
245
- :cert => idp_cert,
252
+ :cert => signing_idp_cert,
246
253
  :sig_alg => options[:get_params]['SigAlg'],
247
254
  :signature => options[:get_params]['Signature'],
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)
259
275
  end
260
- true
276
+ true
261
277
  end
262
278
 
263
279
  end
@@ -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?
@@ -171,9 +173,10 @@ module OneLogin
171
173
  # identify the subject in an SP rather than email or other less opaque attributes
172
174
  # NameQualifier, if present is prefixed with a "/" to the value
173
175
  else
174
- REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
175
- (n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + Utils.element_text(n)
176
- }
176
+ REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n|
177
+ base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : ''
178
+ "#{base_path}#{Utils.element_text(n)}"
179
+ end
177
180
  end
178
181
  }
179
182
 
@@ -221,14 +224,13 @@ module OneLogin
221
224
  "/p:Response/p:Status/p:StatusCode/p:StatusCode",
222
225
  { "p" => PROTOCOL }
223
226
  )
224
- statuses = nodes.collect do |node|
225
- node.attributes["Value"]
226
- end
227
- extra_code = statuses.join(" | ")
228
- if extra_code
229
- code = "#{code} | #{extra_code}"
227
+ statuses = nodes.collect do |inner_node|
228
+ inner_node.attributes["Value"]
230
229
  end
230
+
231
+ code = [code, statuses].flatten.join(" | ")
231
232
  end
233
+
232
234
  code
233
235
  end
234
236
  end
@@ -288,7 +290,6 @@ module OneLogin
288
290
  raise ValidationError.new(error_msg)
289
291
  end
290
292
 
291
- doc = decrypted_document.nil? ? document : decrypted_document
292
293
  issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
293
294
  unless issuer_assertion_nodes.size == 1
294
295
  error_msg = "Issuer of the Assertion not found or multiple."
@@ -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] || 0
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,18 @@ 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 audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
593
617
 
594
- unless audiences.include? settings.issuer
618
+ unless audiences.include? settings.sp_entity_id
595
619
  s = audiences.count > 1 ? 's' : '';
596
- error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.issuer}"
620
+ error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
597
621
  return append_error(error_msg)
598
622
  end
599
623
 
@@ -668,13 +692,13 @@ module OneLogin
668
692
 
669
693
  now = Time.now.utc
670
694
 
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})"
695
+ if not_before && now < (not_before - allowed_clock_drift)
696
+ error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
673
697
  return append_error(error_msg)
674
698
  end
675
699
 
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})"
700
+ if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
701
+ 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
702
  return append_error(error_msg)
679
703
  end
680
704
 
@@ -709,15 +733,15 @@ module OneLogin
709
733
  # this time validation is relaxed by the allowed_clock_drift value)
710
734
  # If fails, the error is added to the errors array
711
735
  # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
712
- # @return [Boolean] True if the SessionNotOnOrAfter of the AttributeStatement is valid, otherwise (when expired) False if soft=True
736
+ # @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True
713
737
  # @raise [ValidationError] if soft == false and validation fails
714
738
  #
715
739
  def validate_session_expiration(soft = true)
716
740
  return true if session_expires_at.nil?
717
741
 
718
742
  now = Time.now.utc
719
- unless (session_expires_at + allowed_clock_drift) > now
720
- error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response"
743
+ unless now < (session_expires_at + allowed_clock_drift)
744
+ error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
721
745
  return append_error(error_msg)
722
746
  end
723
747
 
@@ -754,8 +778,8 @@ module OneLogin
754
778
 
755
779
  attrs = confirmation_data_node.attributes
756
780
  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)) ||
781
+ (attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
782
+ (attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
759
783
  (attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
760
784
 
761
785
  valid_subject_confirmation = true
@@ -781,8 +805,8 @@ module OneLogin
781
805
  return append_error("An empty NameID value found")
782
806
  end
783
807
 
784
- unless settings.issuer.nil? || settings.issuer.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
785
- if name_id_spnamequalifier != settings.issuer
808
+ unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
809
+ if name_id_spnamequalifier != settings.sp_entity_id
786
810
  return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
787
811
  end
788
812
  end
@@ -802,7 +826,7 @@ module OneLogin
802
826
  # otherwise, review if the decrypted assertion contains a signature
803
827
  sig_elements = REXML::XPath.match(
804
828
  document,
805
- "/p:Response[@ID=$id]/ds:Signature]",
829
+ "/p:Response[@ID=$id]/ds:Signature",
806
830
  { "p" => PROTOCOL, "ds" => DSIG },
807
831
  { 'id' => document.signed_element_id }
808
832
  )
@@ -821,28 +845,59 @@ module OneLogin
821
845
  end
822
846
 
823
847
  if sig_elements.size != 1
848
+ if sig_elements.size == 0
849
+ append_error("Signed element id ##{doc.signed_element_id} is not found")
850
+ else
851
+ append_error("Signed element id ##{doc.signed_element_id} is found more than once")
852
+ end
824
853
  return append_error(error_msg)
825
854
  end
826
855
 
856
+ old_errors = @errors.clone
857
+
827
858
  idp_certs = settings.get_idp_cert_multi
828
859
  if idp_certs.nil? || idp_certs[:signing].empty?
829
860
  opts = {}
830
861
  opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
831
- opts[:cert] = settings.get_idp_cert
862
+ idp_cert = settings.get_idp_cert
832
863
  fingerprint = settings.get_fingerprint
864
+ opts[:cert] = idp_cert
833
865
 
834
- unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
866
+ if fingerprint && doc.validate_document(fingerprint, @soft, opts)
867
+ if settings.security[:check_idp_cert_expiration]
868
+ if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
869
+ error_msg = "IdP x509 certificate expired"
870
+ return append_error(error_msg)
871
+ end
872
+ end
873
+ else
835
874
  return append_error(error_msg)
836
875
  end
837
876
  else
838
877
  valid = false
878
+ expired = false
839
879
  idp_certs[:signing].each do |idp_cert|
840
- valid = doc.validate_document_with_cert(idp_cert)
880
+ valid = doc.validate_document_with_cert(idp_cert, true)
841
881
  if valid
882
+ if settings.security[:check_idp_cert_expiration]
883
+ if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
884
+ expired = true
885
+ end
886
+ end
887
+
888
+ # At least one certificate is valid, restore the old accumulated errors
889
+ @errors = old_errors
842
890
  break
843
891
  end
892
+
893
+ end
894
+ if expired
895
+ error_msg = "IdP x509 certificate expired"
896
+ return append_error(error_msg)
844
897
  end
845
898
  unless valid
899
+ # Remove duplicated errors
900
+ @errors = @errors.uniq
846
901
  return append_error(error_msg)
847
902
  end
848
903
  end
@@ -943,17 +998,6 @@ module OneLogin
943
998
  XMLSecurity::SignedDocument.new(response_node.to_s)
944
999
  end
945
1000
 
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
1001
  # Decrypts an EncryptedAssertion element
958
1002
  # @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
959
1003
  # @return [REXML::Document] The decrypted EncryptedAssertion element