ruby-saml 0.8.8 → 0.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ruby-saml might be problematic. Click here for more details.

Files changed (45) hide show
  1. checksums.yaml +7 -7
  2. data/Gemfile +11 -1
  3. data/README.md +5 -2
  4. data/Rakefile +0 -14
  5. data/lib/onelogin/ruby-saml/authrequest.rb +86 -20
  6. data/lib/onelogin/ruby-saml/logoutrequest.rb +95 -20
  7. data/lib/onelogin/ruby-saml/logoutresponse.rb +5 -28
  8. data/lib/onelogin/ruby-saml/metadata.rb +5 -5
  9. data/lib/onelogin/ruby-saml/response.rb +187 -4
  10. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  11. data/lib/onelogin/ruby-saml/settings.rb +146 -10
  12. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
  13. data/lib/onelogin/ruby-saml/utils.rb +169 -0
  14. data/lib/onelogin/ruby-saml/version.rb +1 -1
  15. data/lib/ruby-saml.rb +2 -1
  16. data/lib/xml_security.rb +330 -78
  17. data/test/certificates/ruby-saml-2.crt +15 -0
  18. data/test/certificates/ruby-saml.crt +14 -0
  19. data/test/certificates/ruby-saml.key +15 -0
  20. data/test/logoutrequest_test.rb +177 -44
  21. data/test/logoutresponse_test.rb +25 -29
  22. data/test/request_test.rb +100 -37
  23. data/test/response_test.rb +213 -111
  24. data/test/responses/adfs_response_xmlns.xml +45 -0
  25. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  26. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  27. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  28. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  29. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  30. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  31. data/test/responses/logoutresponse_fixtures.rb +6 -6
  32. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  33. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  34. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  35. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  36. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  37. data/test/responses/response_wrapped.xml.base64 +150 -0
  38. data/test/responses/valid_response.xml.base64 +1 -0
  39. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  40. data/test/settings_test.rb +7 -7
  41. data/test/slo_logoutresponse_test.rb +226 -0
  42. data/test/test_helper.rb +117 -12
  43. data/test/utils_test.rb +10 -10
  44. data/test/xml_security_test.rb +310 -68
  45. metadata +88 -45
@@ -22,15 +22,15 @@ module OneLogin
22
22
  # However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
23
23
  "WantAssertionsSigned" => (!settings.idp_cert_fingerprint.nil? || !settings.idp_cert.nil?)
24
24
  }
25
- if settings.issuer != nil
26
- root.attributes["entityID"] = settings.issuer
25
+ if settings.sp_entity_id != nil
26
+ root.attributes["entityID"] = settings.sp_entity_id
27
27
  end
28
- if settings.assertion_consumer_logout_service_url != nil
28
+ if settings.single_logout_service_url != nil
29
29
  sp_sso.add_element "md:SingleLogoutService", {
30
30
  # Add this as a setting to create different bindings?
31
31
  "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
32
- "Location" => settings.assertion_consumer_logout_service_url,
33
- "ResponseLocation" => settings.assertion_consumer_logout_service_url,
32
+ "Location" => settings.single_logout_service_url,
33
+ "ResponseLocation" => settings.single_logout_service_url,
34
34
  "isDefault" => true,
35
35
  "index" => 0
36
36
  }
@@ -1,6 +1,7 @@
1
1
  require "xml_security"
2
2
  require "time"
3
3
  require "nokogiri"
4
+ require "onelogin/ruby-saml/utils"
4
5
  require 'onelogin/ruby-saml/attributes'
5
6
 
6
7
  # Only supports SAML 2.0
@@ -22,7 +23,7 @@ module OneLogin
22
23
  def initialize(response, options = {})
23
24
  raise ArgumentError.new("Response cannot be nil") if response.nil?
24
25
  @options = options
25
- @response = (response =~ /^</) ? response : Base64.decode64(response)
26
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
26
27
  @document = XMLSecurity::SignedDocument.new(@response)
27
28
  end
28
29
 
@@ -42,6 +43,8 @@ module OneLogin
42
43
  end
43
44
  end
44
45
 
46
+ alias nameid name_id
47
+
45
48
  def sessionindex
46
49
  @sessionindex ||= begin
47
50
  node = xpath_first_from_signed_assertion('/a:AuthnStatement')
@@ -131,6 +134,15 @@ module OneLogin
131
134
  end
132
135
  end
133
136
 
