ruby-saml 0.9.4 → 1.0.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/LICENSE +1 -1
  4. data/README.md +71 -15
  5. data/changelog.md +15 -6
  6. data/lib/onelogin/ruby-saml.rb +1 -0
  7. data/lib/onelogin/ruby-saml/attribute_service.rb +25 -2
  8. data/lib/onelogin/ruby-saml/attributes.rb +42 -23
  9. data/lib/onelogin/ruby-saml/authrequest.rb +33 -8
  10. data/lib/onelogin/ruby-saml/http_error.rb +7 -0
  11. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +65 -10
  12. data/lib/onelogin/ruby-saml/logging.rb +14 -10
  13. data/lib/onelogin/ruby-saml/logoutrequest.rb +39 -14
  14. data/lib/onelogin/ruby-saml/logoutresponse.rb +166 -39
  15. data/lib/onelogin/ruby-saml/metadata.rb +40 -23
  16. data/lib/onelogin/ruby-saml/response.rb +562 -88
  17. data/lib/onelogin/ruby-saml/saml_message.rb +80 -14
  18. data/lib/onelogin/ruby-saml/settings.rb +62 -23
  19. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +210 -20
  20. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +44 -13
  21. data/lib/onelogin/ruby-saml/utils.rb +163 -40
  22. data/lib/onelogin/ruby-saml/version.rb +1 -1
  23. data/lib/schemas/saml-schema-metadata-2.0.xsd +0 -2
  24. data/lib/xml_security.rb +87 -29
  25. data/ruby-saml.gemspec +1 -0
  26. data/test/certificates/{r1_certificate2_base64 → certificate_without_head_foot} +0 -0
  27. data/test/certificates/formatted_certificate +14 -0
  28. data/test/certificates/formatted_private_key +12 -0
  29. data/test/certificates/formatted_rsa_private_key +12 -0
  30. data/test/certificates/invalid_certificate1 +1 -0
  31. data/test/certificates/invalid_certificate2 +1 -0
  32. data/test/certificates/invalid_certificate3 +12 -0
  33. data/test/certificates/invalid_private_key1 +1 -0
  34. data/test/certificates/invalid_private_key2 +1 -0
  35. data/test/certificates/invalid_private_key3 +10 -0
  36. data/test/certificates/invalid_rsa_private_key1 +1 -0
  37. data/test/certificates/invalid_rsa_private_key2 +1 -0
  38. data/test/certificates/invalid_rsa_private_key3 +10 -0
  39. data/test/idp_metadata_parser_test.rb +41 -4
  40. data/test/logging_test.rb +62 -0
  41. data/test/logout_requests/invalid_slo_request.xml +6 -0
  42. data/test/{responses → logout_requests}/slo_request.xml +0 -0
  43. data/test/logout_requests/slo_request.xml.base64 +1 -0
  44. data/test/logout_requests/slo_request_deflated.xml.base64 +1 -0
  45. data/test/logout_requests/slo_request_with_session_index.xml +5 -0
  46. data/test/{responses → logout_responses}/logoutresponse_fixtures.rb +6 -6
  47. data/test/logoutrequest_test.rb +79 -52
  48. data/test/logoutresponse_test.rb +206 -59
  49. data/test/metadata_test.rb +77 -7
  50. data/test/request_test.rb +80 -65
  51. data/test/response_test.rb +862 -189
  52. data/test/responses/attackxee.xml +13 -0
  53. data/test/responses/invalids/invalid_audience.xml.base64 +1 -0
  54. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +1 -0
  55. data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -0
  56. data/test/responses/invalids/invalid_signature_position.xml.base64 +1 -0
  57. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +1 -0
  58. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +1 -0
  59. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +1 -0
  60. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +1 -0
  61. data/test/responses/invalids/multiple_assertions.xml.base64 +2 -0
  62. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  63. data/test/responses/invalids/no_id.xml.base64 +1 -0
  64. data/test/responses/invalids/no_saml2.xml.base64 +1 -0
  65. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  66. data/test/responses/invalids/no_status.xml.base64 +1 -0
  67. data/test/responses/invalids/no_status_code.xml.base64 +1 -0
  68. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +1 -0
  69. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +1 -0
  70. data/test/responses/invalids/response_encrypted_attrs.xml.base64 +1 -0
  71. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +1 -0
  72. data/test/responses/invalids/status_code_responder.xml.base64 +1 -0
  73. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +1 -0
  74. data/test/responses/{response4.xml.base64 → response_assertion_wrapped.xml.base64} +0 -0
  75. data/test/responses/response_encrypted_nameid.xml.base64 +1 -0
  76. data/test/responses/response_unsigned_xml_base64 +1 -0
  77. data/test/responses/{response5.xml.base64 → response_with_saml2_namespace.xml.base64} +0 -0
  78. data/test/responses/{response3.xml.base64 → response_with_signed_assertion.xml.base64} +0 -0
  79. data/test/responses/{r1_response6.xml.base64 → response_with_signed_assertion_2.xml.base64} +0 -0
  80. data/test/responses/{response1.xml.base64 → response_with_undefined_recipient.xml.base64} +0 -0
  81. data/test/responses/{response2.xml.base64 → response_without_attributes.xml.base64} +0 -0
  82. data/test/responses/{wrapped_response_2.xml.base64 → response_wrapped.xml.base64} +0 -0
  83. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +1 -0
  84. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  85. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +1 -0
  86. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +1 -0
  87. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +1 -0
  88. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +1 -0
  89. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +1 -0
  90. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +1 -0
  91. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +1 -0
  92. data/test/responses/valid_response.xml.base64 +1 -0
  93. data/test/saml_message_test.rb +56 -0
  94. data/test/settings_test.rb +138 -1
  95. data/test/slo_logoutrequest_test.rb +239 -28
  96. data/test/slo_logoutresponse_test.rb +93 -71
  97. data/test/test_helper.rb +138 -31
  98. data/test/utils_test.rb +129 -25
  99. data/test/xml_security_test.rb +140 -71
  100. metadata +142 -25
  101. data/test/responses/response_node_text_attack.xml.base64 +0 -1
