spid-es 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +15 -0
  2. data/.document +5 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +19 -0
  6. data/README.md +124 -0
  7. data/Rakefile +41 -0
  8. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  9. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  10. data/lib/schemas/xenc_schema.xsd +146 -0
  11. data/lib/schemas/xmldsig_schema.xsd +318 -0
  12. data/lib/spid/ruby-saml/authrequest.rb +196 -0
  13. data/lib/spid/ruby-saml/coding.rb +34 -0
  14. data/lib/spid/ruby-saml/logging.rb +26 -0
  15. data/lib/spid/ruby-saml/logout_request.rb +126 -0
  16. data/lib/spid/ruby-saml/logout_response.rb +132 -0
  17. data/lib/spid/ruby-saml/metadata.rb +353 -0
  18. data/lib/spid/ruby-saml/request.rb +81 -0
  19. data/lib/spid/ruby-saml/response.rb +202 -0
  20. data/lib/spid/ruby-saml/settings.rb +72 -0
  21. data/lib/spid/ruby-saml/validation_error.rb +7 -0
  22. data/lib/spid/ruby-saml/version.rb +5 -0
  23. data/lib/spid-es.rb +14 -0
  24. data/lib/xml_security.rb +165 -0
  25. data/spid-es.gemspec +23 -0
  26. data/test/certificates/certificate1 +12 -0
  27. data/test/logoutrequest_test.rb +98 -0
  28. data/test/request_test.rb +53 -0
  29. data/test/response_test.rb +219 -0
  30. data/test/responses/adfs_response_sha1.xml +46 -0
  31. data/test/responses/adfs_response_sha256.xml +46 -0
  32. data/test/responses/adfs_response_sha384.xml +46 -0
  33. data/test/responses/adfs_response_sha512.xml +46 -0
  34. data/test/responses/no_signature_ns.xml +48 -0
  35. data/test/responses/open_saml_response.xml +56 -0
  36. data/test/responses/response1.xml.base64 +1 -0
  37. data/test/responses/response2.xml.base64 +79 -0
  38. data/test/responses/response3.xml.base64 +66 -0
  39. data/test/responses/response4.xml.base64 +93 -0
  40. data/test/responses/response5.xml.base64 +102 -0
  41. data/test/responses/response_with_ampersands.xml +139 -0
  42. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  43. data/test/responses/simple_saml_php.xml +71 -0
  44. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  45. data/test/settings_test.rb +43 -0
  46. data/test/test_helper.rb +65 -0
  47. data/test/xml_security_test.rb +123 -0
  48. metadata +158 -0
