kl-ruby-saml 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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +14 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +9 -0
  6. data/LICENSE +19 -0
  7. data/README.md +575 -0
  8. data/Rakefile +41 -0
  9. data/changelog.md +75 -0
  10. data/gemfiles/nokogiri-1.5.gemfile +5 -0
  11. data/lib/onelogin/ruby-saml.rb +17 -0
  12. data/lib/onelogin/ruby-saml/attribute_service.rb +57 -0
  13. data/lib/onelogin/ruby-saml/attributes.rb +128 -0
  14. data/lib/onelogin/ruby-saml/authrequest.rb +156 -0
  15. data/lib/onelogin/ruby-saml/http_error.rb +7 -0
  16. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +161 -0
  17. data/lib/onelogin/ruby-saml/logging.rb +30 -0
  18. data/lib/onelogin/ruby-saml/logoutrequest.rb +131 -0
  19. data/lib/onelogin/ruby-saml/logoutresponse.rb +241 -0
  20. data/lib/onelogin/ruby-saml/metadata.rb +123 -0
  21. data/lib/onelogin/ruby-saml/response.rb +722 -0
  22. data/lib/onelogin/ruby-saml/saml_message.rb +158 -0
  23. data/lib/onelogin/ruby-saml/settings.rb +165 -0
  24. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +258 -0
  25. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +136 -0
  26. data/lib/onelogin/ruby-saml/utils.rb +172 -0
  27. data/lib/onelogin/ruby-saml/validation_error.rb +7 -0
  28. data/lib/onelogin/ruby-saml/version.rb +5 -0
  29. data/lib/ruby-saml.rb +1 -0
  30. data/lib/schemas/saml-schema-assertion-2.0.xsd +283 -0
  31. data/lib/schemas/saml-schema-authn-context-2.0.xsd +23 -0
  32. data/lib/schemas/saml-schema-authn-context-types-2.0.xsd +821 -0
  33. data/lib/schemas/saml-schema-metadata-2.0.xsd +337 -0
  34. data/lib/schemas/saml-schema-protocol-2.0.xsd +302 -0
  35. data/lib/schemas/sstc-metadata-attr.xsd +35 -0
  36. data/lib/schemas/sstc-saml-attribute-ext.xsd +25 -0
  37. data/lib/schemas/sstc-saml-metadata-algsupport-v1.0.xsd +41 -0
  38. data/lib/schemas/sstc-saml-metadata-ui-v1.0.xsd +89 -0
  39. data/lib/schemas/xenc-schema.xsd +136 -0
  40. data/lib/schemas/xml.xsd +287 -0
  41. data/lib/schemas/xmldsig-core-schema.xsd +309 -0
  42. data/lib/xml_security.rb +358 -0
  43. data/ruby-saml.gemspec +57 -0
  44. data/test/certificates/certificate1 +12 -0
  45. data/test/certificates/certificate_without_head_foot +1 -0
  46. data/test/certificates/formatted_certificate +14 -0
  47. data/test/certificates/formatted_private_key +12 -0
  48. data/test/certificates/formatted_rsa_private_key +12 -0
  49. data/test/certificates/invalid_certificate1 +1 -0
  50. data/test/certificates/invalid_certificate2 +1 -0
  51. data/test/certificates/invalid_certificate3 +12 -0
  52. data/test/certificates/invalid_private_key1 +1 -0
  53. data/test/certificates/invalid_private_key2 +1 -0
  54. data/test/certificates/invalid_private_key3 +10 -0
  55. data/test/certificates/invalid_rsa_private_key1 +1 -0
  56. data/test/certificates/invalid_rsa_private_key2 +1 -0
  57. data/test/certificates/invalid_rsa_private_key3 +10 -0
  58. data/test/certificates/ruby-saml.crt +14 -0
  59. data/test/certificates/ruby-saml.key +15 -0
  60. data/test/idp_metadata_parser_test.rb +95 -0
  61. data/test/logging_test.rb +62 -0
  62. data/test/logout_requests/invalid_slo_request.xml +6 -0
  63. data/test/logout_requests/slo_request.xml +4 -0
  64. data/test/logout_requests/slo_request.xml.base64 +1 -0
  65. data/test/logout_requests/slo_request_deflated.xml.base64 +1 -0
  66. data/test/logout_requests/slo_request_with_session_index.xml +5 -0
  67. data/test/logout_responses/logoutresponse_fixtures.rb +67 -0
  68. data/test/logoutrequest_test.rb +211 -0
  69. data/test/logoutresponse_test.rb +258 -0
  70. data/test/metadata_test.rb +203 -0
  71. data/test/request_test.rb +282 -0
  72. data/test/response_test.rb +1094 -0
  73. data/test/responses/adfs_response_sha1.xml +46 -0
  74. data/test/responses/adfs_response_sha256.xml +46 -0
  75. data/test/responses/adfs_response_sha384.xml +46 -0
  76. data/test/responses/adfs_response_sha512.xml +46 -0
  77. data/test/responses/adfs_response_xmlns.xml +45 -0
  78. data/test/responses/attackxee.xml +13 -0
  79. data/test/responses/idp_descriptor.xml +3 -0
  80. data/test/responses/invalids/invalid_audience.xml.base64 +1 -0
  81. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  82. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  83. data/test/responses/invalids/invalid_signature_position.xml.base64 +1 -0
  84. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +1 -0
  85. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +1 -0
  86. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +1 -0
  87. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +1 -0
  88. data/test/responses/invalids/multiple_assertions.xml.base64 +2 -0
  89. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  90. data/test/responses/invalids/no_id.xml.base64 +1 -0
  91. data/test/responses/invalids/no_saml2.xml.base64 +1 -0
  92. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  93. data/test/responses/invalids/no_status.xml.base64 +1 -0
  94. data/test/responses/invalids/no_status_code.xml.base64 +1 -0
  95. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +1 -0
  96. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +1 -0
  97. data/test/responses/invalids/response_encrypted_attrs.xml.base64 +1 -0
  98. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +1 -0
  99. data/test/responses/invalids/status_code_responder.xml.base64 +1 -0
  100. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +1 -0
  101. data/test/responses/no_signature_ns.xml +48 -0
  102. data/test/responses/open_saml_response.xml +56 -0
  103. data/test/responses/response_assertion_wrapped.xml.base64 +93 -0
  104. data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
  105. data/test/responses/response_eval.xml +7 -0
  106. data/test/responses/response_no_cert_and_encrypted_attrs.xml +29 -0
  107. data/test/responses/response_unsigned_xml_base64 +1 -0
  108. data/test/responses/response_with_ampersands.xml +139 -0
  109. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  110. data/test/responses/response_with_multiple_attribute_values.xml +67 -0
  111. data/test/responses/response_with_saml2_namespace.xml.base64 +102 -0
  112. data/test/responses/response_with_signed_assertion.xml.base64 +66 -0
  113. data/test/responses/response_with_signed_assertion_2.xml.base64 +1 -0
  114. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  115. data/test/responses/response_without_attributes.xml.base64 +79 -0
  116. data/test/responses/response_wrapped.xml.base64 +150 -0
  117. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
  118. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  119. data/test/responses/simple_saml_php.xml +71 -0
  120. data/test/responses/starfield_response.xml.base64 +1 -0
  121. data/test/responses/test_sign.xml +43 -0
  122. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
  123. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
  124. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
  125. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
  126. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
  127. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
  128. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  129. data/test/responses/valid_response.xml.base64 +1 -0
  130. data/test/saml_message_test.rb +56 -0
  131. data/test/settings_test.rb +218 -0
  132. data/test/slo_logoutrequest_test.rb +275 -0
  133. data/test/slo_logoutresponse_test.rb +185 -0
  134. data/test/test_helper.rb +252 -0
  135. data/test/utils_test.rb +145 -0
  136. data/test/xml_security_test.rb +329 -0
  137. metadata +415 -0