@@ -0,0 +1,7 @@
1
+ module OneLogin
2
+ module RubySaml
3
+ class HttpError < StandardError
4
+ end
5
+ end
6
+ end
7
+
@@ -7,22 +7,35 @@ require "net/https"
7
7
  require "rexml/document"
8
8
  require "rexml/xpath"
9
9
 
10
+ # Only supports SAML 2.0
10
11
  module OneLogin
11
12
  module RubySaml
12
13
  include REXML
13
14
 
15
+ # Auxiliary class to retrieve and parse the Identity Provider Metadata
16
+ #
14
17
  class IdpMetadataParser
15
18
 
16
19
  METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
17
20
  DSIG = "http://www.w3.org/2000/09/xmldsig#"
18
21
 
19
22
  attr_reader :document
20
-
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)
21
31
  def parse_remote(url, validate_cert = true)
22
32
  idp_metadata = get_idp_metadata(url, validate_cert)
23
33
  parse(idp_metadata)
24
34
  end
25
35
 
36
+ # Parse the Identity Provider metadata and update the settings with the IdP values
37
+ # @param idp_metadata [String]
38
+ #
26
39
  def parse(idp_metadata)
27
40
  @document = REXML::Document.new(idp_metadata)
28
41
 
@@ -37,8 +50,11 @@ module OneLogin
37
50
 
38
51
  private
39
52
 
40
- # Retrieve the remote IdP metadata from the URL or a cached copy
41
- # # returns a REXML document of the metadata
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
42
58
  def get_idp_metadata(url, validate_cert)
43
59
  uri = URI.parse(url)
44
60
  if uri.scheme == "http"
