ruby-saml 0.8.9 → 0.8.14

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 (42) hide show
  1. data/Gemfile +11 -1
  2. data/Rakefile +0 -14
  3. data/lib/onelogin/ruby-saml/authrequest.rb +84 -18
  4. data/lib/onelogin/ruby-saml/logoutrequest.rb +93 -18
  5. data/lib/onelogin/ruby-saml/logoutresponse.rb +1 -24
  6. data/lib/onelogin/ruby-saml/response.rb +206 -11
  7. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  8. data/lib/onelogin/ruby-saml/settings.rb +73 -12
  9. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +158 -0
  10. data/lib/onelogin/ruby-saml/utils.rb +169 -0
  11. data/lib/onelogin/ruby-saml/version.rb +1 -1
  12. data/lib/ruby-saml.rb +2 -1
  13. data/lib/xml_security.rb +332 -78
  14. data/test/certificates/ruby-saml-2.crt +15 -0
  15. data/test/certificates/ruby-saml.crt +14 -0
  16. data/test/certificates/ruby-saml.key +15 -0
  17. data/test/logoutrequest_test.rb +177 -44
  18. data/test/logoutresponse_test.rb +23 -28
  19. data/test/request_test.rb +100 -37
  20. data/test/response_test.rb +337 -129
  21. data/test/responses/adfs_response_xmlns.xml +45 -0
  22. data/test/responses/encrypted_new_attack.xml.base64 +1 -0
  23. data/test/responses/invalids/multiple_signed.xml.base64 +1 -0
  24. data/test/responses/invalids/no_signature.xml.base64 +1 -0
  25. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
  26. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
  27. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +1 -0
  28. data/test/responses/response_with_concealed_signed_assertion.xml +51 -0
  29. data/test/responses/response_with_doubled_signed_assertion.xml +49 -0
  30. data/test/responses/response_with_signed_assertion_3.xml +30 -0
  31. data/test/responses/response_with_signed_message_and_assertion.xml +34 -0
  32. data/test/responses/response_with_undefined_recipient.xml.base64 +1 -0
  33. data/test/responses/response_wrapped.xml.base64 +150 -0
  34. data/test/responses/valid_response.xml.base64 +1 -0
  35. data/test/responses/valid_response_without_x509certificate.xml.base64 +1 -0
  36. data/test/settings_test.rb +5 -5
  37. data/test/slo_logoutresponse_test.rb +226 -0
  38. data/test/test_helper.rb +117 -12
  39. data/test/utils_test.rb +10 -10
  40. data/test/xml_security_test.rb +354 -68
  41. metadata +64 -18
  42. checksums.yaml +0 -7
data/Gemfile CHANGED
@@ -5,9 +5,19 @@ source 'http://rubygems.org'
5
5
 
6
6
  gemspec
7
7
 
8
+ if RUBY_VERSION < '1.9'
9
+ gem 'nokogiri', '~> 1.5.0'
10
+ gem 'minitest', '~> 5.5', '<= 5.11.3'
11
+ elsif RUBY_VERSION < '2.1'
12
+ gem 'nokogiri', '>= 1.5.0', '<= 1.6.8.1'
13
+ gem 'minitest', '~> 5.5'
14
+ else
15
+ gem 'nokogiri', '>= 1.5.0'
16
+ gem 'minitest', '~> 5.5'
17
+ end
18
+
8
19
  group :test do
9
20
  if RUBY_VERSION < '1.9'
10
- gem 'nokogiri', '~> 1.5.0'
11
21
  gem 'ruby-debug', '~> 0.10.4'
12
22
  elsif RUBY_VERSION < '2.0'
13
23
  gem 'debugger-linecache', '~> 1.2.0'
data/Rakefile CHANGED
@@ -25,17 +25,3 @@ end
25
25
  task :test
26
26
 
27
27
  task :default => :test
28
-
29
- # require 'rake/rdoctask'
30
- # Rake::RDocTask.new do |rdoc|
31
- # if File.exist?('VERSION')
32
- # version = File.read('VERSION')
33
- # else
34
- # version = ""
35
- # end
36
-
37
- # rdoc.rdoc_dir = 'rdoc'
38
- # rdoc.title = "ruby-saml #{version}"
39
- # rdoc.rdoc_files.include('README*')
40
- # rdoc.rdoc_files.include('lib/**/*.rb')
41
- #end
@@ -1,16 +1,50 @@
1
1
  require "base64"
2
- require "uuid"
3
2
  require "zlib"
