ruby-saml 0.9.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of ruby-saml might be problematic. Click here for more details.

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