r-saml 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +14 -0
  4. data/.travis.yml +23 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +19 -0
  7. data/README.md +584 -0
  8. data/Rakefile +27 -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 +165 -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 +735 -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 +368 -0
  43. data/r-saml.gemspec +64 -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 +1159 -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_without_reference_uri.xml.base64 +1 -0
  117. data/test/responses/response_wrapped.xml.base64 +150 -0
  118. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
  119. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  120. data/test/responses/signed_nameid_in_atts.xml +47 -0
  121. data/test/responses/signed_unqual_nameid_in_atts.xml +47 -0
  122. data/test/responses/simple_saml_php.xml +71 -0
  123. data/test/responses/starfield_response.xml.base64 +1 -0
  124. data/test/responses/test_sign.xml +43 -0
  125. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
  126. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
  127. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
  128. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
  129. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
  130. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
  131. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  132. data/test/responses/valid_response.xml.base64 +1 -0
  133. data/test/saml_message_test.rb +56 -0
  134. data/test/settings_test.rb +218 -0
  135. data/test/slo_logoutrequest_test.rb +275 -0
  136. data/test/slo_logoutresponse_test.rb +185 -0
  137. data/test/test_helper.rb +257 -0
  138. data/test/utils_test.rb +145 -0
  139. data/test/xml_security_test.rb +328 -0
  140. metadata +421 -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