4
3
  require "cgi"
5
- require "rexml/document"
6
- require "rexml/xpath"
4
+ require "onelogin/ruby-saml/utils"
5
+ require "onelogin/ruby-saml/setting_error"
7
6
 
8
7
  module OneLogin
9
8
  module RubySaml
10
- include REXML
9
+
11
10
  class Authrequest
11
+ # AuthNRequest ID
12
+ attr_reader :uuid
13
+
14
+ # Initializes the AuthNRequest. An Authrequest Object.
15
+ # Asigns an ID, a random uuid.
16
+ #
17
+ def initialize
18
+ @uuid = OneLogin::RubySaml::Utils.uuid
19
+ end
20
+
12
21
  def create(settings, params = {})
13
- params = {} if params.nil?
22
+ params = create_params(settings, params)
23
+ params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
24
+ saml_request = CGI.escape(params.delete("SAMLRequest"))
25
+ request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
26
+ params.each_pair do |key, value|
27
+ request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
28
+ end
29
+ raise SettingError.new "Invalid settings, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
30
+ @login_url = settings.idp_sso_target_url + request_params
31
+ end
32
+
33
+ # Creates the Get parameters for the request.
34
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
35
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
36
+ # @return [Hash] Parameters
37
+ #
38
+ def create_params(settings, params={})
39
+ # The method expects :RelayState but sometimes we get 'RelayState' instead.
40
+ # Based on the HashWithIndifferentAccess value in Rails we could experience
41
+ # conflicts so this line will solve them.
42
+ relay_state = params[:RelayState] || params['RelayState']
43
+
44
+ if relay_state.nil?
45
+ params.delete(:RelayState)
46
+ params.delete('RelayState')
47
+ end
14
48
 
15
49
  request_doc = create_authentication_xml_doc(settings)
16
50
  request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
@@ -20,34 +54,55 @@ module OneLogin
20
54
 
21
55
  Logging.debug "Created AuthnRequest: #{request}"
22
56
 
23
- request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
57
+ request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
24
58
  if Base64.respond_to?('strict_encode64')
25
- base64_request = Base64.strict_encode64(request)
59
+ base64_request = Base64.strict_encode64(request)
26
60
  else
27
- base64_request = Base64.encode64(request).gsub(/\n/, "")
61
+ base64_request = Base64.encode64(request).gsub(/\n/, "")
62
+ end
63
+
64
+ request_params = {"SAMLRequest" => base64_request}
65
+
66
+ if settings.security[:authn_requests_signed] && !settings.security[:embed_sign] && settings.private_key
67
+ params['SigAlg'] = settings.security[:signature_method]
68
+ url_string = OneLogin::RubySaml::Utils.build_query(
69
+ :type => 'SAMLRequest',
70
+ :data => base64_request,
71
+ :relay_state => relay_state,
72
+ :sig_alg => params['SigAlg']
73
+ )
74
+ sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
75
+ signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
76
+ if Base64.respond_to?('strict_encode64')
77
+ params['Signature'] = Base64.strict_encode64(signature)
78
+ else
79
+ params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
80
+ end
28
81
  end
29
- encoded_request = CGI.escape(base64_request)
30
- params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
31
- request_params = "#{params_prefix}SAMLRequest=#{encoded_request}"
32
82
 
33
83
  params.each_pair do |key, value|
34
- request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
84
+ request_params[key] = value.to_s
35
85
  end
36
86
 
37
- settings.idp_sso_target_url + request_params
87
+ request_params
38
88
  end
39
89
 
40
90
  def create_authentication_xml_doc(settings)
41
- uuid = "_" + UUID.new.generate
42
- time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
43
- # Create AuthnRequest root element using REXML
44
- request_doc = REXML::Document.new
91
+ document = create_xml_document(settings)
92
+ sign_document(document, settings)
93
+ end
94
+
95
+ def create_xml_document(settings)
96
+ time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
97
+
98
+ request_doc = XMLSecurity::Document.new
99
+ request_doc.uuid = uuid
45
100
 
46
101
  root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
47
102
  root.attributes['ID'] = uuid
48
103
  root.attributes['IssueInstant'] = time
49
104
  root.attributes['Version'] = "2.0"
50
- root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil?
105
+ root.attributes['Destination'] = settings.idp_sso_target_url unless settings.idp_sso_target_url.nil? or settings.idp_sso_target_url.empty?
51
106
  root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
52
107
  root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