@@ -0,0 +1,7 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class HttpError < StandardError
4
+ end
5
+ end
6
+ end
7
+
@@ -0,0 +1,161 @@
1
+ require "base64"
2
+ require "uuid"
3
+ require "zlib"
4
+ require "cgi"
5
+ require "net/http"
6
+ require "net/https"
7
+ require "rexml/document"
8
+ require "rexml/xpath"
9
+
10
+ # Only supports SAML 2.0
11
+ module OneLogin
12
+ module RubySaml
13
+ include REXML
14
+
15
+ # Auxiliary class to retrieve and parse the Identity Provider Metadata
16
+ #
17
+ class IdpMetadataParser
18
+
19
+ METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
20
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
21
+
22
+ attr_reader :document
23
+ attr_reader :response
24
+
25
+ # Parse the Identity Provider metadata and update the settings with the
26
+ # IdP values
27
+ #
28
+ # @param (see IdpMetadataParser#get_idp_metadata)
29
+ # @return (see IdpMetadataParser#get_idp_metadata)
30
+ # @raise (see IdpMetadataParser#get_idp_metadata)
31
+ def parse_remote(url, validate_cert = true)
32
+ idp_metadata = get_idp_metadata(url, validate_cert)
33
+ parse(idp_metadata)
34
+ end
35
+
36
+ # Parse the Identity Provider metadata and update the settings with the IdP values
37
+ # @param idp_metadata [String]
38
+ #
39
+ def parse(idp_metadata)
40
+ @document = REXML::Document.new(idp_metadata)
41
+
42
+ OneLogin::RubySaml::Settings.new.tap do |settings|
43
+ settings.idp_entity_id = idp_entity_id
44
+ settings.name_identifier_format = idp_name_id_format
45
+ settings.idp_sso_target_url = single_signon_service_url
46
+ settings.idp_slo_target_url = single_logout_service_url
47
+ settings.idp_cert_fingerprint = fingerprint
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # Retrieve the remote IdP metadata from the URL or a cached copy.
54
+ # @param url [String] Url where the XML of the Identity Provider Metadata is published.
55
+ # @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
56
+ # @return [REXML::document] Parsed XML IdP metadata
57
+ # @raise [HttpError] Failure to fetch remote IdP metadata
58
+ def get_idp_metadata(url, validate_cert)
59
+ uri = URI.parse(url)
60
+ if uri.scheme == "http"
61
+ response = Net::HTTP.get_response(uri)
62
+ meta_text = response.body
63
+ elsif uri.scheme == "https"
64
+ http = Net::HTTP.new(uri.host, uri.port)
65
+ http.use_ssl = true
66
+ # Most IdPs will probably use self signed certs
67
+ if validate_cert
68
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
69
+
70
+ # Net::HTTP in Ruby 1.8 did not set the default certificate store
71
+ # automatically when VERIFY_PEER was specified.
72
+ if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
73
+ http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
74
+ end
75
+ else
76
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
77
+ end
78
+ get = Net::HTTP::Get.new(uri.request_uri)
79
+ response = http.request(get)
80
+ meta_text = response.body
81
+ else
82
+ raise ArgumentError.new("url must begin with http or https")
83
+ end
84
+
85
+ unless response.is_a? Net::HTTPSuccess
86
+ raise OneLogin::RubySaml::HttpError.new("Failed to fetch idp metadata")
87
+ end
88
+
89
+ meta_text
90
+ end
91
+
92
+ # @return [String|nil] IdP Entity ID value if exists
93
+ #
94
+ def idp_entity_id
95
+ node = REXML::XPath.first(
96
+ document,
97
+ "/md:EntityDescriptor/@entityID",
98
+ { "md" => METADATA }
99
+ )
100
+ node.value if node
101
+ end
102
+
103
+ # @return [String|nil] IdP Name ID Format value if exists
104
+ #
105
+ def idp_name_id_format
106
+ node = REXML::XPath.first(
107
+ document,
108
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
109
+ { "md" => METADATA }
110
+ )
111
+ node.text if node
112
+ end
113
+
114
+ # @return [String|nil] SingleSignOnService endpoint if exists
115
+ #
116
+ def single_signon_service_url
117
+ node = REXML::XPath.first(
118
+ document,
119
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location",
120
+ { "md" => METADATA }
121
+ )
122
+ node.value if node
123
+ end
124
+
125
+ # @return [String|nil] SingleLogoutService endpoint if exists
126
+ #
127
+ def single_logout_service_url
128
+ node = REXML::XPath.first(
129
+ document,
130
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location",
131
+ { "md" => METADATA }
132
+ )
133
+ node.value if node
134
+ end
135
+
136
+ # @return [String|nil] X509Certificate if exists
137
+ #
138
+ def certificate
139
+ @certificate ||= begin
140
+ node = REXML::XPath.first(
141
+ document,
142
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
143
+ { "md" => METADATA, "ds" => DSIG }
144
+ )
145
+ Base64.decode64(node.text) if node
146
+ end
147
+ end
148
+
149
+ # @return [String|nil] the SHA-1 fingerpint of the X509Certificate if it exists
150
+ #
151
+ def fingerprint
152
+ @fingerprint ||= begin
153
+ if certificate
154
+ cert = OpenSSL::X509::Certificate.new(certificate)
155
+ Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,30 @@
1
+ require 'logger'
2
+
3
+ # Simplistic log class when we're running in Rails
4
+ module OneLogin
5
+ module RubySaml
6
+ class Logging
7
+ DEFAULT_LOGGER = ::Logger.new(STDOUT)
8
+
9
+ def self.logger
10
+ @logger || (defined?(::Rails) && Rails.logger) || DEFAULT_LOGGER
11
+ end
12
+
13
+ def self.logger=(logger)
14
+ @logger = logger
15
+ end
16
+
17
+ def self.debug(message)
18
+ return if !!ENV["ruby-saml/testing"]
19
+
20
+ logger.debug message
21
+ end
22
+
23
+ def self.info(message)
24
+ return if !!ENV["ruby-saml/testing"]
25
+
26
+ logger.info message
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,131 @@
1
+ require "uuid"
2
+
3
+ require "onelogin/ruby-saml/logging"
4
+ require "onelogin/ruby-saml/saml_message"
5
+
6
+ # Only supports SAML 2.0
7
+ module OneLogin
8
+ module RubySaml
9
+
10
+ # SAML2 Logout Request (SLO SP initiated, Builder)
11
+ #
12
+ class Logoutrequest < SamlMessage
13
+
14
+ # Logout Request ID
15
+ attr_reader :uuid
16
+
17
+ # Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class.
18
+ # Asigns an ID, a random uuid.
19
+ #
20
+ def initialize
21
+ @uuid = "_" + UUID.new.generate
22
+ end
23
+
24
+ # Creates the Logout Request string.
25
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
26
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
27
+ # @return [String] Logout Request string that includes the SAMLRequest
28
+ #
29
+ def create(settings, params={})
30
+ params = create_params(settings, params)
31
+ params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
32
+ saml_request = CGI.escape(params.delete("SAMLRequest"))
33
+ request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
34
+ params.each_pair do |key, value|
35
+ request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
36
+ end
37
+ @logout_url = settings.idp_slo_target_url + request_params
38
+ end
39
+
40
+ # Creates the Get parameters for the logout request.
41
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
42
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
43
+ # @return [Hash] Parameters
44
+ #
45
+ def create_params(settings, params={})
46
+ # The method expects :RelayState but sometimes we get 'RelayState' instead.
47
+ # Based on the HashWithIndifferentAccess value in Rails we could experience
48
+ # conflicts so this line will solve them.
49
+ relay_state = params[:RelayState] || params['RelayState']
50
+
51
+ request_doc = create_logout_request_xml_doc(settings)
52
+ request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
53
+
54
+ request = ""
55
+ request_doc.write(request)
56
+
57
+ Logging.debug "Created SLO Logout Request: #{request}"
58
+
59
+ request = deflate(request) if settings.compress_request
60
+ base64_request = encode(request)
61
+ request_params = {"SAMLRequest" => base64_request}
62
+
63
+ if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
64
+ params['SigAlg'] = settings.security[:signature_method]
65
+ url_string = OneLogin::RubySaml::Utils.build_query(
66
+ :type => 'SAMLRequest',
67
+ :data => base64_request,
68
+ :relay_state => relay_state,
69
+ :sig_alg => params['SigAlg']
70
+ )
71
+ sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
72
+ signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
73
+ params['Signature'] = encode(signature)
74
+ end
75
+
76
+ params.each_pair do |key, value|
77
+ request_params[key] = value.to_s
78
+ end
79
+
80
+ request_params
81
+ end
82
+
83
+ # Creates the SAMLRequest String.
84
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
85
+ # @return [String] The SAMLRequest String.
86
+ #
87
+ def create_logout_request_xml_doc(settings)
88
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
89
+
90
+ request_doc = XMLSecurity::Document.new
91
+ request_doc.uuid = uuid
92
+
93
+ root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
94
+ root.attributes['ID'] = uuid
95
+ root.attributes['IssueInstant'] = time
96
+ root.attributes['Version'] = "2.0"
97
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil?
98
+
99
+ if settings.issuer
100
+ issuer = root.add_element "saml:Issuer"
101
+ issuer.text = settings.issuer
102
+ end
103
+
104
+ nameid = root.add_element "saml:NameID"
105
+ if settings.name_identifier_value
106
+ nameid.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
107
+ nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
108
+ nameid.text = settings.name_identifier_value
109
+ else
110
+ # If no NameID is present in the settings we generate one
111
+ nameid.text = "_" + UUID.new.generate
112
+ nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
113
+ end
114
+
115
+ if settings.sessionindex
116
+ sessionindex = root.add_element "samlp:SessionIndex"
117
+ sessionindex.text = settings.sessionindex
118
+ end
119
+
120
+ # embed signature
121
+ if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
122
+ private_key = settings.get_sp_key
123
+ cert = settings.get_sp_cert
124
+ request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
125
+ end
126
+
127
+ request_doc
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,241 @@
1
+ require "xml_security"
2
+ require "onelogin/ruby-saml/saml_message"
3
+
4
+ require "time"
5
+
6
+ # Only supports SAML 2.0
7
+ module OneLogin
8
+ module RubySaml
9
+
10
+ # SAML2 Logout Response (SLO IdP initiated, Parser)
11
+ #
12
+ class Logoutresponse < SamlMessage
13
+
14
+ # OneLogin::RubySaml::Settings Toolkit settings
15
+ attr_accessor :settings
16
+
17
+ # Array with the causes
18
+ attr_accessor :errors
19
+
20
+ attr_reader :document
21
+ attr_reader :response
22
+ attr_reader :options
23
+
24
+ attr_accessor :soft
25
+
26
+ # Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class.
27
+ # @param response [String] A UUEncoded logout response from the IdP.
28
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
29
+ # @param options [Hash] Extra parameters.
30
+ # :matches_request_id It will validate that the logout response matches the ID of the request.
31
+ # :get_params GET Parameters, including the SAMLResponse
32
+ # @raise [ArgumentError] if response is nil
33
+ #
34
+ def initialize(response, settings = nil, options = {})
35
+ @errors = []
36
+ raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
37
+ @settings = settings
38
+
39
+ if settings.nil? || settings.soft.nil?
40
+ @soft = true
41
+ else
42
+ @soft = settings.soft
43
+ end
44
+
45
+ @options = options
46
+ @response = decode_raw_saml(response)
47
+ @document = XMLSecurity::SignedDocument.new(@response)
48
+ end
49
+
50
+ # Append the cause to the errors array, and based on the value of soft, return false or raise
51
+ # an exception
52
+ def append_error(error_msg)
53
+ @errors << error_msg
54
+ return soft ? false : validation_error(error_msg)
55
+ end
56
+
57
+ # Reset the errors array
58
+ def reset_errors!
59
+ @errors = []
60
+ end
61
+
62
+ # Checks if the Status has the "Success" code
63
+ # @return [Boolean] True if the StatusCode is Sucess
64
+ # @raise [ValidationError] if soft == false and validation fails
65
+ #
66
+ def success?
67
+ unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
68
+ return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>")
69
+ end
70
+ true
71
+ end
72
+
73
+ # @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
74
+ #
75
+ def in_response_to
76
+ @in_response_to ||= begin
77
+ node = REXML::XPath.first(
78
+ document,
79
+ "/p:LogoutResponse",
80
+ { "p" => PROTOCOL, "a" => ASSERTION }
81
+ )
82
+ node.nil? ? nil : node.attributes['InResponseTo']
83
+ end
84
+ end
85
+
86
+ # @return [String] Gets the Issuer from the Logout Response.
87
+ #
88
+ def issuer
89
+ @issuer ||= begin
90
+ node = REXML::XPath.first(
91
+ document,
92
+ "/p:LogoutResponse/a:Issuer",
93
+ { "p" => PROTOCOL, "a" => ASSERTION }
94
+ )
95
+ node.nil? ? nil : node.text
96
+ end
97
+ end
98
+
99
+ # @return [String] Gets the StatusCode from a Logout Response.
100
+ #
101
+ def status_code
102
+ @status_code ||= begin
103
+ node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
104
+ node.nil? ? nil : node.attributes["Value"]
105
+ end
106
+ end
107
+
108
+ def status_message
109
+ @status_message ||= begin
110
+ node = REXML::XPath.first(
111
+ document,
112
+ "/p:LogoutResponse/p:Status/p:StatusMessage",
113
+ { "p" => PROTOCOL, "a" => ASSERTION }
114
+ )
115
+ node.text if node
116
+ end
117
+ end
118
+
119
+ # Aux function to validate the Logout Response
120
+ # @return [Boolean] TRUE if the SAML Response is valid
121
+ # @raise [ValidationError] if soft == false and validation fails
122
+ #
123
+ def validate
124
+ reset_errors!
125
+
126
+ valid_state? &&
127
+ validate_success_status &&
128
+ validate_structure &&
129
+ valid_in_response_to? &&
130
+ valid_issuer? &&
131
+ validate_signature
132
+ end
133
+
134
+ private
135
+
136
+ # Validates the Status of the Logout Response
137
+ # If fails, the error is added to the errors array, including the StatusCode returned and the Status Message.
138
+ # @return [Boolean] True if the Logout Response contains a Success code, otherwise False if soft=True
139
+ # @raise [ValidationError] if soft == false and validation fails
140
+ #
141
+ def validate_success_status
142
+ return true if success?
143
+
144
+ error_msg = 'The status code of the Logout Response was not Success'
145
+ status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
146
+ append_error(status_error_msg)
147
+ end
148
+
149
+ # Validates the Logout Response against the specified schema.
150
+ # @return [Boolean] True if the XML is valid, otherwise False if soft=True
151
+ # @raise [ValidationError] if soft == false and validation fails
152
+ #
153
+ def validate_structure
154
+ unless valid_saml?(document, soft)
155
+ return append_error("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd")
156
+ end
157
+
158
+ true
159
+ end
160
+
161
+ # Validates that the Logout Response provided in the initialization is not empty,
162
+ # also check that the setting and the IdP cert were also provided
163
+ # @return [Boolean] True if the required info is found, otherwise False if soft=True
164
+ # @raise [ValidationError] if soft == false and validation fails
165
+ #
166
+ def valid_state?
167
+ return append_error("Blank logout response") if response.empty?
168
+
169
+ return append_error("No settings on logout response") if settings.nil?
170
+
171
+ return append_error("No issuer in settings of the logout response") if settings.issuer.nil?
172
+
173
+ if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
174
+ return append_error("No fingerprint or certificate on settings of the logout response")
175
+ end
176
+
177
+ true
178
+ end
179
+
180
+ # Validates if a provided :matches_request_id matchs the inResponseTo value.
181
+ # @param soft [String|nil] request_id The ID of the Logout Request sent by this SP to the IdP (if was sent any)
182
+ # @return [Boolean] True if there is no request_id or it match, otherwise False if soft=True
183
+ # @raise [ValidationError] if soft == false and validation fails
184
+ #
185
+ def valid_in_response_to?
186
+ return true unless options.has_key? :matches_request_id
187
+
188
+ unless options[:matches_request_id] == in_response_to
189
+ return append_error("Response does not match the request ID, expected: <#{options[:matches_request_id]}>, but was: <#{in_response_to}>")
190
+ end
191
+
192
+ true
193
+ end
194
+
195
+ # Validates the Issuer of the Logout Response
196
+ # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
197
+ # @raise [ValidationError] if soft == false and validation fails
198
+ #
199
+ def valid_issuer?
200
+ return true if settings.idp_entity_id.nil? || issuer.nil?
201
+
202
+ unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
203
+ return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
204
+ end
205
+ true
206
+ end
207
+
208
+ # Validates the Signature if it exists and the GET parameters are provided
209
+ # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
210
+ # @raise [ValidationError] if soft == false and validation fails
211
+ #
212
+ def validate_signature
213
+ return true unless !options.nil?
214
+ return true unless options.has_key? :get_params
215
+ return true unless options[:get_params].has_key? 'Signature'
216
+ return true if settings.nil? || settings.get_idp_cert.nil?
217
+
218
+ query_string = OneLogin::RubySaml::Utils.build_query(
219
+ :type => 'SAMLResponse',
220
+ :data => options[:get_params]['SAMLResponse'],
221
+ :relay_state => options[:get_params]['RelayState'],
222
+ :sig_alg => options[:get_params]['SigAlg']
223
+ )
224
+
225
+ valid = OneLogin::RubySaml::Utils.verify_signature(
226
+ :cert => settings.get_idp_cert,
227
+ :sig_alg => options[:get_params]['SigAlg'],
228
+ :signature => options[:get_params]['Signature'],
229
+ :query_string => query_string
230
+ )
231
+
232
+ unless valid
233
+ error_msg = "Invalid Signature on Logout Response"
234
+ return append_error(error_msg)
235
+ end
236
+ true
237
+ end
238
+
239
+ end
240
+ end
241
+ end