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
@@ -6,8 +6,12 @@ require 'rexml/document'
6
6
  require 'rexml/xpath'
7
7
  require 'thread'
8
8
 
9
+ # Only supports SAML 2.0
9
10
  module OneLogin
10
11
  module RubySaml
12
+
13
+ # SAML2 Message
14
+ #
11
15
  class SamlMessage
12
16
  include REXML
13
17
 
@@ -16,6 +20,8 @@ module OneLogin
16
20
 
17
21
  BASE64_FORMAT = %r(\A[A-Za-z0-9+/]{4}*[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=?\Z)
18
22
 
23
+ # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
24
+ #
19
25
  def self.schema
20
26
  @schema ||= Mutex.new.synchronize do
21
27
  Dir.chdir(File.expand_path("../../../schemas", __FILE__)) do
@@ -24,29 +30,68 @@ module OneLogin
24
30
  end
25
31
  end
26
32
 
33
+ # @return [String|nil] Gets the Version attribute from the SAML Message if exists.
34
+ #
35
+ def version(document)
36
+ @version ||= begin
37
+ node = REXML::XPath.first(
38
+ document,
39
+ "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
40
+ { "p" => PROTOCOL }
41
+ )
42
+ node.nil? ? nil : node.attributes['Version']
43
+ end
44
+ end
45
+
46
+ # @return [String|nil] Gets the ID attribute from the SAML Message if exists.
47
+ #
48
+ def id(document)
49
+ @id ||= begin
50
+ node = REXML::XPath.first(
51
+ document,
52
+ "/p:AuthnRequest | /p:Response | /p:LogoutResponse | /p:LogoutRequest",
53
+ { "p" => PROTOCOL }
54
+ )
55
+ node.nil? ? nil : node.attributes['ID']
56
+ end
57
+ end
58
+
59
+ # Validates the SAML Message against the specified schema.
60
+ # @param document [REXML::Document] The message that will be validated
61
+ # @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not)
62
+ # @return [Boolean] True if the XML is valid, otherwise False, if soft=True
63
+ # @raise [ValidationError] if soft == false and validation fails
64
+ #
27
65
  def valid_saml?(document, soft = true)
28
- xml = Nokogiri::XML(document.to_s)
66
+ begin
67
+ xml = Nokogiri::XML(document.to_s) do |config|
68
+ config.options = XMLSecurity::BaseDocument::NOKOGIRI_OPTIONS
69
+ end
70
+ rescue Exception => error
71
+ return false if soft
72
+ validation_error("XML load failed: #{error.message}")
73
+ end
29
74
 
30
75
  SamlMessage.schema.validate(xml).map do |error|
31
- break false if soft
76
+ return false if soft
32
77
  validation_error("#{error.message}\n\n#{xml.to_s}")
33
78
  end
34
79
  end
35
80
 
81
+ # Raise a ValidationError with the provided message
82
+ # @param message [String] Message of the exception
83
+ # @raise [ValidationError]
84
+ #
36
85
  def validation_error(message)
37
86
  raise ValidationError.new(message)
38
87
  end
39
88
 
40
89
  private
41
90
 
42
- ##
43
- # Take a SAML object provided by +saml+, determine its status and return
44
- # a decoded XML as a String.
91
+ # Base64 decode and try also to inflate a SAML Message
92
+ # @param saml [String] The deflated and encoded SAML Message
93
+ # @return [String] The plain SAML Message
45
94
  #
46
- # Since SAML decided to use the RFC1951 and therefor has no zlib markers,
47
- # the only reliable method of deciding whether we have a zlib stream or not
48
- # is to try and inflate it and fall back to the base64 decoded string if
49
- # the stream contains errors.
50
95
  def decode_raw_saml(saml)
51
96
  return saml unless base64_encoded?(saml)
52
97
 
@@ -58,32 +103,53 @@ module OneLogin
58
103
  end
59
104
  end
60
105
 
106
+ # Deflate, base64 encode and url-encode a SAML Message (To be used in the HTTP-redirect binding)
107
+ # @param saml [String] The plain SAML Message
108
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
109
+ # @return [String] The deflated and encoded SAML Message (encoded if the compression is requested)
110
+ #
61
111
  def encode_raw_saml(saml, settings)
62
112
  saml = deflate(saml) if settings.compress_request
63
113
 
64
114
  CGI.escape(Base64.encode64(saml))
65
115
  end
66
116
 
67
- def decode(encoded)
68
- Base64.decode64(encoded)
117
+ # Base 64 decode method
118
+ # @param string [String] The string message
119
+ # @return [String] The decoded string
120
+ #
121
+ def decode(string)
122
+ Base64.decode64(string)
69
123
  end
70
124
 
71
- def encode(encoded)
72
- Base64.encode64(encoded).gsub(/\n/, "")
125
+ # Base 64 encode method
126
+ # @param string [String] The string
127
+ # @return [String] The encoded string
128
+ #
129
+ def encode(string)
130
+ Base64.encode64(string).gsub(/\n/, "")
73
131
  end
74
132
 
75
133
  # Check if a string is base64 encoded
76
- #
77
134
  # @param string [String] string to check the encoding of
78
135
  # @return [true, false] whether or not the string is base64 encoded
136
+ #
79
137
  def base64_encoded?(string)
80
138
  !!string.gsub(/[\r\n]|\\r|\\n/, "").match(BASE64_FORMAT)
81
139
  end
82
140
 
141
+ # Inflate method
142
+ # @param deflated [String] The string
143
+ # @return [String] The inflated string
144
+ #
83
145
  def inflate(deflated)
84
146
  Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(deflated)
85
147
  end
86
148
 
149
+ # Deflate method
150
+ # @param inflated [String] The string
151
+ # @return [String] The deflated string
152
+ #
87
153
  def deflate(inflated)
88
154
  Zlib::Deflate.deflate(inflated, 9)[2..-5]
89
155
  end
@@ -2,16 +2,20 @@ require "xml_security"
2
2
  require "onelogin/ruby-saml/attribute_service"
3
3
  require "onelogin/ruby-saml/utils"
4
4
 
5
+ # Only supports SAML 2.0
5
6
  module OneLogin
6
7
  module RubySaml
8
+
9
+ # SAML2 Toolkit Settings
10
+ #
7
11
  class Settings
8
12
  def initialize(overrides = {})
9
13
  config = DEFAULTS.merge(overrides)
10
14
  config.each do |k,v|
11
15
  acc = "#{k.to_s}=".to_sym
12
- if self.respond_to? acc
16
+ if respond_to? acc
13
17
  value = v.is_a?(Hash) ? v.dup : v
14
- self.send(acc, value)
18
+ send(acc, value)
15
19
  end
16
20
  end
17
21
  @attribute_consuming_service = AttributeService.new
@@ -39,18 +43,22 @@ module OneLogin
39
43
  attr_accessor :protocol_binding
40
44
  attr_accessor :attributes_index
41
45
  attr_accessor :force_authn
42
- attr_accessor :security
43
46
  attr_accessor :certificate
44
47
  attr_accessor :private_key
45
48
  attr_accessor :authn_context
46
49
  attr_accessor :authn_context_comparison
47
50
  attr_accessor :authn_context_decl_ref
48
51
  attr_reader :attribute_consuming_service
52
+ # Work-flow
53
+ attr_accessor :security
54
+ attr_accessor :soft
49
55
  # Compability
50
56
  attr_accessor :assertion_consumer_logout_service_url
51
57
  attr_accessor :assertion_consumer_logout_service_binding
52
58
 
53
- def single_logout_service_url()
59
+ # @return [String] Single Logout Service URL.
60
+ #
61
+ def single_logout_service_url
54
62
  val = nil
55
63
  if @single_logout_service_url.nil?
56
64
  if @assertion_consumer_logout_service_url
@@ -62,12 +70,16 @@ module OneLogin
62
70
  val
63
71
  end
64
72
 
65
- # setter
66
- def single_logout_service_url=(val)
67
- @single_logout_service_url = val
73
+ # Setter for the Single Logout Service URL.
74
+ # @param url [String].
75
+ #
76
+ def single_logout_service_url=(url)
77
+ @single_logout_service_url = url
68
78
  end
69
79
 
70
- def single_logout_service_binding()
80
+ # @return [String] Single Logout Service Binding.
81
+ #
82
+ def single_logout_service_binding
71
83
  val = nil
72
84
  if @single_logout_service_binding.nil?
73
85
  if @assertion_consumer_logout_service_binding
@@ -79,27 +91,53 @@ module OneLogin
79
91
  val
80
92
  end
81
93
 
82
- # setter
83
- def single_logout_service_binding=(val)
84
- @single_logout_service_binding = val
94
+ # Setter for Single Logout Service Binding.
95
+ #
96
+ # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
97
+ # @param url [String]
98
+ #
99
+ def single_logout_service_binding=(url)
100
+ @single_logout_service_binding = url
85
101
  end
86
102
 
87
- def get_sp_cert
88
- cert = nil
89
- if self.certificate
90
- formated_cert = OneLogin::RubySaml::Utils.format_cert(self.certificate)
91
- cert = OpenSSL::X509::Certificate.new(formated_cert)
103
+ # Calculates the fingerprint of the IdP x509 certificate.
104
+ # @return [String] The fingerprint
105
+ #
106
+ def get_fingerprint
107
+ idp_cert_fingerprint || begin
108
+ idp_cert = get_idp_cert
109
+ if idp_cert
110
+ fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(idp_cert_fingerprint_algorithm).new
111
+ fingerprint_alg.hexdigest(idp_cert.to_der).upcase.scan(/../).join(":")
112
+ end
92
113
  end
93
- cert
94
114
  end
95
115
 
116
+ # @return [OpenSSL::X509::Certificate|nil] Build the IdP certificate from the settings (previously format it)
117
+ #
118
+ def get_idp_cert
119
+ return nil if idp_cert.nil? || idp_cert.empty?
120
+
121
+ formated_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
122
+ OpenSSL::X509::Certificate.new(formated_cert)
123
+ end
124
+
125
+ # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
126
+ #
127
+ def get_sp_cert
128
+ return nil if certificate.nil? || certificate.empty?
129
+
130
+ formated_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
131
+ OpenSSL::X509::Certificate.new(formated_cert)
132
+ end
133
+
134
+ # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
135
+ #
96
136
  def get_sp_key
97
- private_key = nil
98
- if self.private_key
99
- formated_private_key = OneLogin::RubySaml::Utils.format_private_key(self.private_key)
100
- private_key = OpenSSL::PKey::RSA.new(formated_private_key)
101
- end
102
- private_key
137
+ return nil if private_key.nil? || private_key.empty?
138
+
139
+ formated_private_key = OneLogin::RubySaml::Utils.format_private_key(private_key)
140
+ OpenSSL::PKey::RSA.new(formated_private_key)
103
141
  end
104
142
 
105
143
  private
@@ -110,6 +148,7 @@ module OneLogin
110
148
  :idp_cert_fingerprint_algorithm => XMLSecurity::Document::SHA1,
111
149
  :compress_request => true,
112
150
  :compress_response => true,
151
+ :soft => true,
113
152
  :security => {
114
153
  :authn_requests_signed => false,
115
154
  :logout_requests_signed => false,
@@ -7,59 +7,249 @@ require "onelogin/ruby-saml/saml_message"
7
7
  # Only supports SAML 2.0
8
8
  module OneLogin
9
9
  module RubySaml
10
+
11
+ # SAML2 Logout Request (SLO IdP initiated, Parser)
12
+ #
10
13
  class SloLogoutrequest < SamlMessage
11
- attr_reader :options
12
- attr_reader :request
14
+
15
+ # OneLogin::RubySaml::Settings Toolkit settings
16
+ attr_accessor :settings
17
+
18
+ # Array with the causes [Array of strings]
19
+ attr_accessor :errors
20
+
13
21
  attr_reader :document
22
+ attr_reader :request
23
+ attr_reader :options
14
24
 
25
+ attr_accessor :soft
26
+
27
+ # Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class.
28
+ # @param request [String] A UUEncoded Logout Request from the IdP.
29
+ # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
30
+ # Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with
31
+ #
32
+ # @raise [ArgumentError] If Request is nil
33
+ #
15
34
  def initialize(request, options = {})
35
+ @errors = []
16
36
  raise ArgumentError.new("Request cannot be nil") if request.nil?
17
37
  @options = options
38
+
39
+ @soft = true
40
+ if !options.empty? && !options[:settings].nil?
41
+ @settings = options[:settings]
42
+ if !options[:settings].soft.nil?
43
+ @soft = options[:settings].soft
44
+ end
45
+ end
46
+
18
47
  @request = decode_raw_saml(request)
19
48
  @document = REXML::Document.new(@request)
20
49
  end
21
50
 
22
- def is_valid?
23
- validate
51
+ # Append the cause to the errors array, and based on the value of soft, return false or raise
52
+ # an exception
53
+ def append_error(error_msg)
54
+ @errors << error_msg
55
+ return soft ? false : validation_error(error_msg)
24
56
  end
25
57
 
26
- def validate!
27
- validate(false)
58
+ # Reset the errors array
59
+ def reset_errors!
60
+ @errors = []
28
61
  end
29
62
 
30
- # The value of the user identifier as designated by the initialization request response
63
+ # Validates the Logout Request with the default values (soft = true)
64
+ # @return [Boolean] TRUE if the Logout Request is valid
65
+ #
66
+ def is_valid?
67
+ validate
68
+ end
69
+
70
+ # @return [String] Gets the NameID of the Logout Request.
71
+ #
31
72
  def name_id
32
73
  @name_id ||= begin
33
74
  node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
34
- Utils.element_text(node)
75
+ node.nil? ? nil : node.text
35
76
  end
36
77
  end
37
78
 
79
+ alias_method :nameid, :name_id
80
+
81
+ # @return [String|nil] Gets the ID attribute from the Logout Request. if exists.
82
+ #
38
83
  def id
39
- return @id if @id
40
- element = REXML::XPath.first(document, "/p:LogoutRequest", {
41
- "p" => PROTOCOL} )
42
- return nil if element.nil?
43
- return element.attributes["ID"]
84
+ super(document)
44
85
  end
45
86
 
87
+ # @return [String] Gets the Issuer from the Logout Request.
88
+ #
46
89
  def issuer
47
90
  @issuer ||= begin
48
- node = REXML::XPath.first(document, "/p:LogoutRequest/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
49
- Utils.element_text(node)
91
+ node = REXML::XPath.first(
92
+ document,
93
+ "/p:LogoutRequest/a:Issuer",
94
+ { "p" => PROTOCOL, "a" => ASSERTION }
95
+ )
96
+ node.nil? ? nil : node.text
97
+ end
98
+ end
99
+
100
+ # @return [Time|nil] Gets the NotOnOrAfter Attribute value if exists.
101
+ #
102
+ def not_on_or_after
103
+ @not_on_or_after ||= begin
104
+ node = REXML::XPath.first(
105
+ document,
106
+ "/p:LogoutRequest",
107
+ { "p" => PROTOCOL }
108
+ )
109
+ if node && node.attributes["NotOnOrAfter"]
110
+ Time.parse(node.attributes["NotOnOrAfter"])
111
+ end
50
112
  end
51
113
  end
52
114
 
115
+ # @return [Array] Gets the SessionIndex if exists (Supported multiple values). Empty Array if none found
116
+ #
117
+ def session_indexes
118
+ s_indexes = []
119
+ nodes = REXML::XPath.match(
120
+ document,
121
+ "/p:LogoutRequest/p:SessionIndex",
122
+ { "p" => PROTOCOL }
123
+ )
124
+
125
+ nodes.each do |node|
126
+ s_indexes << node.text
127
+ end
128
+
129
+ s_indexes
130
+ end
131
+
53
132
  private
54
133
 
55
- def validate(soft = true)
56
- valid_saml?(document, soft) && validate_request_state(soft)
134
+ # Hard aux function to validate the Logout Request
135
+ # @return [Boolean] TRUE if the Logout Request is valid
136
+ # @raise [ValidationError] if soft == false and validation fails
137
+ #
138
+ def validate
139
+ reset_errors!
140
+
141
+ validate_request_state &&
142
+ validate_id &&
143
+ validate_version &&
144
+ validate_structure &&
145
+ validate_not_on_or_after &&
146
+ validate_issuer &&
147
+ validate_signature
148
+ end
149
+
150
+ # Validates that the Logout Request contains an ID
151
+ # If fails, the error is added to the errors array.
152
+ # @return [Boolean] True if the Logout Request contains an ID, otherwise returns False
153
+ #
154
+ def validate_id
155
+ unless id
156
+ return append_error("Missing ID attribute on Logout Request")
157
+ end
158
+
159
+ true
160
+ end
161
+
162
+ # Validates the SAML version (2.0)
163
+ # If fails, the error is added to the errors array.
164
+ # @return [Boolean] True if the Logout Request is 2.0, otherwise returns False
165
+ #
166
+ def validate_version
167
+ unless version(document) == "2.0"
168
+ return append_error("Unsupported SAML version")
169
+ end
170
+
171
+ true
172
+ end
173
+
174
+ # Validates the time. (If the logout request was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
175
+ # If fails, the error is added to the errors array
176
+ # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
177
+ # @raise [ValidationError] if soft == false and validation fails
178
+ #
179
+ def validate_not_on_or_after
180
+ now = Time.now.utc
181
+ if not_on_or_after && now >= (not_on_or_after + (options[:allowed_clock_drift] || 0))
182
+ return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after})")
183
+ end
184
+
185
+ true
57
186
  end
58
187
 
59
- def validate_request_state(soft = true)
60
- if request.empty?
61
- return soft ? false : validation_error("Blank request")
188
+ # Validates the Logout Request against the specified schema.
189
+ # @return [Boolean] True if the XML is valid, otherwise False if soft=True
190
+ # @raise [ValidationError] if soft == false and validation fails
191
+ #
192
+ def validate_structure
193
+ unless valid_saml?(document, soft)
194
+ return append_error("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd")
62
195
  end
196
+
197
+ true
198
+ end
199
+
200
+ # Validates that the Logout Request provided in the initialization is not empty,
201
+ # @return [Boolean] True if the required info is found, otherwise False if soft=True
202
+ # @raise [ValidationError] if soft == false and validation fails
203
+ #
204
+ def validate_request_state
205
+ return append_error("Blank logout request") if request.nil? || request.empty?
206
+
207
+ true
208
+ end
209
+
210
+ # Validates the Issuer of the Logout Request
211
+ # If fails, the error is added to the errors array
212
+ # @return [Boolean] True if the Issuer matchs the IdP entityId, otherwise False if soft=True
213
+ # @raise [ValidationError] if soft == false and validation fails
214
+ #
215
+ def validate_issuer
216
+ return true if settings.idp_entity_id.nil? || issuer.nil?
217
+
218
+ unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
219
+ return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
220
+ end
221
+
222
+ true
223
+ end
224
+
225
+ # Validates the Signature if exists and GET parameters are provided
226
+ # @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
227
+ # @raise [ValidationError] if soft == false and validation fails
228
+ #
229
+ def validate_signature
230
+ return true if options.nil?
231
+ return true unless options.has_key? :get_params
232
+ return true unless options[:get_params].has_key? 'Signature'
233
+ return true if settings.nil? || settings.get_idp_cert.nil?
234
+
235
+ query_string = OneLogin::RubySaml::Utils.build_query(
236
+ :type => 'SAMLRequest',
237
+ :data => options[:get_params]['SAMLRequest'],
238
+ :relay_state => options[:get_params]['RelayState'],
239
+ :sig_alg => options[:get_params]['SigAlg']
240
+ )
241
+
242
+ valid = OneLogin::RubySaml::Utils.verify_signature(
243
+ :cert => settings.get_idp_cert,
244
+ :sig_alg => options[:get_params]['SigAlg'],
245
+ :signature => options[:get_params]['Signature'],
246
+ :query_string => query_string
247
+ )
248
+
249
+ unless valid
250
+ return append_error("Invalid Signature on Logout Request")
251
+ end
252
+
63
253
  true
64
254
  end
65
255