53
108
  root.attributes['ForceAuthn'] = settings.force_authn unless settings.force_authn.nil?
@@ -97,6 +152,17 @@ module OneLogin
97
152
  request_doc
98
153
  end
99
154
 
155
+ def sign_document(document, settings)
156
+ # embed signature
157
+ if settings.security[:authn_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
158
+ private_key = settings.get_sp_key
159
+ cert = settings.get_sp_cert
160
+ document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
161
+ end
162
+
163
+ document
164
+ end
165
+
100
166
  end
101
167
  end
102
168
  end
@@ -1,51 +1,111 @@
1
1
  require "base64"
2
- require "uuid"
3
2
  require "zlib"
4
3
  require "cgi"
4
+ require 'rexml/document'
5
+ require "onelogin/ruby-saml/utils"
6
+ require "onelogin/ruby-saml/setting_error"
5
7
 
6
8
  module OneLogin
7
9
  module RubySaml
8
- include REXML
10
+
9
11
  class Logoutrequest
10
12
 
11
13
  attr_reader :uuid # Can be obtained if neccessary
12
14
 
13
15
  def initialize
14
- @uuid = "_" + UUID.new.generate
16
+ @uuid = OneLogin::RubySaml::Utils.uuid
15
17
  end
16
18
 
17
19
  def create(settings, params={})
18
- request_doc = create_unauth_xml_doc(settings, params)
20
+ params = create_params(settings, params)
21
+ params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
22
+ saml_request = CGI.escape(params.delete("SAMLRequest"))
23
+ request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
24
+ params.each_pair do |key, value|
25
+ request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
26
+ end
27
+ raise SettingError.new "Invalid settings, idp_slo_target_url is not set!" if settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
28
+ @logout_url = settings.idp_slo_target_url + request_params
29
+ end
30
+
31
+ # Creates the Get parameters for the logout request.
32
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
33
+ # @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
34
+ # @return [Hash] Parameters
35
+ #
36
+ def create_params(settings, params={})
37
+ # The method expects :RelayState but sometimes we get 'RelayState' instead.
38
+ # Based on the HashWithIndifferentAccess value in Rails we could experience
39
+ # conflicts so this line will solve them.
40
+ relay_state = params[:RelayState] || params['RelayState']
41
+
42
+ if relay_state.nil?
43
+ params.delete(:RelayState)
44
+ params.delete('RelayState')
45
+ end
46
+
47
+ request_doc = create_logout_request_xml_doc(settings)
48
+ request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
49
+
19
50
  request = ""
20
51
  request_doc.write(request)
21
52
 
22
- deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5]
53
+ Logging.debug "Created SLO Logout Request: #{request}"
54
+
55
+ request = Zlib::Deflate.deflate(request, 9)[2..-5] if settings.compress_request
23
56
  if Base64.respond_to?('strict_encode64')
24
- base64_request = Base64.strict_encode64(deflated_request)
57
+ base64_request = Base64.strict_encode64(request)
25
58
  else
26
- base64_request = Base64.encode64(deflated_request).gsub(/\n/, "")
59
+ base64_request = Base64.encode64(request).gsub(/\n/, "")
27
60
  end
28
- encoded_request = CGI.escape(base64_request)
61
+ request_params = {"SAMLRequest" => base64_request}
29
62
 
30
- params_prefix = (settings.idp_slo_target_url =~ /\?/) ? '&' : '?'
31
- request_params = "#{params_prefix}SAMLRequest=#{encoded_request}"
63
+ if settings.security[:logout_requests_signed] && !settings.security[:embed_sign] && settings.private_key
64
+ params['SigAlg'] = settings.security[:signature_method]
65
+ url_string = OneLogin::RubySaml::Utils.build_query(
66
+ :type => 'SAMLRequest',
67
+ :data => base64_request,
68
+ :relay_state => relay_state,
69
+ :sig_alg => params['SigAlg']
70
+ )
71
+ sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
72
+ signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
73
+ if Base64.respond_to?('strict_encode64')
74
+ params['Signature'] = Base64.strict_encode64(signature)
75
+ else
76
+ params['Signature'] = Base64.encode64(signature).gsub(/\n/, "")
77
+ end
78
+ end
32
79
 
33
80
  params.each_pair do |key, value|
34
- request_params << "&#{key}=#{CGI.escape(value.to_s)}"
81
+ request_params[key] = value.to_s
35
82
  end
36
83
 
37
- @logout_url = settings.idp_slo_target_url + request_params
84
+ request_params
38
85
  end