@@ -62,37 +78,76 @@ module OneLogin
62
78
  get = Net::HTTP::Get.new(uri.request_uri)
63
79
  response = http.request(get)
64
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")
65
87
  end
88
+
66
89
  meta_text
67
90
  end
68
91
 
92
+ # @return [String|nil] IdP Entity ID value if exists
93
+ #
69
94
  def idp_entity_id
70
- node = REXML::XPath.first(document, "/md:EntityDescriptor/@entityID", { "md" => METADATA })
95
+ node = REXML::XPath.first(
96
+ document,
97
+ "/md:EntityDescriptor/@entityID",
98
+ { "md" => METADATA }
99
+ )
71
100
  node.value if node
72
101
  end
73
102
 
103
+ # @return [String|nil] IdP Name ID Format value if exists
104
+ #
74
105
  def idp_name_id_format
75
- node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat", { "md" => METADATA })
76
- Utils.element_text(node)
106
+ node = REXML::XPath.first(
107
+ document,
108
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
109
+ { "md" => METADATA }
110
+ )
111
+ node.text if node
77
112
  end
78
113
 
114
+ # @return [String|nil] SingleSignOnService endpoint if exists
115
+ #
79
116
  def single_signon_service_url
80
- node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location", { "md" => METADATA })
117
+ node = REXML::XPath.first(
118
+ document,
119
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location",
120
+ { "md" => METADATA }
121
+ )
81
122
  node.value if node
82
123
  end
83
124
 
125
+ # @return [String|nil] SingleLogoutService endpoint if exists
126
+ #
84
127
  def single_logout_service_url
85
- node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location", { "md" => METADATA })
128
+ node = REXML::XPath.first(
129
+ document,
130
+ "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location",
131
+ { "md" => METADATA }
132
+ )
86
133
  node.value if node
87
134
  end
88
135
 
136
+ # @return [String|nil] X509Certificate if exists
137
+ #
89
138
  def certificate
90
139
  @certificate ||= begin
91
- node = REXML::XPath.first(document, "/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate", { "md" => METADATA, "ds" => DSIG })
92
- Base64.decode64(Utils.element_text(node)) if node
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
93
146
  end
94
147
  end
95
148
 
149
+ # @return [String|nil] the SHA-1 fingerpint of the X509Certificate if it exists
150
+ #
96
151
  def fingerprint
97
152
  @fingerprint ||= begin
98
153
  if certificate
@@ -1,25 +1,29 @@
1
+ require 'logger'
2
+
1
3
  # Simplistic log class when we're running in Rails
2
4
  module OneLogin
3
5
  module RubySaml
4
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
+
5
17
  def self.debug(message)
6
18
  return if !!ENV["ruby-saml/testing"]
7
19
 
8
- if defined? Rails
9
- Rails.logger.debug message
10
- else
11
- puts message
12
- end
20
+ logger.debug message
13
21
  end
14
22
 
15
23
  def self.info(message)
16
24
  return if !!ENV["ruby-saml/testing"]
17
25
 
18
- if defined? Rails
19
- Rails.logger.info message
20
- else
21
- puts message
22
- end
26
+ logger.info message
23
27
  end
24
28
  end
25
29
  end
@@ -3,16 +3,29 @@ require "uuid"
3
3
  require "onelogin/ruby-saml/logging"
4
4
  require "onelogin/ruby-saml/saml_message"
5
5
 
6
+ # Only supports SAML 2.0
6
7
  module OneLogin
7
8
  module RubySaml
9
+
10
+ # SAML2 Logout Request (SLO SP initiated, Builder)
11
+ #
8
12
  class Logoutrequest < SamlMessage
9
13
 
10
- attr_reader :uuid # Can be obtained if neccessary
14
+ # Logout Request ID
15
+ attr_reader :uuid
11
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
+ #
12
20
  def initialize
13
21
  @uuid = "_" + UUID.new.generate
14
22
  end
15
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
+ #
16
29
  def create(settings, params={})