137
+ # @return [Array] The Audience elements from the Contitions of the SAML Response.
138
+ #
139
+ def audiences
140
+ @audiences ||= begin
141
+ nodes = xpath_from_signed_assertion('/a:Conditions/a:AudienceRestriction/a:Audience')
142
+ nodes.map { |node| Utils.element_text(node) }.reject(&:empty?)
143
+ end
144
+ end
145
+
134
146
  private
135
147
 
136
148
  def validation_error(message)
@@ -138,13 +150,166 @@ module OneLogin
138
150
  end
139
151
 
140
152
  def validate(soft = true)
141
- validate_structure(soft) &&
142
- validate_response_state(soft) &&
143
- validate_conditions(soft) &&
153
+ validate_structure(soft) &&
154
+ validate_success_status(soft) &&
155
+ validate_num_assertion &&
156
+ validate_signed_elements(soft) &&
157
+ validate_response_state(soft) &&
158
+ validate_conditions(soft) &&
159
+ validate_audience(soft) &&
144
160
  document.validate_document(get_fingerprint, soft) &&
145
161
  success?
146
162
  end
147
163
 
164
+ # Validates that the SAML Response only contains a single Assertion (encrypted or not).
165
+ # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
166
+ #
167
+ def validate_num_assertion(soft = true)
168
+ assertions = REXML::XPath.match(
169
+ document,
170
+ "//a:Assertion",
171
+ { "a" => ASSERTION }
172
+ )
173
+ encrypted_assertions = REXML::XPath.match(
174
+ document,
175
+ "//a:EncryptedAssertion",
176
+ { "a" => ASSERTION }
177
+ )
178
+
179
+ unless assertions.size + encrypted_assertions.size == 1
180
+ return soft ? false : validation_error("SAML Response must contain 1 assertion")
181
+ end
182
+
183
+ true
184
+ end
185
+
186
+ # Validates the Signed elements
187
+ # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
188
+ # an are a Response or an Assertion Element, otherwise False if soft=True
189
+ #
190
+ def validate_signed_elements(soft)
191
+ signature_nodes = REXML::XPath.match(
192
+ document,
193
+ "//ds:Signature",
194
+ {"ds"=>DSIG}
195
+ )
196
+ signed_elements = []
197
+ verified_seis = []
198
+ verified_ids = []
199
+ signature_nodes.each do |signature_node|
200
+ signed_element = signature_node.parent.name
201
+ if signed_element != 'Response' && signed_element != 'Assertion'
202
+ return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
203
+ end
204
+
205
+ if signature_node.parent.attributes['ID'].nil?
206
+ return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
207
+ end
208
+
209
+ id = signature_node.parent.attributes.get_attribute("ID").value
210
+ if verified_ids.include?(id)
211
+ return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
212
+ end
213
+ verified_ids.push(id)
214
+
215
+ # Check that reference URI matches the parent ID and no duplicate References or IDs
216
+ ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
217
+ if ref
218
+ uri = ref.attributes.get_attribute("URI")
219
+ if uri && !uri.value.empty?
220
+ sei = uri.value[1..-1]
221
+
222
+ unless sei == id
223
+ return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
224
+ end
225
+
226
+ if verified_seis.include?(sei)
227
+ return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
228
+ return append_error("Duplicated Reference URI. SAML Response rejected")
229
+ end
230
+
231
+ verified_seis.push(sei)
232
+ end
233
+ end
234
+
235
+ signed_elements << signed_element
236
+ end
237
+
238
+ unless signature_nodes.length < 3 && !signed_elements.empty?
239
+ return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
240
+ end
241
+
242
+ true
243
+ end
244
+
245
+ # Validates the Status of the SAML Response
246
+ # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
247
+ # @raise [ValidationError] if soft == false and validation fails
248
+ #
249
+ def validate_success_status(soft = true)
250
+ return true if success?
251
+
252
+ return false unless soft
253
+
254
+ error_msg = 'The status code of the Response was not Success'
255
+ status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
256
+ return validation_error(status_error_msg)
257
+ end
258
+
259
+ # Checks if the Status has the "Success" code
260
+ # @return [Boolean] True if the StatusCode is Sucess
261
+ #
262
+ def success?
263
+ status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
264
+ end
265
+
266
+ # @return [String] StatusCode value from a SAML Response.
267
+ #
268
+ def status_code
269
+ @status_code ||= begin
270
+ nodes = REXML::XPath.match(
271
+ document,
272
+ "/p:Response/p:Status/p:StatusCode",
273
+ { "p" => PROTOCOL }
274
+ )
275
+ if nodes.size == 1
276
+ node = nodes[0]
277
+ code = node.attributes["Value"] if node && node.attributes
278
+
279
+ unless code == "urn:oasis:names:tc:SAML:2.0:status:Success"
280
+ nodes = REXML::XPath.match(
281
+ document,
282
+ "/p:Response/p:Status/p:StatusCode/p:StatusCode",
283
+ { "p" => PROTOCOL }
284
+ )
285
+ statuses = nodes.collect do |inner_node|
286
+ inner_node.attributes["Value"]
287
+ end
288
+ extra_code = statuses.join(" | ")
289
+ if extra_code
290
+ code = "#{code} | #{extra_code}"
291
+ end
292
+ end
293
+ code
294
+ end
295
+ end
296
+ end
297
+
298
+ # @return [String] the StatusMessage value from a SAML Response.
299
+ #
300
+ def status_message
301
+ @status_message ||= begin
302
+ nodes = REXML::XPath.match(
303
+ document,
304
+ "/p:Response/p:Status/p:StatusMessage",
305
+ { "p" => PROTOCOL }
306
+ )
307
+ if nodes.size == 1
308
+ Utils.element_text(nodes.first)
309
+ end
310
+ end
311
+ end
312
+
148
313
  def validate_structure(soft = true)