39
86
 
40
- def create_unauth_xml_doc(settings, params)
87
+ # Creates the SAMLRequest String.
88
+ # @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
89
+ # @return [String] The SAMLRequest String.
90
+ #
91
+ def create_logout_request_xml_doc(settings)
92
+ document = create_xml_document(settings)
93
+ sign_document(document, settings)
94
+ end
95
+
96
+ def create_xml_document(settings, request_doc=nil)
97
+ time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
41
98
 
42
- time = Time.new().strftime("%Y-%m-%dT%H:%M:%S")
99
+ if request_doc.nil?
100
+ request_doc = XMLSecurity::Document.new
101
+ request_doc.uuid = uuid
102
+ end
43
103
 
44
- request_doc = REXML::Document.new
45
104
  root = request_doc.add_element "samlp:LogoutRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
46
- root.attributes['ID'] = @uuid
105
+ root.attributes['ID'] = uuid
47
106
  root.attributes['IssueInstant'] = time
48
107
  root.attributes['Version'] = "2.0"
108
+ root.attributes['Destination'] = settings.idp_slo_target_url unless settings.idp_slo_target_url.nil? or settings.idp_slo_target_url.empty?
49
109
 
50
110
  if settings.sp_entity_id
51
111
  issuer = root.add_element "saml:Issuer", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
@@ -57,8 +117,6 @@ module OneLogin
57
117
  name_id.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
58
118
  name_id.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
59
119
  name_id.text = settings.name_identifier_value
60
- else
61
- raise ValidationError.new("Missing required name identifier")
62
120
  end
63
121
 
64
122
  if settings.sessionindex
@@ -81,6 +139,23 @@ module OneLogin
81
139
  end
82
140
  request_doc
83
141
  end
142
+
143
+ def sign_document(document, settings)
144
+ # embed signature
145
+ if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
146
+ private_key = settings.get_sp_key
147
+ cert = settings.get_sp_cert
148
+ document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
149
+ end
150
+
151
+ document
152
+ end
153
+
154
+ # Leave due compatibility
155
+ def create_unauth_xml_doc(settings, params)
156
+ request_doc = ReXML::Document.new
157
+ create_xml_document(settings, request_doc)
158
+ end
84
159
  end
85
160
  end
86
161
  end
@@ -1,7 +1,5 @@
1
1
  require "xml_security"
2
2
  require "time"
3
- require "base64"
4
- require "zlib"
5
3
 
6
4
  module OneLogin
7
5
  module RubySaml
@@ -30,7 +28,7 @@ module OneLogin
30
28
  self.settings = settings
31
29
 
32
30
  @options = options
33
- @response = decode_raw_response(response)
31
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
34
32
  @document = XMLSecurity::SignedDocument.new(response)
35
33
  end
36
34
 
@@ -75,27 +73,6 @@ module OneLogin
75
73
 
76
74
  private
77
75
 
78
- def decode(encoded)
79
- Base64.decode64(encoded)
80
- end
81
-
82
- def inflate(deflated)
83
- zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
84
- zlib.inflate(deflated)
85
- end
86
-
87
- def decode_raw_response(response)
88
- if response =~ /^</
89
- return response
90
- elsif (decoded = decode(response)) =~ /^</
91
- return decoded
92
- elsif (inflated = inflate(decoded)) =~ /^</
93
- return inflated
94
- end
95
-
96
- raise "Couldn't decode SAMLResponse"
97
- end
98
-
99
76
  def valid_saml?(soft = true)
100
77
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
101
78
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -1,6 +1,7 @@
1
1
  require "xml_security"
2
2
  require "time"
3
3
  require "nokogiri"
4
+ require "onelogin/ruby-saml/utils"
4
5
  require 'onelogin/ruby-saml/attributes'
5
6
 
6
7
  # Only supports SAML 2.0
@@ -22,7 +23,7 @@ module OneLogin
22
23
  def initialize(response, options = {})
23
24
  raise ArgumentError.new("Response cannot be nil") if response.nil?
24
25
  @options = options
25
- @response = (response =~ /^</) ? response : Base64.decode64(response)
26
+ @response = OneLogin::RubySaml::Utils.decode_raw_saml(response)
26
27
  @document = XMLSecurity::SignedDocument.new(@response)
27
28
  end
28
29
 
@@ -42,6 +43,8 @@ module OneLogin
42
43
  end
43
44
  end
44
45
 
46
+ alias nameid name_id
47
+
45
48
  def sessionindex