17
30
  params = create_params(settings, params)
18
31
  params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
@@ -24,6 +37,11 @@ module OneLogin
24
37
  @logout_url = settings.idp_slo_target_url + request_params
25
38
  end
26
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
+ #
27
45
  def create_params(settings, params={})
28
46
  # The method expects :RelayState but sometimes we get 'RelayState' instead.
29
47
  # Based on the HashWithIndifferentAccess value in Rails we could experience
@@ -44,11 +62,14 @@ module OneLogin
44
62
 
45
63
  if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
46
64
  params['SigAlg'] = settings.security[:signature_method]
47
- url_string = "SAMLRequest=#{CGI.escape(base64_request)}"
48
- url_string << "&RelayState=#{CGI.escape(relay_state)}" if relay_state
49
- url_string << "&SigAlg=#{CGI.escape(params['SigAlg'])}"
50
- private_key = settings.get_sp_key()
51
- signature = private_key.sign(XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method]).new, url_string)
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)
52
73
  params['Signature'] = encode(signature)
53
74
  end
54
75
 
@@ -59,6 +80,10 @@ module OneLogin
59
80
  request_params
60
81
  end
61
82
 
83
+ # Creates the SAMLRequest String.
84
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
85
+ # @return [String] The SAMLRequest String.
86
+ #
62
87
  def create_logout_request_xml_doc(settings)
63
88
  time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
64
89
 
@@ -76,15 +101,15 @@ module OneLogin
76
101
  issuer.text = settings.issuer
77
102
  end
78
103
 
79
- name_id = root.add_element "saml:NameID"
104
+ nameid = root.add_element "saml:NameID"
80
105
  if settings.name_identifier_value
81
- name_id.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
82
- name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
83
- name_id.text = 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
84
109
  else
85
110
  # If no NameID is present in the settings we generate one
86
- name_id.text = "_" + UUID.new.generate
87
- name_id.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
111
+ nameid.text = "_" + UUID.new.generate
112
+ nameid.attributes['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
88
113
  end
89
114
 
90
115
  if settings.sessionindex
@@ -94,8 +119,8 @@ module OneLogin
94
119
 
95
120
  # embed signature
96
121
  if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
97
- private_key = settings.get_sp_key()
98
- cert = settings.get_sp_cert()
122
+ private_key = settings.get_sp_key
123
+ cert = settings.get_sp_cert
99
124
  request_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
100
125
  end
101
126
 
@@ -3,65 +3,101 @@ require "onelogin/ruby-saml/saml_message"
3
3
 
4
4
  require "time"
5
5
 
6
+ # Only supports SAML 2.0
6
7
  module OneLogin
7
8
  module RubySaml
9
+
10
+ # SAML2 Logout Response (SLO IdP initiated, Parser)
11
+ #
8
12
  class Logoutresponse < SamlMessage
9
- # For API compability, this is mutable.
13
+
14
+ # OneLogin::RubySaml::Settings Toolkit settings
10
15
  attr_accessor :settings
11
16
 
17
+ # Array with the causes
18
+ attr_accessor :errors
19
+
12
20
  attr_reader :document
13
21
  attr_reader :response
14
22
  attr_reader :options
15
23
 
16
- #
17
- # In order to validate that the response matches a given request, append
18
- # the option:
19
- # :matches_request_id => REQUEST_ID
20
- #
21
- # It will validate that the logout response matches the ID of the request.
22
- # You can also do this yourself through the in_response_to accessor.
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
23
33
  #
24
34
  def initialize(response, settings = nil, options = {})
35
+ @errors = []
25
36
  raise ArgumentError.new("Logoutresponse cannot be nil") if response.nil?
26
- self.settings = settings
37
+ @settings = settings
38
+
39
+ if settings.nil? || settings.soft.nil?
40
+ @soft = true
41
+ else
42
+ @soft = settings.soft
43
+ end
27
44
 
28
45
  @options = options
29
46
  @response = decode_raw_saml(response)
30
47
  @document = XMLSecurity::SignedDocument.new(@response)
31
48
  end
32
49
 
33
- def validate!
34
- validate(false)
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)
35
55
  end
