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
@@ -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