46
49
  @sessionindex ||= begin
47
50
  node = xpath_first_from_signed_assertion('/a:AuthnStatement')
@@ -147,14 +150,165 @@ module OneLogin
147
150
  end
148
151
 
149
152
  def validate(soft = true)
150
- validate_structure(soft) &&
151
- validate_response_state(soft) &&
152
- validate_conditions(soft) &&
153
- validate_audience(soft) &&
154
- document.validate_document(get_fingerprint, soft) &&
153
+ validate_structure(soft) &&
154
+ validate_success_status(soft) &&
155
+ validate_num_assertion &&
156
+ validate_signed_elements(soft) &&
157
+ validate_response_state(soft) &&
158
+ validate_conditions(soft) &&
159
+ validate_audience(soft) &&
160
+ validate_signature(soft) &&
155
161
  success?
156
162
  end
157
163
 
164
+ # Validates that the SAML Response only contains a single Assertion (encrypted or not).
165
+ # @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
166
+ #
167
+ def validate_num_assertion(soft = true)
168
+ assertions = REXML::XPath.match(
169
+ document,
170
+ "//a:Assertion",
171
+ { "a" => ASSERTION }
172
+ )
173
+ encrypted_assertions = REXML::XPath.match(
174
+ document,
175
+ "//a:EncryptedAssertion",
176
+ { "a" => ASSERTION }
177
+ )
178
+
179
+ unless assertions.size + encrypted_assertions.size == 1
180
+ return soft ? false : validation_error("SAML Response must contain 1 assertion")
181
+ end
182
+
183
+ true
184
+ end
185
+
186
+ # Validates the Signed elements
187
+ # @return [Boolean] True if there is 1 or 2 Elements signed in the SAML Response
188
+ # an are a Response or an Assertion Element, otherwise False if soft=True
189
+ #
190
+ def validate_signed_elements(soft)
191
+ signature_nodes = REXML::XPath.match(
192
+ document,
193
+ "//ds:Signature",
194
+ {"ds"=>DSIG}
195
+ )
196
+ signed_elements = []
197
+ verified_seis = []
198
+ verified_ids = []
199
+ signature_nodes.each do |signature_node|
200
+ signed_element = signature_node.parent.name
201
+ if signed_element != 'Response' && signed_element != 'Assertion'
202
+ return soft ? false : validation_error("Invalid Signature Element '#{signed_element}'. SAML Response rejected")
203
+ end
204
+
205
+ if signature_node.parent.attributes['ID'].nil?
206
+ return soft ? false : validation_error("Signed Element must contain an ID. SAML Response rejected")
207
+ end
208
+
209
+ id = signature_node.parent.attributes.get_attribute("ID").value
210
+ if verified_ids.include?(id)
211
+ return soft ? false : validation_error("Duplicated ID. SAML Response rejected")
212
+ end
213
+ verified_ids.push(id)
214
+
215
+ # Check that reference URI matches the parent ID and no duplicate References or IDs
216
+ ref = REXML::XPath.first(signature_node, ".//ds:Reference", {"ds"=>DSIG})
217
+ if ref
218
+ uri = ref.attributes.get_attribute("URI")
219
+ if uri && !uri.value.empty?
220
+ sei = uri.value[1..-1]
221
+
222
+ unless sei == id
223
+ return soft ? false : validation_error("Found an invalid Signed Element. SAML Response rejected")
224
+ end
225
+
226
+ if verified_seis.include?(sei)
227
+ return soft ? false : validation_error("Duplicated Reference URI. SAML Response rejected")
228
+ end
229
+
230
+ verified_seis.push(sei)
231
+ end
232
+ end
233
+
234
+ signed_elements << signed_element
235
+ end
236
+
237
+ unless signature_nodes.length < 3 && !signed_elements.empty?
238
+ return soft ? false : validation_error("Found an unexpected number of Signature Element. SAML Response rejected")
239
+ end
240
+
241
+ true
242
+ end
243
+
244
+ # Validates the Status of the SAML Response
245
+ # @return [Boolean] True if the SAML Response contains a Success code, otherwise False if soft == false
246
+ # @raise [ValidationError] if soft == false and validation fails
247
+ #
248
+ def validate_success_status(soft = true)
249
+ return true if success?
250
+
251
+ return false unless soft
252
+
253
+ error_msg = 'The status code of the Response was not Success'
254
+ status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
255
+ return validation_error(status_error_msg)
256
+ end
257
+
258
+ # Checks if the Status has the "Success" code
259
+ # @return [Boolean] True if the StatusCode is Sucess
260
+ #
261
+ def success?
262
+ status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
263
+ end
264
+
265
+ # @return [String] StatusCode value from a SAML Response.
266
+ #
267
+ def status_code
268
+ @status_code ||= begin
269
+ nodes = REXML::XPath.match(
270
+ document,
271
+ "/p:Response/p:Status/p:StatusCode",
272
+ { "p" => PROTOCOL }
273
+ )
274
+ if nodes.size == 1
275
+ node = nodes[0]
276
+ code = node.attributes["Value"] if node && node.attributes
277
+
278
+ unless code == "urn:oasis:names:tc:SAML:2.0:status:Success"
279
+ nodes = REXML::XPath.match(
280
+ document,
281
+ "/p:Response/p:Status/p:StatusCode/p:StatusCode",
282
+ { "p" => PROTOCOL }
283
+ )
284
+ statuses = nodes.collect do |inner_node|
285
+ inner_node.attributes["Value"]
286
+ end
287
+ extra_code = statuses.join(" | ")
288
+ if extra_code
289
+ code = "#{code} | #{extra_code}"
290
+ end
291
+ end
292
+ code
293
+ end
294
+ end
295
+ end
296
+
297
+ # @return [String] the StatusMessage value from a SAML Response.
298
+ #
299
+ def status_message
300
+ @status_message ||= begin
301
+ nodes = REXML::XPath.match(
302
+ document,
303
+ "/p:Response/p:Status/p:StatusMessage",
304
+ { "p" => PROTOCOL }
305
+ )
306
+ if nodes.size == 1
307
+ Utils.element_text(nodes.first)
308
+ end
309
+ end
310
+ end
311
+
158
312
  def validate_structure(soft = true)