36
56
 
37
- def validate(soft = true)
38
- return false unless valid_saml?(document, soft) && valid_state?(soft)
39
-
40
- valid_in_response_to?(soft) && valid_issuer?(soft) && success?(soft)
57
+ # Reset the errors array
58
+ def reset_errors!
59
+ @errors = []
41
60
  end
42
61
 
43
- def success?(soft = true)
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?
44
67
  unless status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
45
- return soft ? false : validation_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code> ")
68
+ return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>")
46
69
  end
47
70
  true
48
71
  end
49
72
 
73
+ # @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
74
+ #
50
75
  def in_response_to
51
76
  @in_response_to ||= begin
52
- node = REXML::XPath.first(document, "/p:LogoutResponse", { "p" => PROTOCOL, "a" => ASSERTION })
77
+ node = REXML::XPath.first(
78
+ document,
79
+ "/p:LogoutResponse",
80
+ { "p" => PROTOCOL, "a" => ASSERTION }
81
+ )
53
82
  node.nil? ? nil : node.attributes['InResponseTo']
54
83
  end
55
84
  end
56
85
 
86
+ # @return [String] Gets the Issuer from the Logout Response.
87
+ #
57
88
  def issuer
58
89
  @issuer ||= begin
59
- node = REXML::XPath.first(document, "/p:LogoutResponse/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
60
- node ||= REXML::XPath.first(document, "/p:LogoutResponse/a:Assertion/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
61
- Utils.element_text(node)
90
+ node = REXML::XPath.first(
91
+ document,
92
+ "/p:LogoutResponse/a:Issuer",
93
+ { "p" => PROTOCOL, "a" => ASSERTION }
94
+ )
95
+ node.nil? ? nil : node.text
62
96
  end
63
97
  end
64
98
 
99
+ # @return [String] Gets the StatusCode from a Logout Response.
100
+ #
65
101
  def status_code
66
102
  @status_code ||= begin
67
103
  node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL, "a" => ASSERTION })
@@ -69,46 +105,137 @@ module OneLogin
69
105
  end
70
106
  end
71
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
+
72
134
  private
73
135
 
74
- def valid_state?(soft = true)
75
- if response.empty?
76
- return soft ? false : validation_error("Blank response")
77
- end
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?
78
143
 
79
- if settings.nil?
80
- return soft ? false : validation_error("No settings on response")
81
- end
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
82
148
 
83
- if settings.issuer.nil?
84
- return soft ? false : validation_error("No issuer in settings")
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")
85
156
  end
86
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
+
87
173
  if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
88
- return soft ? false : validation_error("No fingerprint or certificate on settings")
174
+ return append_error("No fingerprint or certificate on settings of the logout response")
89
175
  end
90
176
 
91
177
  true
92
178
  end
93
179
 
94
- def valid_in_response_to?(soft = true)
95
- return true unless self.options.has_key? :matches_request_id
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
96
187
 
97
- unless self.options[:matches_request_id] == in_response_to
98
- return soft ? false : validation_error("Response does not match the request ID, expected: <#{self.options[:matches_request_id]}>, but was: <#{in_response_to}>")
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}>")
99
190
  end
100
191
 
101
192
  true
102
193
  end
103
194
 
104
- def valid_issuer?(soft = true)
105
- return true if self.settings.idp_entity_id.nil? or self.issuer.nil?
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?
106
201
 
107
- unless URI.parse(self.issuer) == URI.parse(self.settings.idp_entity_id)
108
- return soft ? false : validation_error("Doesn't match the issuer, expected: <#{self.settings.issuer}>, but was: <#{issuer}>")
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}>")
109
204
  end
110
205
  true
111
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
+
112
239
  end
113
240
  end
114
241
  end