ruby-saml 0.8.12

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +37 -0
  6. data/LICENSE +19 -0
  7. data/README.md +160 -0
  8. data/Rakefile +27 -0
  9. data/changelog.md +24 -0
  10. data/lib/onelogin/ruby-saml/attributes.rb +147 -0
  11. data/lib/onelogin/ruby-saml/authrequest.rb +168 -0
  12. data/lib/onelogin/ruby-saml/logging.rb +26 -0
  13. data/lib/onelogin/ruby-saml/logoutrequest.rb +161 -0
  14. data/lib/onelogin/ruby-saml/logoutresponse.rb +153 -0
  15. data/lib/onelogin/ruby-saml/metadata.rb +66 -0
  16. data/lib/onelogin/ruby-saml/response.rb +426 -0
  17. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  18. data/lib/onelogin/ruby-saml/settings.rb +166 -0
  19. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
  20. data/lib/onelogin/ruby-saml/utils.rb +119 -0
  21. data/lib/onelogin/ruby-saml/validation_error.rb +7 -0
  22. data/lib/onelogin/ruby-saml/version.rb +5 -0
  23. data/lib/ruby-saml.rb +12 -0
  24. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  25. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  26. data/lib/schemas/xenc_schema.xsd +146 -0
  27. data/lib/schemas/xmldsig_schema.xsd +318 -0
  28. data/lib/xml_security.rb +292 -0
  29. data/ruby-saml.gemspec +28 -0
  30. data/test/certificates/certificate1 +12 -0
  31. data/test/certificates/r1_certificate2_base64 +1 -0
  32. data/test/certificates/ruby-saml.crt +14 -0
  33. data/test/certificates/ruby-saml.key +15 -0
  34. data/test/logoutrequest_test.rb +244 -0
  35. data/test/logoutresponse_test.rb +112 -0
  36. data/test/request_test.rb +229 -0
  37. data/test/response_test.rb +475 -0
  38. data/test/responses/adfs_response_sha1.xml +46 -0
  39. data/test/responses/adfs_response_sha256.xml +46 -0
  40. data/test/responses/adfs_response_sha384.xml +46 -0
  41. data/test/responses/adfs_response_sha512.xml +46 -0
  42. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  43. data/test/responses/logoutresponse_fixtures.rb +67 -0
  44. data/test/responses/no_signature_ns.xml +48 -0
  45. data/test/responses/open_saml_response.xml +56 -0
  46. data/test/responses/r1_response6.xml.base64 +1 -0
  47. data/test/responses/response1.xml.base64 +1 -0
  48. data/test/responses/response2.xml.base64 +79 -0
  49. data/test/responses/response3.xml.base64 +66 -0
  50. data/test/responses/response4.xml.base64 +93 -0
  51. data/test/responses/response5.xml.base64 +102 -0
  52. data/test/responses/response_eval.xml +7 -0
  53. data/test/responses/response_node_text_attack.xml.base64 +1 -0
  54. data/test/responses/response_with_ampersands.xml +139 -0
  55. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  56. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  57. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  58. data/test/responses/response_with_multiple_attribute_statements.xml +72 -0
  59. data/test/responses/response_with_multiple_attribute_values.xml +67 -0
  60. data/test/responses/response_wrapped.xml.base64 +150 -0
  61. data/test/responses/simple_saml_php.xml +71 -0
  62. data/test/responses/starfield_response.xml.base64 +1 -0
  63. data/test/responses/valid_response.xml.base64 +1 -0
  64. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  65. data/test/settings_test.rb +47 -0
  66. data/test/slo_logoutresponse_test.rb +226 -0
  67. data/test/test_helper.rb +155 -0
  68. data/test/utils_test.rb +41 -0
  69. data/test/xml_security_test.rb +158 -0
  70. metadata +178 -0