159
313
  Dir.chdir(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'schemas'))) do
160
314
  @schema = Nokogiri::XML::Schema(IO.read('saml20protocol_schema.xsd'))
@@ -245,17 +399,57 @@ module OneLogin
245
399
  true
246
400
  end
247
401
 
402
+ def validate_signature(soft = true)
403
+ error_msg = "Invalid Signature on SAML Response"
404
+
405
+ sig_elements = REXML::XPath.match(
406
+ document,
407
+ "/p:Response[@ID=$id]/ds:Signature]",
408
+ { "p" => PROTOCOL, "ds" => DSIG },
409
+ { 'id' => document.signed_element_id }
410
+ )
411
+
412
+ # Check signature nodes
413
+ if sig_elements.nil? || sig_elements.size == 0
414
+ sig_elements = REXML::XPath.match(
415
+ document,
416
+ "/p:Response/a:Assertion[@ID=$id]/ds:Signature",
417
+ {"p" => PROTOCOL, "a" => ASSERTION, "ds"=>DSIG},
418
+ { 'id' => document.signed_element_id }
419
+ )
420
+ end
421
+
422
+ if sig_elements.size != 1
423
+ if sig_elements.size == 0
424
+ error_msg += ". Signed element id ##{doc.signed_element_id} is not found"
425
+ else
426
+ error_msg += ". Signed element id ##{doc.signed_element_id} is found more than once"
427
+ end
428
+ return soft ? false : validation_error(error_msg)
429
+ end
430
+
431
+ opts = {}
432
+ opts[:fingerprint_alg] = OpenSSL::Digest::SHA1.new
433
+ opts[:cert] = settings.idp_cert
434
+ fingerprint = get_fingerprint
435
+
436
+ unless fingerprint
437
+ return soft ? false : validation_error("No fingerprint or certificate on settings")
438
+ end
439
+
440
+ unless document.validate_document(fingerprint, soft, opts)
441
+ return soft ? false : validation_error(error_msg)
442
+ end
443
+
444
+ true
445
+ end
446
+
248
447
  def parse_time(node, attribute)
249
448
  if node && node.attributes[attribute]
250
449
  Time.parse(node.attributes[attribute])
251
450
  end
252
451
  end
253
452
 
254
- # Validates the Audience, (If the Audience match the Service Provider EntityID)
255
- # If fails, the error is added to the errors array
256
- # @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
257
- # @raise [ValidationError] if soft == false and validation fails
258
- #
259
453
  def validate_audience(soft = true)
260
454
  return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
261
455
 
@@ -267,6 +461,7 @@ module OneLogin
267
461
 
268
462
  true
269
463
  end
464
+
270
465
  end
271
466
  end
272
467
  end