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.
- checksums.yaml +7 -7
- data/Gemfile +11 -1
- data/README.md +5 -2
- data/Rakefile +0 -14
- data/lib/onelogin/ruby-saml/authrequest.rb +86 -20
- data/lib/onelogin/ruby-saml/logoutrequest.rb +95 -20
- data/lib/onelogin/ruby-saml/logoutresponse.rb +5 -28
- data/lib/onelogin/ruby-saml/metadata.rb +5 -5
- data/lib/onelogin/ruby-saml/response.rb +187 -4
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +146 -10
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
- data/lib/onelogin/ruby-saml/utils.rb +169 -0
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/ruby-saml.rb +2 -1
- data/lib/xml_security.rb +330 -78
- data/test/certificates/ruby-saml-2.crt +15 -0
- data/test/certificates/ruby-saml.crt +14 -0
- data/test/certificates/ruby-saml.key +15 -0
- data/test/logoutrequest_test.rb +177 -44
- data/test/logoutresponse_test.rb +25 -29
- data/test/request_test.rb +100 -37
- data/test/response_test.rb +213 -111
- data/test/responses/adfs_response_xmlns.xml +45 -0
- data/test/responses/encrypted_new_attack.xml.base64 +1 -0
- data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
- data/test/responses/invalids/no_signature.xml.base64 +1 -0
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
- data/test/responses/logoutresponse_fixtures.rb +6 -6
- data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/response_with_signed_assertion_3.xml +30 -0
- data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
- data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
- data/test/responses/response_wrapped.xml.base64 +150 -0
- data/test/responses/valid_response.xml.base64 +1 -0
- data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
- data/test/settings_test.rb +7 -7
- data/test/slo_logoutresponse_test.rb +226 -0
- data/test/test_helper.rb +117 -12
- data/test/utils_test.rb +10 -10
- data/test/xml_security_test.rb +310 -68
- 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.
|
26
|
-
root.attributes["entityID"] = settings.
|
25
|
+
if settings.sp_entity_id != nil
|
26
|
+
root.attributes["entityID"] = settings.sp_entity_id
|
27
27
|
end
|
28
|
-
if settings.
|
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.
|
33
|
-
"ResponseLocation" => settings.
|
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 =
|
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
|
-
|
143
|
-
|
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
|
@@ -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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
12
|
-
|
13
|
-
attr_accessor :
|
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 = {
|
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
|