ruby-saml 0.8.8 → 0.8.13

Sign up to get free protection for your applications and to get access to all the features.

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