149
314
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
150
315
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -240,6 +405,24 @@ module OneLogin
240
405
  Time.parse(node.attributes[attribute])
241
406
  end
242
407
  end
408
+
409
+ # Validates the Audience, (If the Audience match the Service Provider EntityID)
410
+ # If fails, the error is added to the errors array
411
+ # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
412
+ # @raise [ValidationError] if soft == false and validation fails
413
+ #
414
+ def validate_audience(soft = true)
415
+ return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
416
+
417
+ unless audiences.include? settings.sp_entity_id
418
+ s = audiences.count > 1 ? 's' : '';
419
+ error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
420
+ return soft ? false : validation_error(error_msg)
421
+ end
422
+
423
+ true
424
+ end
425
+
243
426
  end
244
427
  end
245
428
  end
@@ -0,0 +1,6 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class SettingError < StandardError
4
+ end
5
+ end
6
+ end
@@ -1,30 +1,166 @@
1
+ require "xml_security"
2
+ require "onelogin/ruby-saml/utils"
3
+
1
4
  module OneLogin
2
5
  module RubySaml
3
6
  class Settings
4
- def initialize(overrides = {})
5
- config = DEFAULTS.merge(overrides)
6
- config.each do |k,v|
7
- acc = "#{k.to_s}=".to_sym
8
- self.send(acc, v) if self.respond_to? acc
9
- end
7
+ def initialize(overrides = {}, keep_security_attributes = false)
8
+ if keep_security_attributes
9
+ security_attributes = overrides.delete(:security) || {}
10
+ config = DEFAULTS.merge(overrides)
11
+ config[:security] = DEFAULTS[:security].merge(security_attributes)
12
+ else
13
+ config = DEFAULTS.merge(overrides)
14
+ end
15
+
16
+ config.each do |k,v|
17
+ acc = "#{k.to_s}=".to_sym
18
+ if respond_to? acc
19
+ value = v.is_a?(Hash) ? v.dup : v
20
+ send(acc, value)
21
+ end
22
+ end
10
23
  end
11
- attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier
12
- attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format
13
- attr_accessor :authn_context
24
+
25
+ #idp data
26
+ attr_accessor :idp_sso_target_url
27
+ attr_accessor :idp_cert_fingerprint
28
+ attr_accessor :idp_cert
14
29
  attr_accessor :idp_slo_target_url
30
+ #sp data
31
+ attr_accessor :sp_entity_id
32
+ attr_accessor :assertion_consumer_service_url
33
+ attr_accessor :authn_context
34
+ attr_accessor :sp_name_qualifier
35
+ attr_accessor :name_identifier_format
15
36
  attr_accessor :name_identifier_value
16
37
  attr_accessor :name_identifier_value_requested
17
38
  attr_accessor :sessionindex
18
39
  attr_accessor :assertion_consumer_logout_service_url
19
40
  attr_accessor :compress_request
41
+ attr_accessor :compress_response
20
42
  attr_accessor :double_quote_xml_attribute_values
21
43
  attr_accessor :force_authn
22
44
  attr_accessor :passive
23
45
  attr_accessor :protocol_binding