@@ -0,0 +1,202 @@
1
+ require "xml_security"
2
+ require "time"
3
+ require "nokogiri"
4
+ require "base64"
5
+ require "openssl"
6
+ require "digest/sha1"
7
+
8
+ # Only supports SAML 2.0
9
+ module Spid
10
+ module Saml
11
+
12
+ class Response
13
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
14
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
15
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
16
+
17
+ attr_accessor :options, :response, :document, :settings
18
+
19
+ def initialize(response, options = {})
20
+ raise ArgumentError.new("Response cannot be nil") if response.nil?
21
+ self.options = options
22
+ self.response = response
23
+ begin
24
+ self.document = XMLSecurity::SignedDocument.new(Base64.decode64(response))
25
+ rescue REXML::ParseException => e
26
+ if response =~ /</
27
+ self.document = XMLSecurity::SignedDocument.new(response)
28
+ else
29
+ raise e
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ def is_valid?
36
+ validate
37
+ end
38
+
39
+ def validate!
40
+ validate(false)
41
+ end
42
+
43
+ # The value of the user identifier as designated by the initialization request response
44
+ def name_id
45
+ @name_id ||= begin
46
+ node = REXML::XPath.first(document, "/saml2p:Response/saml2:Assertion[@ID='#{document.signed_element_id}']/saml2:Subject/saml2:NameID")
47
+ node ||= REXML::XPath.first(document, "/saml2p:Response[@ID='#{document.signed_element_id}']/saml2:Assertion/saml2:Subject/saml2:NameID")
48
+ node.nil? ? nil : node.text
49
+ end
50
+ end
51
+
52
+ # A hash of alle the attributes with the response. Assuming there is only one value for each key
53
+ def attributes
54
+ @attr_statements ||= begin
55
+ result = {}
56
+
57
+ stmt_element = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AttributeStatement", { "p" => PROTOCOL, "a" => ASSERTION })
58
+ return {} if stmt_element.nil?
59
+
60
+ stmt_element.elements.each do |attr_element|
61
+ name = attr_element.attributes["Name"]
62
+ value = attr_element.elements.first.text
63
+
64
+ result[name] = value
65
+ end
66
+
67
+ result.keys.each do |key|
68
+ result[key.intern] = result[key]
69
+ end
70
+
71
+ result
72
+ end
73
+ end
74
+
75
+ # When this user session should expire at latest
76
+ def session_expires_at
77
+ @expires_at ||= begin
78
+ node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
79
+ parse_time(node, "SessionNotOnOrAfter")
80
+ end
81
+ end
82
+
83
+ # Checks the status of the response for a "Success" code
84
+ def success?
85
+ @status_code ||= begin
86
+ node = REXML::XPath.first(document, "/p:Response/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
87
+ node.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
88
+ end
89
+ end
90
+
91
+ # Conditions (if any) for the assertion to run
92
+ def conditions
93
+ @conditions ||= begin
94
+ REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id}']/a:Conditions", { "p" => PROTOCOL, "a" => ASSERTION })
95
+ end
96
+ end
97
+
98
+ def issuer
99
+ @issuer ||= begin
100
+ node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
101
+ node ||= REXML::XPath.first(document, "/p:Response/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
102
+ node.nil? ? nil : node.text
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def validation_error(message)
109
+ raise ValidationError.new(message)
110
+ end
111
+
112
+ def validate(soft = true)
113
+ # prime the IdP metadata before the document validation.
114
+ # The idp_cert needs to be populated before the validate_response_state method
115
+
116
+ if settings
117
+ Spid::Saml::Metadata.new(settings).get_idp_metadata
118
+ end
119
+ return false if validate_structure(soft) == false
120
+ return false if validate_response_state(soft) == false
121
+ return false if validate_conditions(soft) == false
122
+
123
+ # Just in case a user needs to toss out the signature validation,
124
+ # I'm adding in an option for it. (Sometimes canonicalization is a bitch!)
125
+ return true if settings.skip_validation == true
126
+
127
+ # document.validte populates the idp_cert
128
+ return false if document.validate(get_fingerprint, soft) == false
129
+
130
+ # validate response code
131
+ return false if success? == false
132
+
133
+ return true
134
+ end
135
+
136
+
137
+ def validate_structure(soft = true)
138
+ Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
139
+ @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
140
+ @xml = Nokogiri::XML(self.document.to_s)
141
+ end
142
+ if soft
143
+ @schema.validate(@xml).map{ return false }
144
+ else
145
+ @schema.validate(@xml).map{ |error| raise(Exception.new("#{error.message}\n\n#{@xml.to_s}")) }
146
+ end
147
+ end
148
+
149
+ def validate_response_state(soft = true)
150
+ if response.empty?
151
+ return soft ? false : validation_error("Blank response")
152
+ end
153
+
154
+ if settings.nil?
155
+ return soft ? false : validation_error("No settings on response")
156
+ end
157
+
158
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
159
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
160
+ end
161
+
162
+ true
163
+ end
164
+
165
+ def get_fingerprint
166
+ if settings.idp_cert
167
+ cert_text = Base64.decode64(settings.idp_cert)
168
+ cert = OpenSSL::X509::Certificate.new(cert_text)
169
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
170
+ else
171
+ settings.idp_cert_fingerprint
172
+ end
173
+
174
+ end
175
+
176
+ def validate_conditions(soft = true)
177
+ return true if conditions.nil?
178
+ return true if options[:skip_conditions]
179
+
180
+ if not_before = parse_time(conditions, "NotBefore")
181
+ if Time.now.utc < not_before
182
+ return soft ? false : validation_error("Current time is earlier than NotBefore condition")
183
+ end
184
+ end
185
+
186
+ if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
187
+ if Time.now.utc >= not_on_or_after
188
+ return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
189
+ end
190
+ end
191
+
192
+ true
193
+ end
194
+
195
+ def parse_time(node, attribute)
196
+ if node && node.attributes[attribute]
197
+ Time.parse(node.attributes[attribute])
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,72 @@
1
+ require "xml_security_new"
2
+
3
+ module Spid
4
+ module Saml
5
+ class Settings
6
+
7
+ attr_accessor :sp_name_qualifier, :sp_cert, :sp_private_key, :metadata_signed, :requested_attribute, :organization
8
+ attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :idp_slo_target_url, :idp_metadata, :idp_metadata_ttl, :idp_name_qualifier
9
+ attr_accessor :assertion_consumer_service_binding, :assertion_consumer_service_url
10
+ attr_accessor :name_identifier_value, :name_identifier_format
11
+ attr_accessor :sessionindex, :issuer, :destination_service_url, :authn_context, :requester_identificator
12
+ attr_accessor :single_logout_service_url, :single_logout_service_binding, :single_logout_destination
13
+ attr_accessor :skip_validation
14
+
15
+ def initialize(config = {})
16
+ config.each do |k,v|
17
+ acc = "#{k.to_s}=".to_sym
18
+ self.send(acc, v) if self.respond_to? acc
19
+ end
20
+
21
+ # Set some sane default values on a few options
22
+ self.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
23
+ self.single_logout_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
24
+ # Default cache TTL for metadata is 1 day
25
+ self.idp_metadata_ttl = 86400
26
+ end
27
+
28
+
29
+ def get_fingerprint
30
+ idp_cert_fingerprint || begin
31
+ idp_cert = get_idp_cert
32
+ if idp_cert
33
+ fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new
34
+ fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
35
+ end
36
+ end
37
+ end
38
+
39
+ # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
40
+ #
41
+ def get_idp_cert
42
+ return nil if idp_cert.nil? || idp_cert.empty?
43
+ #decoded_content = Base64.decode64(File.read(idp_cert))
44
+ #formatted_cert = Spid::Saml::Utils.format_cert(idp_cert)
45
+ OpenSSL::X509::Certificate.new(File.read(idp_cert))
46
+ end
47
+
48
+ # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
49
+ #
50
+ def get_sp_cert
51
+ return nil if sp_cert.nil? || sp_cert.empty?
52
+ #decoded_content = Base64.decode64(File.read(sp_cert))
53
+ #formatted_cert = Spid::Saml::Utils.format_cert(decoded_content)
54
+ OpenSSL::X509::Certificate.new(File.read(sp_cert))
55
+ end
56
+
57
+ # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
58
+ #
59
+ def get_sp_key
60
+ return nil if sp_private_key.nil? || sp_private_key.empty?
61
+
62
+ #formatted_private_key = Spid::Saml::Utils.format_private_key(sp_private_key)
63
+ OpenSSL::PKey::RSA.new(File.read(sp_private_key))
64
+ end
65
+
66
+
67
+
68
+
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,7 @@
1
+ module Spid
2
+ module Saml
3
+ class ValidationError < Exception
4
+ end
5
+ end
6
+ end
7
+
@@ -0,0 +1,5 @@
1
+ module Spid
2
+ module Saml
3
+ VERSION = '0.6.0'
4
+ end
5
+ end
data/lib/spid-es.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "xml_security"
2
+ require 'spid/ruby-saml/utils'
3
+ require 'spid/ruby-saml/logging'
4
+ require 'spid/ruby-saml/coding'
5
+ require 'spid/ruby-saml/request'
6
+ require 'spid/ruby-saml/authrequest'
7
+ require 'spid/ruby-saml/logout_request'
8
+ require 'spid/ruby-saml/logout_response'
9
+ require 'spid/ruby-saml/response'
10
+ require 'spid/ruby-saml/settings'
11
+ require 'spid/ruby-saml/error_handling'
12
+ require 'spid/ruby-saml/validation_error'
13
+ require 'spid/ruby-saml/metadata'
14
+ require 'spid/ruby-saml/version'
@@ -0,0 +1,165 @@
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 "spid/ruby-saml/validation_error"
33
+
34
+ module XMLSecurity
35
+
36
+ class SignedDocument < REXML::Document
37
+ C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
38
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
39
+
40
+ attr_accessor :signed_element_id, :sig_element, :noko_sig_element
41
+
42
+ def initialize(response)
43
+ super(response)
44
+ extract_signed_element_id
45
+ end
46
+
47
+ def validate(idp_cert_fingerprint, soft = true)
48
+ # get cert from response
49
+ cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
50
+ base64_cert = cert_element.text
51
+ cert_text = Base64.decode64(base64_cert)
52
+ cert = OpenSSL::X509::Certificate.new(cert_text)
53
+
54
+ # check cert matches registered idp cert
55
+ fingerprint = Digest::SHA1.hexdigest(cert.to_der)
56
+
57
+ if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
58
+ return soft ? false : (raise Spid::Saml::ValidationError.new("Fingerprint mismatch"))
59
+ end
60
+
61
+ validate_doc(base64_cert, soft)
62
+ end
63
+
64
+ def validate_doc(base64_cert, soft = true)
65
+ # validate references
66
+
67
+ # check for inclusive namespaces
68
+ inclusive_namespaces = extract_inclusive_namespaces
69
+
70
+ document = Nokogiri.parse(self.to_s)
71
+
72
+ # store and remove signature node
73
+ self.sig_element ||= begin
74
+ element = REXML::XPath.first(self, "//ds:Signature", {"ds"=>DSIG})
75
+ element.remove
76
+ end
77
+
78
+
79
+ # verify signature
80
+ signed_info_element = REXML::XPath.first(sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
81
+ self.noko_sig_element ||= document.at_xpath('//ds:Signature', 'ds' => DSIG)
82
+ noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
83
+ canon_algorithm = canon_algorithm REXML::XPath.first(sig_element, '//ds:CanonicalizationMethod')
84
+ canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
85
+ noko_sig_element.remove
86
+
87
+ # check digests
88
+ REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
89
+ uri = ref.attributes.get_attribute("URI").value
90
+
91
+ hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
92
+ canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod')
93
+ canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces).gsub('&','&amp;')
94
+
95
+ digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
96
+
97
+ hash = digest_algorithm.digest(canon_hashed_element)
98
+ digest_value = Base64.decode64(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG}).text)
99
+
100
+ unless digests_match?(hash, digest_value)
101
+ return soft ? false : (raise Spid::Saml::ValidationError.new("Digest mismatch"))
102
+ end
103
+ end
104
+
105
+ base64_signature = REXML::XPath.first(sig_element, "//ds:SignatureValue", {"ds"=>DSIG}).text
106
+ signature = Base64.decode64(base64_signature)
107
+
108
+ # get certificate object
109
+ cert_text = Base64.decode64(base64_cert)
110
+ cert = OpenSSL::X509::Certificate.new(cert_text)
111
+
112
+ # signature method
113
+ signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
114
+
115
+ unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
116
+ return soft ? false : (raise Spid::Saml::ValidationError.new("Key validation error"))
117
+ end
118
+
119
+ return true
120
+ end
121
+
122
+ private
123
+
124
+ def digests_match?(hash, digest_value)
125
+ hash == digest_value
126
+ end
127
+
128
+ def extract_signed_element_id
129
+ reference_element = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
130
+ self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
131
+ end
132
+
133
+ def canon_algorithm(element)
134
+ algorithm = element.attribute('Algorithm').value if element
135
+ case algorithm
136
+ when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
137
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
138
+ when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
139
+ else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
140
+ end
141
+ end
142
+
143
+ def algorithm(element)
144
+ algorithm = element.attribute("Algorithm").value if element
145
+ algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
146
+ case algorithm
147
+ when 256 then OpenSSL::Digest::SHA256
148
+ when 384 then OpenSSL::Digest::SHA384
149
+ when 512 then OpenSSL::Digest::SHA512
150
+ else
151
+ OpenSSL::Digest::SHA1
152
+ end
153
+ end
154
+
155
+ def extract_inclusive_namespaces
156
+ if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
157
+ prefix_list = element.attributes.get_attribute("PrefixList").value
158
+ prefix_list.split(" ")
159
+ else
160
+ []
161
+ end
162
+ end
163
+
164
+ end
165
+ end
data/spid-es.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'spid-es'
5
+ s.version = '0.0.1'
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Fabiano Pavan"]
9
+ s.date = Time.now.strftime("%Y-%m-%d")
10
+ s.description = %q{SAML toolkit for Ruby programs to integrate with SPID }
11
+ s.email = %q{fabiano.pavan@soluzionipa.it}
12
+ s.files = `git ls-files`.split("\n")
13
+ s.homepage = %q{https://github.com/EuroServizi/spid-es}
14
+ s.rdoc_options = ["--charset=UTF-8"]
15
+ s.require_paths = ["lib"]
16
+ s.summary = %q{SAML Ruby Tookit Spid}
17
+ s.license = "MIT"
18
+
19
+ s.add_runtime_dependency("canonix", ["0.1.1"])
20
+ s.add_runtime_dependency("uuid", ["~> 2.3"])
21
+ s.add_runtime_dependency("nokogiri", '~> 1.6', '>= 1.6.7.2')
22
+ s.add_runtime_dependency("addressable", '~> 2.4', '>= 2.4.0')
23
+ 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,98 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2
+
3
+ class RequestTest < Test::Unit::TestCase
4
+
5
+ context "Logoutrequest" do
6
+ settings = Spid::Saml::Settings.new
7
+
8
+ should "create the deflated SAMLRequest URL parameter" do
9
+ settings.idp_slo_target_url = "http://unauth.com/logout"
10
+ unauth_url = Spid::Saml::Logoutrequest.new.create(settings)
11
+ assert unauth_url =~ /^http:\/\/unauth\.com\/logout\?SAMLRequest=/
12
+
13
+ inflated = decode_saml_request_payload(unauth_url)
14
+
15
+ assert_match /^<samlp:LogoutRequest/, inflated
16
+ end
17
+
18
+ should "support additional params" do
19
+
20
+ unauth_url = Spid::Saml::Logoutrequest.new.create(settings, { :hello => nil })
21
+ assert unauth_url =~ /&hello=$/
22
+
23
+ unauth_url = Spid::Saml::Logoutrequest.new.create(settings, { :foo => "bar" })
24
+ assert unauth_url =~ /&foo=bar$/
25
+ end
26
+
27
+ should "set sessionindex" do
28
+ settings.idp_slo_target_url = "http://example.com"
29
+ sessionidx = UUID.new.generate
30
+ settings.sessionindex = sessionidx
31
+
32
+ unauth_url = Spid::Saml::Logoutrequest.new.create(settings, { :name_id => "there" })
33
+ inflated = decode_saml_request_payload(unauth_url)
34
+
35
+ assert_match /<samlp:SessionIndex/, inflated
36
+ assert_match %r(#{sessionidx}</samlp:SessionIndex>), inflated
37
+ end
38
+
39
+ should "set name_identifier_value" do
40
+ settings = Spid::Saml::Settings.new
41
+ settings.idp_slo_target_url = "http://example.com"
42
+ settings.name_identifier_format = "transient"
43
+ name_identifier_value = "abc123"
44
+ settings.name_identifier_value = name_identifier_value
45
+
46
+ unauth_url = Spid::Saml::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
+ context "when the target url doesn't contain a query string" do
54
+ should "create the SAMLRequest parameter correctly" do
55
+ settings = Spid::Saml::Settings.new
56
+ settings.idp_slo_target_url = "http://example.com"
57
+
58
+ unauth_url = Spid::Saml::Logoutrequest.new.create(settings)
59
+ assert unauth_url =~ /^http:\/\/example.com\?SAMLRequest/
60
+ end
61
+ end
62
+
63
+ context "when the target url contains a query string" do
64
+ should "create the SAMLRequest parameter correctly" do
65
+ settings = Spid::Saml::Settings.new
66
+ settings.idp_slo_target_url = "http://example.com?field=value"
67
+
68
+ unauth_url = Spid::Saml::Logoutrequest.new.create(settings)
69
+ assert unauth_url =~ /^http:\/\/example.com\?field=value&SAMLRequest/
70
+ end
71
+ end
72
+
73
+ context "consumation of logout may need to track the transaction" do
74
+ should "have access to the request uuid" do
75
+ settings = Spid::Saml::Settings.new
76
+ settings.idp_slo_target_url = "http://example.com?field=value"
77
+
78
+ unauth_req = Spid::Saml::Logoutrequest.new
79
+ unauth_url = unauth_req.create(settings)
80
+
81
+ inflated = decode_saml_request_payload(unauth_url)
82
+ assert_match %r[ID='#{unauth_req.uuid}'], inflated
83
+ end
84
+ end
85
+ end
86
+
87
+ def decode_saml_request_payload(unauth_url)
88
+ payload = CGI.unescape(unauth_url.split("SAMLRequest=").last)
89
+ decoded = Base64.decode64(payload)
90
+
91
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
92
+ inflated = zstream.inflate(decoded)
93
+ zstream.finish
94
+ zstream.close
95
+ inflated
96
+ end
97
+
98
+ end
@@ -0,0 +1,53 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
2
+
3
+ class RequestTest < Test::Unit::TestCase
4
+
5
+ context "Authrequest" do
6
+ should "create the deflated SAMLRequest URL parameter" do
7
+ settings = Spid::Saml::Settings.new
8
+ settings.idp_sso_target_url = "http://example.com"
9
+ auth_url = Spid::Saml::Authrequest.new.create(settings)
10
+ assert auth_url =~ /^http:\/\/example\.com\?SAMLRequest=/
11
+ payload = CGI.unescape(auth_url.split("=").last)
12
+ decoded = Base64.decode64(payload)
13
+
14
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
15
+ inflated = zstream.inflate(decoded)
16
+ zstream.finish
17
+ zstream.close
18
+
19
+ assert_match /^<samlp:AuthnRequest/, inflated
20
+ end
21
+
22
+ should "accept extra parameters" do
23
+ settings = Spid::Saml::Settings.new
24
+ settings.idp_sso_target_url = "http://example.com"
25
+
26
+ auth_url = Spid::Saml::Authrequest.new.create(settings, { :hello => "there" })
27
+ assert auth_url =~ /&hello=there$/
28
+
29
+ auth_url = Spid::Saml::Authrequest.new.create(settings, { :hello => nil })
30
+ assert auth_url =~ /&hello=$/
31
+ end
32
+
33
+ context "when the target url doesn't contain a query string" do
34
+ should "create the SAMLRequest parameter correctly" do
35
+ settings = Spid::Saml::Settings.new
36
+ settings.idp_sso_target_url = "http://example.com"
37
+
38
+ auth_url = Spid::Saml::Authrequest.new.create(settings)
39
+ assert auth_url =~ /^http:\/\/example.com\?SAMLRequest/
40
+ end
41
+ end
42
+
43
+ context "when the target url contains a query string" do
44
+ should "create the SAMLRequest parameter correctly" do
45
+ settings = Spid::Saml::Settings.new
46
+ settings.idp_sso_target_url = "http://example.com?field=value"
47
+
48
+ auth_url = Spid::Saml::Authrequest.new.create(settings)
49
+ assert auth_url =~ /^http:\/\/example.com\?field=value&SAMLRequest/
50
+ end
51
+ end
52
+ end
53
+ end