ruby-saml 0.8.9 → 0.8.14

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.

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