46
+ attr_accessor :certificate
47
+ attr_accessor :private_key
48
+ # Work-flow
49
+ attr_accessor :security
50
+ # Compability
51
+ attr_accessor :issuer
52
+ attr_accessor :assertion_consumer_logout_service_url
53
+ attr_accessor :assertion_consumer_logout_service_binding
54
+
55
+ # @return [String] SP Entity ID
56
+ #
57
+ def sp_entity_id
58
+ val = nil
59
+ if @sp_entity_id.nil?
60
+ if @issuer
61
+ val = @issuer
62
+ end
63
+ else
64
+ val = @sp_entity_id
65
+ end
66
+ val
67
+ end
68
+
69
+ # Setter for SP Entity ID.
70
+ # @param val [String].
71
+ #
72
+ def sp_entity_id=(val)
73
+ @sp_entity_id = val
74
+ end
75
+
76
+ # @return [String] Single Logout Service URL.
77
+ #
78
+ def single_logout_service_url
79
+ val = nil
80
+ if @single_logout_service_url.nil?
81
+ if @assertion_consumer_logout_service_url
82
+ val = @assertion_consumer_logout_service_url
83
+ end
84
+ else
85
+ val = @single_logout_service_url
86
+ end
87
+ val
88
+ end
89
+
90
+ # Setter for the Single Logout Service URL.
91
+ # @param url [String].
92
+ #
93
+ def single_logout_service_url=(url)
94
+ @single_logout_service_url = url
95
+ end
96
+
97
+ # @return [String] Single Logout Service Binding.
98
+ #
99
+ def single_logout_service_binding
100
+ val = nil
101
+ if @single_logout_service_binding.nil?
102
+ if @assertion_consumer_logout_service_binding
103
+ val = @assertion_consumer_logout_service_binding
104
+ end
105
+ else
106
+ val = @single_logout_service_binding
107
+ end
108
+ val
109
+ end
110
+
111
+ # Setter for Single Logout Service Binding.
112
+ #
113
+ # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
114
+ # @param url [String]
115
+ #
116
+ def single_logout_service_binding=(url)
117
+ @single_logout_service_binding = url
118
+ end
119
+
120
+ # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
121
+ #
122
+ def get_sp_cert
123
+ return nil if certificate.nil? || certificate.empty?
124
+
125
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
126
+ OpenSSL::X509::Certificate.new(formatted_cert)
127
+ end
128
+
129
+ # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
130
+ #
131
+ def get_sp_cert_new
132
+ return nil if certificate_new.nil? || certificate_new.empty?
133
+
134
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
135
+ OpenSSL::X509::Certificate.new(formatted_cert)
136
+ end
137
+
138
+ # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
139
+ #
140
+ def get_sp_key
141
+ return nil if private_key.nil? || private_key.empty?
142
+
143
+ formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
144
+ OpenSSL::PKey::RSA.new(formatted_private_key)
145
+ end
24
146
 
25
147
  private
26
148
 
27
- DEFAULTS = {:compress_request => true, :double_quote_xml_attribute_values => false}
149
+ DEFAULTS = {
150
+ :compress_request => true,
151
+ :compress_response => true,
152
+ :double_quote_xml_attribute_values => false,
153
+ :assertion_consumer_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
154
+ :single_logout_service_binding => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze,
155
+ :security => {
156
+ :authn_requests_signed => false,
157
+ :logout_requests_signed => false,
158
+ :logout_responses_signed => false,
159
+ :embed_sign => false,
160
+ :digest_method => XMLSecurity::Document::SHA1,
161
+ :signature_method => XMLSecurity::Document::RSA_SHA1
162
+ }.freeze
163
+ }.freeze
28
164
  end
29
165
  end
30
166
  end