@@ -0,0 +1,292 @@
1
+ # The contents of this file are subject to the terms
2
+ # of the Common Development and Distribution License
3
+ # (the License). You may not use this file except in
4
+ # compliance with the License.
5
+ #
6
+ # You can obtain a copy of the License at
7
+ # https://opensso.dev.java.net/public/CDDLv1.0.html or
8
+ # opensso/legal/CDDLv1.0.txt
9
+ # See the License for the specific language governing
10
+ # permission and limitations under the License.
11
+ #
12
+ # When distributing Covered Code, include this CDDL
13
+ # Header Notice in each file and include the License file
14
+ # at opensso/legal/CDDLv1.0.txt.
15
+ # If applicable, add the following below the CDDL Header,
16
+ # with the fields enclosed by brackets [] replaced by
17
+ # your own identifying information:
18
+ # "Portions Copyrighted [year] [name of copyright owner]"
19
+ #
20
+ # $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
21
+ #
22
+ # Copyright 2007 Sun Microsystems Inc. All Rights Reserved
23
+ # Portions Copyrighted 2007 Todd W Saxton.
24
+
25
+ require 'rubygems'
26
+ require "rexml/document"
27
+ require "rexml/xpath"
28
+ require "openssl"
29
+ require 'nokogiri'
30
+ require "digest/sha1"
31
+ require "digest/sha2"
32
+ require "onelogin/ruby-saml/validation_error"
33
+ require "onelogin/ruby-saml/utils"
34
+
35
+ module XMLSecurity
36
+
37
+ class BaseDocument < REXML::Document
38
+ REXML::Document::entity_expansion_limit = 0
39
+
40
+ C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
41
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
42
+ NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT |
43
+ Nokogiri::XML::ParseOptions::NONET
44
+
45
+ def canon_algorithm(element)
46
+ algorithm = element
47
+ if algorithm.is_a?(REXML::Element)
48
+ algorithm = element.attribute('Algorithm').value
49
+ end
50
+
51
+ case algorithm
52
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
53
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
54
+ Nokogiri::XML::XML_C14N_1_0
55
+ when "http://www.w3.org/2006/12/xml-c14n11",
56
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
57
+ Nokogiri::XML::XML_C14N_1_1
58
+ else
59
+ Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
60
+ end
61
+ end
62
+
63
+ def algorithm(element)
64
+ algorithm = element
65
+ if algorithm.is_a?(REXML::Element)
66
+ algorithm = element.attribute("Algorithm").value
67
+ end
68
+
69
+ algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
70
+
71
+ case algorithm
72
+ when 256 then OpenSSL::Digest::SHA256
73
+ when 384 then OpenSSL::Digest::SHA384
74
+ when 512 then OpenSSL::Digest::SHA512
75
+ else
76
+ OpenSSL::Digest::SHA1
77
+ end
78
+ end
79
+ end
80
+
81
+ class Document < BaseDocument
82
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
83
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
84
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
85
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
86
+ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
87
+ SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
88
+ SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
89
+ SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
90
+ ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
91
+ INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
92
+
93
+ attr_writer :uuid
94
+
95
+ def uuid
96
+ @uuid ||= begin
97
+ document.root.nil? ? nil : document.root.attributes['ID']
98
+ end
99
+ end
100
+
101
+ def sign_document(private_key, certificate, signature_method = RSA_SHA1, digest_method = SHA1)
102
+ noko = Nokogiri::XML(self.to_s) do |config|
103
+ config.options = NOKOGIRI_OPTIONS
104
+ end
105
+
106
+ signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
107
+ signed_info_element = signature_element.add_element("ds:SignedInfo")
108
+ signed_info_element.add_element("ds:CanonicalizationMethod", {"Algorithm" => C14N})
109
+ signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>signature_method})
110
+
111
+ # Add Reference
112
+ reference_element = signed_info_element.add_element("ds:Reference", {"URI" => "##{uuid}"})
113
+
114
+ # Add Transforms
115
+ transforms_element = reference_element.add_element("ds:Transforms")
116
+ transforms_element.add_element("ds:Transform", {"Algorithm" => ENVELOPED_SIG})
117
+ c14element = transforms_element.add_element("ds:Transform", {"Algorithm" => C14N})
118
+ c14element.add_element("ec:InclusiveNamespaces", {"xmlns:ec" => C14N, "PrefixList" => INC_PREFIX_LIST})
119
+
120
+ digest_method_element = reference_element.add_element("ds:DigestMethod", {"Algorithm" => digest_method})
121
+ inclusive_namespaces = INC_PREFIX_LIST.split(" ")
122
+ canon_doc = noko.canonicalize(canon_algorithm(C14N), inclusive_namespaces)
123
+ reference_element.add_element("ds:DigestValue").text = compute_digest(canon_doc, algorithm(digest_method_element))
124
+
125
+ # add SignatureValue
126
+ noko_sig_element = Nokogiri::XML(signature_element.to_s) do |config|
127
+ config.options = NOKOGIRI_OPTIONS
128
+ end
129
+
130
+ noko_signed_info_element = noko_sig_element.at_xpath('//ds:Signature/ds:SignedInfo', 'ds' => DSIG)
131
+ canon_string = noko_signed_info_element.canonicalize(canon_algorithm(C14N))
132
+
133
+ signature = compute_signature(private_key, algorithm(signature_method).new, canon_string)
134
+ signature_element.add_element("ds:SignatureValue").text = signature
135
+
136
+ # add KeyInfo
137
+ key_info_element = signature_element.add_element("ds:KeyInfo")
138
+ x509_element = key_info_element.add_element("ds:X509Data")
139
+ x509_cert_element = x509_element.add_element("ds:X509Certificate")
140
+ if certificate.is_a?(String)
141
+ certificate = OpenSSL::X509::Certificate.new(certificate)
142
+ end
143
+ x509_cert_element.text = Base64.encode64(certificate.to_der).gsub(/\n/, "")
144
+
145
+ # add the signature
146
+ issuer_element = self.elements["//saml:Issuer"]
147
+ if issuer_element
148
+ self.root.insert_after issuer_element, signature_element
149
+ else
150
+ if sp_sso_descriptor = self.elements["/md:EntityDescriptor"]
151
+ self.root.insert_before sp_sso_descriptor, signature_element
152
+ else
153
+ self.root.add_element(signature_element)
154
+ end
155
+ end
156
+ end
157
+
158
+ protected
159
+
160
+ def compute_signature(private_key, signature_algorithm, document)
161
+ Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "")
162
+ end
163
+
164
+ def compute_digest(document, digest_algorithm)
165
+ digest = digest_algorithm.digest(document)
166
+ Base64.encode64(digest).strip!
167
+ end
168
+
169
+ end
170
+
171
+ class SignedDocument < BaseDocument
172
+
173
+ attr_writer :signed_element_id
174
+
175
+ def initialize(response)
176
+ super(response)
177
+ extract_signed_element_id
178
+ end
179
+
180
+ def signed_element_id
181
+ @signed_element_id ||= extract_signed_element_id
182
+ end
183
+
184
+ def validate_document(idp_cert_fingerprint, soft = true)
185
+ # get cert from response
186
+ cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
187
+ raise OneLogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
188
+ base64_cert = OneLogin::RubySaml::Utils.element_text(cert_element)
189
+ cert_text = Base64.decode64(base64_cert)
190
+ cert = OpenSSL::X509::Certificate.new(cert_text)
191
+
192
+ # check cert matches registered idp cert
193
+ fingerprint = Digest::SHA1.hexdigest(cert.to_der)
194
+
195
+ if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
196
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
197
+ end
198
+
199
+ validate_signature(base64_cert, soft)
200
+ end
201
+
202
+ def validate_signature(base64_cert, soft = true)
203
+ # validate references
204
+
205
+ # check for inclusive namespaces
206
+ inclusive_namespaces = extract_inclusive_namespaces
207
+
208
+ document = Nokogiri::XML(self.to_s) do |config|
209
+ config.options = NOKOGIRI_OPTIONS
210
+ end
211
+
212
+ # create a working copy so we don't modify the original
213
+ @working_copy ||= REXML::Document.new(self.to_s).root
214
+
215
+ # store signature node
216
+ @sig_element ||= begin
217
+ element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
218
+ end
219
+
220
+ # verify signature
221
+ signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
222
+ noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
223
+ noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
224
+ canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
225
+ canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
226
+ noko_sig_element.remove
227
+
228
+ # check digests
229
+ REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
230
+ uri = ref.attributes.get_attribute("URI").value
231
+
232
+ hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
233
+ canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
234
+ canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
235
+
236
+ digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", 'ds' => DSIG))
237
+
238
+ hash = digest_algorithm.digest(canon_hashed_element)
239
+ digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG})))
240
+
241
+ unless digests_match?(hash, digest_value)
242
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Digest mismatch"))
243
+ end
244
+ end
245
+
246
+ base64_signature = OneLogin::RubySaml::Utils.element_text(REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}))
247
+ signature = Base64.decode64(base64_signature)
248
+
249
+ # get certificate object
250
+ cert_text = Base64.decode64(base64_cert)
251
+ cert = OpenSSL::X509::Certificate.new(cert_text)
252
+
253
+ # signature method
254
+ signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
255
+
256
+ unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
257
+ return soft ? false : (raise OneLogin::RubySaml::ValidationError.new("Key validation error"))
258
+ end
259
+
260
+ return true
261
+ end
262
+
263
+ private
264
+
265
+ def digests_match?(hash, digest_value)
266
+ hash == digest_value
267
+ end
268
+
269
+ def extract_signed_element_id
270
+ reference_element = REXML::XPath.first(
271
+ self,
272
+ "//ds:Signature/ds:SignedInfo/ds:Reference",
273
+ {"ds"=>DSIG}
274
+ )
275
+
276
+ return nil if reference_element.nil?
277
+
278
+ sei = reference_element.attribute("URI").value[1..-1]
279
+ sei.nil? ? reference_element.parent.parent.parent.attribute("ID").value : sei
280
+ end
281
+
282
+ def extract_inclusive_namespaces
283
+ if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
284
+ prefix_list = element.attributes.get_attribute("PrefixList").value
285
+ prefix_list.split(" ")
286
+ else
287
+ []
288
+ end
289
+ end
290
+
291
+ end
292
+ end
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'onelogin/ruby-saml/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'ruby-saml'
6
+ s.version = OneLogin::RubySaml::VERSION
7
+
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.authors = ["OneLogin LLC"]
10
+ s.date = Time.now.strftime("%Y-%m-%d")
11
+ s.description = %q{SAML toolkit for Ruby on Rails}
12
+ s.email = %q{support@onelogin.com}
13
+ s.extra_rdoc_files = [
14
+ "LICENSE",
15
+ "README.md"
16
+ ]
17
+ s.files = `git ls-files`.split("\n")
18
+ s.homepage = %q{http://github.com/onelogin/ruby-saml}
19
+ s.rubyforge_project = %q{http://www.rubygems.org/gems/ruby-saml}
20
+ s.rdoc_options = ["--charset=UTF-8"]
21
+ s.require_paths = ["lib"]
22
+ s.rubygems_version = %q{1.3.7}
23
+ s.summary = %q{SAML Ruby Tookit}
24
+ s.test_files = `git ls-files test/*`.split("\n")
25
+
26
+ s.add_runtime_dependency("uuid", ["~> 2.3"])
27
+ s.add_runtime_dependency("nokogiri", [">= 1.5.0"])
28
+ end
@@ -0,0 +1,12 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIBrTCCAaGgAwIBAgIBATADBgEAMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD
3
+ YWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxv
4
+ Z2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMB4XDTEwMTAxMTIxMTUxMloX
5
+ DTE1MTAxMTIxMTUxMlowZzELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3Ju
6
+ aWExFTATBgNVBAcMDFNhbnRhIE1vbmljYTERMA8GA1UECgwIT25lTG9naW4xGTAX
7
+ BgNVBAMMEGFwcC5vbmVsb2dpbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
8
+ AoGBAMPmjfjy7L35oDpeBXBoRVCgktPkLno9DOEWB7MgYMMVKs2B6ymWQLEWrDug
9
+ MK1hkzWFhIb5fqWLGbWy0J0veGR9/gHOQG+rD/I36xAXnkdiXXhzoiAG/zQxM0ed
10
+ MOUf40n314FC8moErcUg6QabttzesO59HFz6shPuxcWaVAgxAgMBAAEwAwYBAAMB
11
+ AA==
12
+ -----END CERTIFICATE-----
@@ -0,0 +1 @@
1
+ MIIEYTCCA0mgAwIBAgIJAMax+2BoUJmCMA0GCSqGSIb3DQEBBQUAMIHGMQswCQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xIzAhBgNVBAoMGldlbGxzcHJpbmcgV29ybGR3aWRlLCBJbmMuMRwwGgYDVQQLDBNTeXN0ZW1zIEVuZ2luZWVyaW5nMSQwIgYDVQQDDBtzc28ud2VsbHNwcmluZ3dvcmxkd2lkZS5jb20xKTAnBgkqhkiG9w0BCQEWGml0QHdlbGxzcHJpbmd3b3JsZHdpZGUuY29tMB4XDTEzMDIyNzIzNTUwOFoXDTIzMDIyNzIzNTUwOFowgcYxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhJbGxpbm9pczEQMA4GA1UEBwwHQ2hpY2FnbzEjMCEGA1UECgwaV2VsbHNwcmluZyBXb3JsZHdpZGUsIEluYy4xHDAaBgNVBAsME1N5c3RlbXMgRW5naW5lZXJpbmcxJDAiBgNVBAMMG3Nzby53ZWxsc3ByaW5nd29ybGR3aWRlLmNvbTEpMCcGCSqGSIb3DQEJARYaaXRAd2VsbHNwcmluZ3dvcmxkd2lkZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzNhrDaXa00JAIRaxEyDrK/Zjj8bBTQD5dPgugDndYf1AOpzSGpGFU+lPu0QRv0o66K64HrF24FATWI18Q6aZ+xX8QbuBrfia6hOFef29Sk5paS9+DcDCmisuNpl84kbbiazy6S6cFtcdrG9/cr2iXtYmIzz7EfUcP/UVAp24ZW7dWhcvxoqxF9n6Fj94N+rA0dmUFUGz6glm7us3p36xbkiUMpgr3feD/9P34H+2YFsQ2b2DblDI5Z7YULHxBsl5nuhPLFuPN1olcWQBsJYO6iHElFRH+487L2yZ1mLVXKI0LFb/w1rAJpPeUc8E5s1MATAjNx3wPwwqgw30sKtoXAgMBAAGjUDBOMB0GA1UdDgQWBBSrGGV9w3hGXTafkJLUaWBsWiDGaTAfBgNVHSMEGDAWgBSrGGV9w3hGXTafkJLUaWBsWiDGaTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCcvZV0VIwIRD4C5CItFwfNRF2LRBmYiNh4FJbo0wESbH0cWT9vXNdcjcx6PHrIj7ICErSCR5eZmIrSgLEBEkptjVsiFsHWSvMHv37WaHwyhZWnhutss32aP9+ifxQ1lzwm54jZWZsVVVFQH155BDsVeU1UwEhvcCExFa7RNjyvqQrZmyQwMFSzL1cQp0humu0hHLtAI7E32lp5itw6kTOfyhjB8d1bzBVZe6RY64RxOPEcx+9hkrHmfCohdt644jRtPdLTvqqxpscYGD+L2QOt1HpbGgAdcgUZeUHo/eosqpwDOoyuFepz7JzqMncxFN//NmjnFGVZdGR+6bTxKUKq
@@ -0,0 +1,14 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICGzCCAYQCCQCNNcQXom32VDANBgkqhkiG9w0BAQUFADBSMQswCQYDVQQGEwJV
3
+ UzELMAkGA1UECBMCSU4xFTATBgNVBAcTDEluZGlhbmFwb2xpczERMA8GA1UEChMI
4
+ T25lTG9naW4xDDAKBgNVBAsTA0VuZzAeFw0xNDA0MjMxODQxMDFaFw0xNTA0MjMx
5
+ ODQxMDFaMFIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTjEVMBMGA1UEBxMMSW5k
6
+ aWFuYXBvbGlzMREwDwYDVQQKEwhPbmVMb2dpbjEMMAoGA1UECxMDRW5nMIGfMA0G
7
+ CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDo6m+QZvYQ/xL0ElLgupK1QDcYL4f5Pckw
8
+ sNgS9pUvV7fzTqCHk8ThLxTk42MQ2McJsOeUJVP728KhymjFCqxgP4VuwRk9rpAl
9
+ 0+mhy6MPdyjyA6G14jrDWS65ysLchK4t/vwpEDz0SQlEoG1kMzllSm7zZS3XregA
10
+ 7DjNaUYQqwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALM2vGCiQ/vm+a6v40+VX2zd
11
+ qHA2Q/1vF1ibQzJ54MJCOVWvs+vQXfZFhdm0OPM2IrDU7oqvKPqP6xOAeJK6H0yP
12
+ 7M4YL3fatSvIYmmfyXC9kt3Svz/NyrHzPhUnJ0ye/sUSXxnzQxwcm/9PwAqrQaA3
13
+ QpQkH57ybF/OoryPe+2h
14
+ -----END CERTIFICATE-----
@@ -0,0 +1,15 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIICXAIBAAKBgQDo6m+QZvYQ/xL0ElLgupK1QDcYL4f5PckwsNgS9pUvV7fzTqCH
3
+ k8ThLxTk42MQ2McJsOeUJVP728KhymjFCqxgP4VuwRk9rpAl0+mhy6MPdyjyA6G1
4
+ 4jrDWS65ysLchK4t/vwpEDz0SQlEoG1kMzllSm7zZS3XregA7DjNaUYQqwIDAQAB
5
+ AoGBALGR6bRBit+yV5TUU3MZSrf8WQSLWDLgs/33FQSAEYSib4+DJke2lKbI6jkG
6
+ UoSJgFUXFbaQLtMY2+3VDsMKPBdAge9gIdvbkC4yoKjLGm/FBDOxxZcfLpR+9OPq
7
+ U3qM9D0CNuliBWI7Je+p/zs09HIYucpDXy9E18KA1KNF6rfhAkEA9KoNam6wAKnm
8
+ vMzz31ws3RuIOUeo2rx6aaVY95+P9tTxd6U+pNkwxy1aCGP+InVSwlYNA1aQ4Axi
9
+ /GdMIWMkxwJBAPO1CP7cQNZQmu7yusY+GUObDII5YK9WLaY4RAicn5378crPBFxv
10
+ Ukqf9G6FHo7u88iTCIp+vwa3Hn9Tumg3iP0CQQDgUXWBasCVqzCxU5wY4tMDWjXY
11
+ hpoLCpmVeRML3dDJt004rFm2HKe7Rhpw7PTZNQZOxUSjFeA4e0LaNf838UWLAkB8
12
+ QfbHM3ffjhOg96PhhjINdVWoZCb230LBOHj/xxPfUmFTHcBEfQIBSJMxcrBFAnLL
13
+ 9qPpMXymqOFk3ETz9DTlAj8E0qGbp78aVbTOtuwEwNJII+RPw+Zkc+lKR+yaWkAz
14
+ fIXw527NPHH3+rnBG72wyZr9ud4LAum9jh+5No1LQpk=
15
+ -----END RSA PRIVATE KEY-----
@@ -0,0 +1,244 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2
+
3
+ class LogoutRequestTest < Minitest::Test
4
+
5
+ describe "Logoutrequest" do
6
+ let(:settings) { OneLogin::RubySaml::Settings.new }
7
+
8
+ before do
9
+ settings.idp_slo_target_url = "http://unauth.com/logout"
10
+ settings.name_identifier_value = "f00f00"
11
+ end
12
+
13
+ it "create the deflated SAMLRequest URL parameter" do
14
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings)
15
+ assert unauth_url =~ /^http:\/\/unauth\.com\/logout\?SAMLRequest=/
16
+
17
+ inflated = decode_saml_request_payload(unauth_url)
18
+
19
+ assert_match /^<samlp:LogoutRequest/, inflated
20
+ end
21
+
22
+ it "support additional params" do
23
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :hello => nil })
24
+ assert unauth_url =~ /&hello=$/
25
+
26
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :foo => "bar" })
27
+ assert unauth_url =~ /&foo=bar$/
28
+ end
29
+
30
+ it "set sessionindex" do
31
+ sessionidx = random_id
32
+ settings.sessionindex = sessionidx
33
+
34
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :name_id => "there" })
35
+ inflated = decode_saml_request_payload(unauth_url)
36
+
37
+ assert_match /<samlp:SessionIndex/, inflated
38
+ assert_match %r(#{sessionidx}</samlp:SessionIndex>), inflated
39
+ end
40
+
41
+ it "set name_identifier_value" do
42
+ settings.name_identifier_format = "transient"
43
+ name_identifier_value = "abc123"
44
+ settings.name_identifier_value = name_identifier_value
45
+
46
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings, { :name_id => "there" })
47
+ inflated = decode_saml_request_payload(unauth_url)
48
+
49
+ assert_match /<saml:NameID/, inflated
50
+ assert_match %r(#{name_identifier_value}</saml:NameID>), inflated
51
+ end
52
+
53
+ describe "when the target url doesn't contain a query string" do
54
+ it "create the SAMLRequest parameter correctly" do
55
+ settings.idp_slo_target_url = "http://example.com"
56
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings)
57
+ assert unauth_url =~ /^http:\/\/example.com\?SAMLRequest/
58
+ end
59
+ end
60
+
61
+ describe "when the target url contains a query string" do
62
+ it "create the SAMLRequest parameter correctly" do
63
+ settings.idp_slo_target_url = "http://example.com?field=value"
64
+ unauth_url = OneLogin::RubySaml::Logoutrequest.new.create(settings)
65
+ assert unauth_url =~ /^http:\/\/example.com\?field=value&SAMLRequest/
66
+ end
67
+ end
68
+
69
+ describe "consumation of logout may need to track the transaction" do
70
+ it "have access to the request uuid" do
71
+ settings.idp_slo_target_url = "http://example.com?field=value"
72
+
73
+ unauth_req = OneLogin::RubySaml::Logoutrequest.new
74
+ unauth_url = unauth_req.create(settings)
75
+
76
+ inflated = decode_saml_request_payload(unauth_url)
77
+ assert_match %r[ID='#{unauth_req.uuid}'], inflated
78
+ end
79
+ end
80
+
81
+
82
+ describe "when the settings indicate to sign (embedded) logout request" do
83
+
84
+ before do
85
+ # sign the logout request
86
+ settings.security[:logout_requests_signed] = true
87
+ settings.security[:embed_sign] = true
88
+ settings.certificate = ruby_saml_cert_text
89
+ settings.private_key = ruby_saml_key_text
90
+ end
91
+
92
+ it "doesn't sign through create_xml_document" do
93
+ unauth_req = OneLogin::RubySaml::Logoutrequest.new
94
+ inflated = unauth_req.create_xml_document(settings).to_s
95
+
96
+ refute_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], inflated
97
+ refute_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], inflated
98
+ refute_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], inflated
99
+ end
100
+
101
+ it "sign unsigned request" do
102
+ unauth_req = OneLogin::RubySaml::Logoutrequest.new
103
+ unauth_req_doc = unauth_req.create_xml_document(settings)
104
+ inflated = unauth_req_doc.to_s
105
+
106
+ refute_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], inflated
107
+ refute_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], inflated
108
+ refute_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], inflated
109
+
110
+ inflated = unauth_req.sign_document(unauth_req_doc, settings).to_s
111
+
112
+ assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], inflated
113
+ assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], inflated
114
+ assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], inflated
115
+ end
116
+
117
+ it "signs through create_logout_request_xml_doc" do
118
+ unauth_req = OneLogin::RubySaml::Logoutrequest.new
119
+ inflated = unauth_req.create_logout_request_xml_doc(settings).to_s
120
+
121
+ assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], inflated
122
+ assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], inflated
123
+ assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], inflated
124
+ end
125
+
126
+ it "created a signed logout request" do
127
+ settings.compress_request = true
128
+
129
+ unauth_req = OneLogin::RubySaml::Logoutrequest.new
130
+ unauth_url = unauth_req.create(settings)
131
+
132
+ inflated = decode_saml_request_payload(unauth_url)
133
+ assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], inflated
134
+ assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>], inflated
135
+ assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/>], inflated
136
+ end
137
+
138
+ it "create a signed logout request with 256 digest and signature method" do
139
+ settings.compress_request = false
140
+ settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
141
+ settings.security[:digest_method] = XMLSecurity::Document::SHA256
142
+
143
+ params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings)
144
+ request_xml = Base64.decode64(params["SAMLRequest"])
145
+
146
+ assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], request_xml
147
+ assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'/>], request_xml
148
+ assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2001/04/xmlenc#sha256'/>], request_xml
149
+ end
150
+
151
+ it "create a signed logout request with 512 digest and signature method RSA_SHA384" do
152
+ settings.compress_request = false
153
+ settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA384
154
+ settings.security[:digest_method] = XMLSecurity::Document::SHA512
155
+
156
+ params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings)
157
+ request_xml = Base64.decode64(params["SAMLRequest"])
158
+
159
+ assert_match %r[<ds:SignatureValue>([a-zA-Z0-9/+=]+)</ds:SignatureValue>], request_xml
160
+ assert_match %r[<ds:SignatureMethod Algorithm='http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'/>], request_xml
161
+ assert_match %r[<ds:DigestMethod Algorithm='http://www.w3.org/2001/04/xmlenc#sha512'/>], request_xml
162
+ end
163
+ end
164
+
165
+ describe "#create_params when the settings indicate to sign the logout request" do
166
+
167
+ let(:cert) { OpenSSL::X509::Certificate.new(ruby_saml_cert_text) }
168
+
169
+ before do
170
+ # sign the logout request
171
+ settings.security[:logout_requests_signed] = true
172
+ settings.security[:embed_sign] = false
173
+ settings.certificate = ruby_saml_cert_text
174
+ settings.private_key = ruby_saml_key_text
175
+ end
176
+
177
+ it "create a signature parameter with RSA_SHA1 / SHA1 and validate it" do
178
+ settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA1
179
+
180
+ params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com')
181
+ assert params['SAMLRequest']
182
+ assert params[:RelayState]
183
+ assert params['Signature']
184
+ assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA1
185
+
186
+ query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}"
187
+ query_string << "&RelayState=#{CGI.escape(params[:RelayState])}"
188
+ query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}"
189
+
190
+ signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg'])
191
+ assert_equal signature_algorithm, OpenSSL::Digest::SHA1
192
+ assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string)
193
+ end
194
+
195
+ it "create a signature parameter with RSA_SHA256 / SHA256 and validate it" do
196
+ settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
197
+
198
+ params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com')
199
+ assert params['Signature']
200
+ assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA256
201
+
202
+ query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}"
203
+ query_string << "&RelayState=#{CGI.escape(params[:RelayState])}"
204
+ query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}"
205
+
206
+ signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg'])
207
+ assert_equal signature_algorithm, OpenSSL::Digest::SHA256
208
+ assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string)
209
+ end
210
+
211
+ it "create a signature parameter with RSA_SHA384 / SHA384 and validate it" do
212
+ settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA384
213
+
214
+ params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com')
215
+ assert params['Signature']
216
+ assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA384
217
+
218
+ query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}"
219
+ query_string << "&RelayState=#{CGI.escape(params[:RelayState])}"
220
+ query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}"
221
+
222
+ signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg'])
223
+ assert_equal signature_algorithm, OpenSSL::Digest::SHA384
224
+ assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string)
225
+ end
226
+
227
+ it "create a signature parameter with RSA_SHA512 / SHA512 and validate it" do
228
+ settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA512
229
+
230
+ params = OneLogin::RubySaml::Logoutrequest.new.create_params(settings, :RelayState => 'http://example.com')
231
+ assert params['Signature']
232
+ assert_equal params['SigAlg'], XMLSecurity::Document::RSA_SHA512
233
+
234
+ query_string = "SAMLRequest=#{CGI.escape(params['SAMLRequest'])}"
235
+ query_string << "&RelayState=#{CGI.escape(params[:RelayState])}"
236
+ query_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}"
237
+
238
+ signature_algorithm = XMLSecurity::BaseDocument.new.algorithm(params['SigAlg'])
239
+ assert_equal signature_algorithm, OpenSSL::Digest::SHA512
240
+ assert cert.public_key.verify(signature_algorithm.new, Base64.decode64(params['Signature']), query_string)
241
+ end
242
+ end
243
+ end
244
+ end