@@ -0,0 +1,158 @@
1
+ require "base64"
2
+ require "zlib"
3
+ require "cgi"
4
+ require "onelogin/ruby-saml/utils"
5
+ require "onelogin/ruby-saml/setting_error"
6
+
7
+ module OneLogin
8
+ module RubySaml
9
+
10
+ # SAML2 Logout Response (SLO SP initiated)
11
+ #
12
+ class SloLogoutresponse
13
+
14
+ # Logout Response ID
15
+ attr_reader :uuid
16
+
17
+ # Initializes the Logout Response. A SloLogoutresponse Object.
18
+ # Asigns an ID, a random uuid.
19
+ #
20
+ def initialize
21
+ @uuid = OneLogin::RubySaml::Utils.uuid
22
+ end
23
+
24
+ # Creates the Logout Response string.
25
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
26
+ # @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
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
28
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
29
+ # @return [String] Logout Request string that includes the SAMLRequest
30
+ #
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 =~ /\?/) ? '&' : '?'
34
+ saml_response = CGI.escape(params.delete("SAMLResponse"))
35
+ response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
36
+ params.each_pair do |key, value|
37
+ response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
38
+ end
39
+ raise SettingError.new "Invalid settings, idp_slo_target_url is not set!" if settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
40
+ @logout_url = settings.idp_slo_target_url + response_params
41
+ end
42
+
43
+ # Creates the Get parameters for the logout response.
44
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
45
+ # @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
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
47
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
48
+ # @return [Hash] Parameters
49
+ #
50
+ def create_params(settings, request_id = nil, logout_message = nil, params = {})
51
+ # The method expects :RelayState but sometimes we get 'RelayState' instead.
52
+ # Based on the HashWithIndifferentAccess value in Rails we could experience
53
+ # conflicts so this line will solve them.
54
+ relay_state = params[:RelayState] || params['RelayState']
55
+
56
+ if relay_state.nil?
57
+ params.delete(:RelayState)
58
+ params.delete('RelayState')
59
+ end
60
+
61
+ response_doc = create_logout_response_xml_doc(settings, request_id, logout_message)
62
+ response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
63
+
64
+ response = ""
65
+ response_doc.write(response)
66
+
67
+ Logging.debug "Created SLO Logout Response: #{response}"
68
+
69
+ response = Zlib::Deflate.deflate(response, 9)[2..-5] if settings.compress_response
70
+ if Base64.respond_to?('strict_encode64')
71
+ base64_response = Base64.strict_encode64(response)
72
+ else
73
+ base64_response = Base64.encode64(response).gsub(/\n/, "")
74
+ end
75
+ response_params = {"SAMLResponse" => base64_response}
76
+
77
+ if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
78
+ params['SigAlg'] = settings.security[:signature_method]
79
+ url_string = OneLogin::RubySaml::Utils.build_query(
80
+ :type => 'SAMLResponse',
81
+ :data => base64_response,
82
+ :relay_state => relay_state,
83
+ :sig_alg => params['SigAlg']
84
+ )
85
+ sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
86
+ signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
87
+ if Base64.respond_to?('strict_encode64')
88
+ params['Signature'] = Base64.strict_encode64(signature)
89
+ else
90
+ params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
91
+ end
92
+ end
93
+
94
+ params.each_pair do |key, value|
95
+ response_params[key] = value.to_s
96
+ end
97
+
98
+ response_params
99
+ end
100
+
101
+ # Creates the SAMLResponse String.
102
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
103
+ # @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
104
+ # @param logout_message [String] The Message to be placed as StatusMessage in the logout response
105
+ # @return [String] The SAMLResponse String.
106
+ #
107
+ def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
108
+ document = create_xml_document(settings, request_id, logout_message)
109
+ sign_document(document, settings)
110
+ end
111
+
112
+ def create_xml_document(settings, request_id = nil, logout_message = nil)
113
+ time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
114
+
115
+ response_doc = XMLSecurity::Document.new
116
+ response_doc.uuid = uuid
117
+
118
+ 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" }
119
+ root.attributes['ID'] = uuid
120
+ root.attributes['IssueInstant'] = time
121
+ root.attributes['Version'] = '2.0'
122
+ root.attributes['InResponseTo'] = request_id unless request_id.nil?
123
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
124
+
125
+ if settings.sp_entity_id != nil
126
+ issuer = root.add_element "saml:Issuer"
127
+ issuer.text = settings.sp_entity_id
128
+ end
129
+
130
+ # add success message
131
+ status = root.add_element 'samlp:Status'
132
+
133
+ # success status code
134
+ status_code = status.add_element 'samlp:StatusCode'
135
+ status_code.attributes['Value'] = 'urn:oasis:names:tc:SAML:2.0:status:Success'
136
+
137
+ # success status message
138
+ logout_message ||= 'Successfully Signed Out'
139
+ status_message = status.add_element 'samlp:StatusMessage'
140
+ status_message.text = logout_message
141
+
142
+ response_doc
143
+ end
144
+
145
+ def sign_document(document, settings)
146
+ # embed signature
147
+ if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
148
+ private_key = settings.get_sp_key
149
+ cert = settings.get_sp_cert
150
+ document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
151
+ end
152
+
153
+ document
154
+ end
155
+
156
+ end
157
+ end
158
+ end