ruby-saml 1.4.2 → 1.4.3

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.

@@ -102,7 +102,8 @@ module OneLogin
102
102
 
103
103
  nameid = root.add_element "saml:NameID"
104
104
  if settings.name_identifier_value
105
- nameid.attributes['NameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
105
+ nameid.attributes['NameQualifier'] = settings.idp_name_qualifier if settings.idp_name_qualifier
106
+ nameid.attributes['SPNameQualifier'] = settings.sp_name_qualifier if settings.sp_name_qualifier
106
107
  nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
107
108
  nameid.text = settings.name_identifier_value
108
109
  else
@@ -27,6 +27,8 @@ module OneLogin
27
27
  # @param options [Hash] Extra parameters.
28
28
  # :matches_request_id It will validate that the logout response matches the ID of the request.
29
29
  # :get_params GET Parameters, including the SAMLResponse
30
+ # :relax_signature_validation to accept signatures if no idp certificate registered on settings
31
+ #
30
32
  # @raise [ArgumentError] if response is nil
31
33
  #
32
34
  def initialize(response, settings = nil, options = {})
@@ -208,8 +210,14 @@ module OneLogin
208
210
  return true unless !options.nil?
209
211
  return true unless options.has_key? :get_params
210
212
  return true unless options[:get_params].has_key? 'Signature'
211
- return true if settings.nil? || settings.get_idp_cert.nil?
212
-
213
+
214
+ idp_cert = settings.get_idp_cert
215
+ idp_certs = settings.get_idp_cert_multi
216
+
217
+ if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?)
218
+ return options.has_key? :relax_signature_validation
219
+ end
220
+
213
221
  query_string = OneLogin::RubySaml::Utils.build_query(
214
222
  :type => 'SAMLResponse',
215
223
  :data => options[:get_params]['SAMLResponse'],
@@ -217,12 +225,27 @@ module OneLogin
217
225
  :sig_alg => options[:get_params]['SigAlg']
218
226
  )
219
227
 
220
- valid = OneLogin::RubySaml::Utils.verify_signature(
221
- :cert => settings.get_idp_cert,
222
- :sig_alg => options[:get_params]['SigAlg'],
223
- :signature => options[:get_params]['Signature'],
224
- :query_string => query_string
225
- )
228
+ if idp_certs.nil? || idp_certs[:signing].empty?
229
+ valid = OneLogin::RubySaml::Utils.verify_signature(
230
+ :cert => settings.get_idp_cert,
231
+ :sig_alg => options[:get_params]['SigAlg'],
232
+ :signature => options[:get_params]['Signature'],
233
+ :query_string => query_string
234
+ )
235
+ else
236
+ valid = false
237
+ idp_certs[:signing].each do |idp_cert|
238
+ valid = OneLogin::RubySaml::Utils.verify_signature(
239
+ :cert => idp_cert,
240
+ :sig_alg => options[:get_params]['SigAlg'],
241
+ :signature => options[:get_params]['Signature'],
242
+ :query_string => query_string
243
+ )
244
+ if valid
245
+ break
246
+ end
247
+ end
248
+ end
226
249
 
227
250
  unless valid
228
251
  error_msg = "Invalid Signature on Logout Response"
@@ -33,21 +33,26 @@ module OneLogin
33
33
  }
34
34
 
35
35
  # Add KeyDescriptor if messages will be signed / encrypted
36
+ # with SP certificate, and new SP certificate if any
36
37
  cert = settings.get_sp_cert
37
- if cert
38
- cert_text = Base64.encode64(cert.to_der).gsub("\n", '')
39
- kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
40
- ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
41
- xd = ki.add_element "ds:X509Data"
42
- xc = xd.add_element "ds:X509Certificate"
43
- xc.text = cert_text
38
+ cert_new = settings.get_sp_cert_new
44
39
 
45
- if settings.security[:want_assertions_encrypted]
46
- kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
47
- ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
48
- xd2 = ki2.add_element "ds:X509Data"
49
- xc2 = xd2.add_element "ds:X509Certificate"
50
- xc2.text = cert_text
40
+ for sp_cert in [cert, cert_new]
41
+ if sp_cert
42
+ cert_text = Base64.encode64(sp_cert.to_der).gsub("\n", '')
43
+ kd = sp_sso.add_element "md:KeyDescriptor", { "use" => "signing" }
44
+ ki = kd.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
45
+ xd = ki.add_element "ds:X509Data"
46
+ xc = xd.add_element "ds:X509Certificate"
47
+ xc.text = cert_text
48
+
49
+ if settings.security[:want_assertions_encrypted]
50
+ kd2 = sp_sso.add_element "md:KeyDescriptor", { "use" => "encryption" }
51
+ ki2 = kd2.add_element "ds:KeyInfo", {"xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"}
52
+ xd2 = ki2.add_element "ds:X509Data"
53
+ xc2 = xd2.add_element "ds:X509Certificate"
54
+ xc2.text = cert_text
55
+ end
51
56
  end
52
57
  end
53
58
 
@@ -88,7 +93,8 @@ module OneLogin
88
93
  sp_req_attr = sp_acs.add_element "md:RequestedAttribute", {
89
94
  "NameFormat" => attribute[:name_format],
90
95
  "Name" => attribute[:name],
91
- "FriendlyName" => attribute[:friendly_name]
96
+ "FriendlyName" => attribute[:friendly_name],
97
+ "isRequired" => attribute[:is_required] || false
92
98
  }
93
99
  unless attribute[:attribute_value].nil?
94
100
  Array(attribute[:attribute_value]).each do |value|
@@ -32,7 +32,7 @@ module OneLogin
32
32
 
33
33
  # Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class.
34
34
  # @param response [String] A UUEncoded SAML response from the IdP.
35
- # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
35
+ # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
36
36
  # Or some options for the response validation process like skip the conditions validation
37
37
  # with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift
38
38
  # or :matches_request_id that will validate that the response matches the ID of the request,
@@ -41,6 +41,7 @@ module OneLogin
41
41
  raise ArgumentError.new("Response cannot be nil") if response.nil?
42
42
 
43
43
  @errors = []
44
+
44
45
  @options = options
45
46
  @soft = true
46
47
  unless options[:settings].nil?
@@ -189,7 +190,7 @@ module OneLogin
189
190
 
190
191
  # Checks if the Status has the "Success" code
191
192
  # @return [Boolean] True if the StatusCode is Sucess
192
- #
193
+ #
193
194
  def success?
194
195
  status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
195
196
  end
@@ -376,7 +377,7 @@ module OneLogin
376
377
  #
377
378
  def validate_success_status
378
379
  return true if success?
379
-
380
+
380
381
  error_msg = 'The status code of the Response was not Success'
381
382
  status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
382
383
  append_error(status_error_msg)
@@ -384,7 +385,7 @@ module OneLogin
384
385
 
385
386
  # Validates the SAML Response against the specified schema.
386
387
  # @return [Boolean] True if the XML is valid, otherwise False if soft=True
387
- # @raise [ValidationError] if soft == false and validation fails
388
+ # @raise [ValidationError] if soft == false and validation fails
388
389
  #
389
390
  def validate_structure
390
391
  structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
@@ -417,7 +418,7 @@ module OneLogin
417
418
  true
418
419
  end
419
420
 
420
- # Validates that the SAML Response contains an ID
421
+ # Validates that the SAML Response contains an ID
421
422
  # If fails, the error is added to the errors array.
422
423
  # @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
423
424
  #
@@ -706,8 +707,9 @@ module OneLogin
706
707
  end
707
708
 
708
709
  # Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option,
709
- # timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the
710
+ # timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the
710
711
  # :skip_subject_confirmation option, this validation is skipped)
712
+ # There is also an optional Recipient check
711
713
  # If fails, the error is added to the errors array
712
714
  # @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True
713
715
  # @raise [ValidationError] if soft == false and validation fails
@@ -717,7 +719,7 @@ module OneLogin
717
719
  valid_subject_confirmation = false
718
720
 
719
721
  subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
720
-
722
+
721
723
  now = Time.now.utc
722
724
  subject_confirmation_nodes.each do |subject_confirmation|
723
725
  if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
@@ -735,8 +737,9 @@ module OneLogin
735
737
  attrs = confirmation_data_node.attributes
736
738
  next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
737
739
  (attrs.include? "NotOnOrAfter" and (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift) <= now) ||
738
- (attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift))
739
-
740
+ (attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift)) ||
741
+ (attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
742
+
740
743
  valid_subject_confirmation = true
741
744
  break
742
745
  end
@@ -803,13 +806,27 @@ module OneLogin
803
806
  return append_error(error_msg)
804
807
  end
805
808
 
806
- opts = {}
807
- opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
808
- opts[:cert] = settings.get_idp_cert
809
- fingerprint = settings.get_fingerprint
809
+ idp_certs = settings.get_idp_cert_multi
810
+ if idp_certs.nil? || idp_certs[:signing].empty?
811
+ opts = {}
812
+ opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
813
+ opts[:cert] = settings.get_idp_cert
814
+ fingerprint = settings.get_fingerprint
810
815
 
811
- unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
812
- return append_error(error_msg)
816
+ unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
817
+ return append_error(error_msg)
818
+ end
819
+ else
820
+ valid = false
821
+ idp_certs[:signing].each do |idp_cert|
822
+ valid = doc.validate_document_with_cert(idp_cert)
823
+ if valid
824
+ break
825
+ end
826
+ end
827
+ unless valid
828
+ return append_error(error_msg)
829
+ end
813
830
  end
814
831
 
815
832
  true
@@ -19,8 +19,7 @@ module OneLogin
19
19
  ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
20
20
  PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
21
21
 
22
- BASE64_FORMAT = %r(\A[A-Za-z0-9+/]{4}*[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=?\Z)
23
-
22
+ BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
24
23
  @@mutex = Mutex.new
25
24
 
26
25
  # @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
@@ -28,7 +28,9 @@ module OneLogin
28
28
  attr_accessor :idp_cert
29
29
  attr_accessor :idp_cert_fingerprint
30
30
  attr_accessor :idp_cert_fingerprint_algorithm
31
+ attr_accessor :idp_cert_multi
31
32
  attr_accessor :idp_attribute_names
33
+ attr_accessor :idp_name_qualifier
32
34
  # SP Data
33
35
  attr_accessor :issuer
34
36
  attr_accessor :assertion_consumer_service_url
@@ -45,6 +47,7 @@ module OneLogin
45
47
  attr_accessor :attributes_index
46
48
  attr_accessor :force_authn
47
49
  attr_accessor :certificate
50
+ attr_accessor :certificate_new
48
51
  attr_accessor :private_key
49
52
  attr_accessor :authn_context
50
53
  attr_accessor :authn_context_comparison
@@ -93,7 +96,7 @@ module OneLogin
93
96
  end
94
97
 
95
98
  # Setter for Single Logout Service Binding.
96
- #
99
+ #
97
100
  # (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
98
101
  # @param url [String]
99
102
  #
@@ -123,6 +126,32 @@ module OneLogin
123
126
  OpenSSL::X509::Certificate.new(formatted_cert)
124
127
  end
125
128
 
129
+ # @return [Hash with 2 arrays of OpenSSL::X509::Certificate] Build multiple IdP certificates from the settings.
130
+ #
131
+ def get_idp_cert_multi
132
+ return nil if idp_cert_multi.nil? || idp_cert_multi.empty?
133
+
134
+ raise ArgumentError.new("Invalid value for idp_cert_multi") if not idp_cert_multi.is_a?(Hash)
135
+
136
+ certs = {:signing => [], :encryption => [] }
137
+
138
+ if idp_cert_multi.key?(:signing) and not idp_cert_multi[:signing].empty?
139
+ idp_cert_multi[:signing].each do |idp_cert|
140
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
141
+ certs[:signing].push(OpenSSL::X509::Certificate.new(formatted_cert))
142
+ end
143
+ end
144
+
145
+ if idp_cert_multi.key?(:encryption) and not idp_cert_multi[:encryption].empty?
146
+ idp_cert_multi[:encryption].each do |idp_cert|
147
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(idp_cert)
148
+ certs[:encryption].push(OpenSSL::X509::Certificate.new(formatted_cert))
149
+ end
150
+ end
151
+
152
+ certs
153
+ end
154
+
126
155
  # @return [OpenSSL::X509::Certificate|nil] Build the SP certificate from the settings (previously format it)
127
156
  #
128
157
  def get_sp_cert
@@ -132,6 +161,15 @@ module OneLogin
132
161
  OpenSSL::X509::Certificate.new(formatted_cert)
133
162
  end
134
163
 
164
+ # @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
165
+ #
166
+ def get_sp_cert_new
167
+ return nil if certificate_new.nil? || certificate_new.empty?
168
+
169
+ formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate_new)
170
+ OpenSSL::X509::Certificate.new(formatted_cert)
171
+ end
172
+
135
173
  # @return [OpenSSL::PKey::RSA] Build the SP private from the settings (previously format it)
136
174
  #
137
175
  def get_sp_key
@@ -26,6 +26,7 @@ module OneLogin
26
26
  # @param request [String] A UUEncoded Logout Request from the IdP.
27
27
  # @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
28
28
  # Or :allowed_clock_drift for the logout request validation process to allow a clock drift when checking dates with
29
+ # Or :relax_signature_validation to accept signatures if no idp certificate registered on settings
29
30
  #
30
31
  # @raise [ArgumentError] If Request is nil
31
32
  #
@@ -227,7 +228,13 @@ module OneLogin
227
228
  return true if options.nil?
228
229
  return true unless options.has_key? :get_params
229
230
  return true unless options[:get_params].has_key? 'Signature'
230
- return true if settings.get_idp_cert.nil?
231
+
232
+ idp_cert = settings.get_idp_cert
233
+ idp_certs = settings.get_idp_cert_multi
234
+
235
+ if idp_cert.nil? && (idp_certs.nil? || idp_certs[:signing].empty?)
236
+ return options.has_key? :relax_signature_validation
237
+ end
231
238
 
232
239
  query_string = OneLogin::RubySaml::Utils.build_query(
233
240
  :type => 'SAMLRequest',
@@ -236,12 +243,27 @@ module OneLogin
236
243
  :sig_alg => options[:get_params]['SigAlg']
237
244
  )
238
245
 
239
- valid = OneLogin::RubySaml::Utils.verify_signature(
240
- :cert => settings.get_idp_cert,
241
- :sig_alg => options[:get_params]['SigAlg'],
242
- :signature => options[:get_params]['Signature'],
243
- :query_string => query_string
244
- )
246
+ if idp_certs.nil? || idp_certs[:signing].empty?
247
+ valid = OneLogin::RubySaml::Utils.verify_signature(
248
+ :cert => settings.get_idp_cert,
249
+ :sig_alg => options[:get_params]['SigAlg'],
250
+ :signature => options[:get_params]['Signature'],
251
+ :query_string => query_string
252
+ )
253
+ else
254
+ valid = false
255
+ idp_certs[:signing].each do |idp_cert|
256
+ valid = OneLogin::RubySaml::Utils.verify_signature(
257
+ :cert => idp_cert,
258
+ :sig_alg => options[:get_params]['SigAlg'],
259
+ :signature => options[:get_params]['Signature'],
260
+ :query_string => query_string
261
+ )
262
+ if valid
263
+ break
264
+ end
265
+ end
266
+ end
245
267
 
246
268
  unless valid
247
269
  return append_error("Invalid Signature on Logout Request")
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.4.2'
3
+ VERSION = '1.4.3'
4
4
  end
5
5
  end
@@ -188,7 +188,7 @@
188
188
  <complexType name="X509IssuerSerialType">
189
189
  <sequence>
190
190
  <element name="X509IssuerName" type="string"/>
191
- <element name="X509SerialNumber" type="integer"/>
191
+ <element name="X509SerialNumber" type="string"/>
192
192
  </sequence>
193
193
  </complexType>
194
194
 
@@ -240,6 +240,31 @@ module XMLSecurity
240
240
  validate_signature(base64_cert, soft)
241
241
  end
242
242
 
243
+ def validate_document_with_cert(idp_cert)
244
+ # get cert from response
245
+ cert_element = REXML::XPath.first(
246
+ self,
247
+ "//ds:X509Certificate",
248
+ { "ds"=>DSIG }
249
+ )
250
+
251
+ if cert_element
252
+ base64_cert = cert_element.text
253
+ cert_text = Base64.decode64(base64_cert)
254
+ begin
255
+ cert = OpenSSL::X509::Certificate.new(cert_text)
256
+ rescue OpenSSL::X509::CertificateError => e
257
+ return append_error("Certificate Error", soft)
258
+ end
259
+
260
+ # check saml response cert matches provided idp cert
261
+ if idp_cert.to_pem != cert.to_pem
262
+ return false
263
+ end
264
+ validate_signature(base64_cert, true)
265
+ end
266
+ end
267
+
243
268
  def validate_signature(base64_cert, soft = true)
244
269
 
245
270
  document = Nokogiri::XML(self.to_s) do |config|
@@ -0,0 +1,15 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQ
3
+ MA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhh
4
+ bXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYD
5
+ VQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIG
6
+ A1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhP
7
+ S4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6s
8
+ ZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIp
9
+ Hc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQW
10
+ BBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7Ns
11
+ CF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3
12
+ kh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWt
13
+ p/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIf
14
+ dtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/
15
+ -----END CERTIFICATE-----
@@ -21,27 +21,26 @@ class IdpMetadataParserTest < Minitest::Test
21
21
  it "extract settings details from xml" do
22
22
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
23
23
 
24
- settings = idp_metadata_parser.parse(idp_metadata)
24
+ settings = idp_metadata_parser.parse(idp_metadata_descriptor)
25
25
 
26
- assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
27
- assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
26
+ assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
27
+ assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
28
28
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
29
- assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
29
+ assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
30
30
  assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
31
31
  assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
32
- assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
33
32
  end
34
33
 
35
34
  it "extract certificate from md:KeyDescriptor[@use='signing']" do
36
35
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
37
- idp_metadata = read_response("idp_descriptor.xml")
36
+ idp_metadata = idp_metadata_descriptor
38
37
  settings = idp_metadata_parser.parse(idp_metadata)
39
38
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
40
39
  end
41
40
 
42
41
  it "extract certificate from md:KeyDescriptor[@use='encryption']" do
43
42
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
44
- idp_metadata = read_response("idp_descriptor.xml")
43
+ idp_metadata = idp_metadata_descriptor
45
44
  idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
46
45
  settings = idp_metadata_parser.parse(idp_metadata)
47
46
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
@@ -49,16 +48,40 @@ class IdpMetadataParserTest < Minitest::Test
49
48
 
50
49
  it "extract certificate from md:KeyDescriptor" do
51
50
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
52
- idp_metadata = read_response("idp_descriptor.xml")
51
+ idp_metadata = idp_metadata_descriptor
53
52
  idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
54
53
  idp_metadata = idp_metadata.sub('<md:KeyDescriptor use="encryption">', '<md:KeyDescriptor>')
55
54
  settings = idp_metadata_parser.parse(idp_metadata)
56
55
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
57
56
  end
58
57
 
58
+ it "extract SSO endpoint with no specific binding, it takes the first" do
59
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
60
+ idp_metadata = idp_metadata_descriptor3
61
+ settings = idp_metadata_parser.parse(idp_metadata)
62
+ assert_equal "https://idp.example.com/idp/profile/Shibboleth/SSO", settings.idp_sso_target_url
63
+ end
64
+
65
+ it "extract SSO endpoint with specific binding" do
66
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
67
+ idp_metadata = idp_metadata_descriptor3
68
+ options = {}
69
+ options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']
70
+ settings = idp_metadata_parser.parse(idp_metadata, options)
71
+ assert_equal "https://idp.example.com/idp/profile/SAML2/POST/SSO", settings.idp_sso_target_url
72
+
73
+ options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
74
+ settings = idp_metadata_parser.parse(idp_metadata, options)
75
+ assert_equal "https://idp.example.com/idp/profile/SAML2/Redirect/SSO", settings.idp_sso_target_url
76
+
77
+ options[:sso_binding] = ['invalid_binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
78
+ settings = idp_metadata_parser.parse(idp_metadata, options)
79
+ assert_equal "https://idp.example.com/idp/profile/SAML2/Redirect/SSO", settings.idp_sso_target_url
80
+ end
81
+
59
82
  it "uses settings options as hash for overrides" do
60
83
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
61
- idp_metadata = read_response("idp_descriptor.xml")
84
+ idp_metadata = idp_metadata_descriptor
62
85
  settings = idp_metadata_parser.parse(idp_metadata, {
63
86
  :settings => {
64
87
  :security => {
@@ -72,12 +95,124 @@ class IdpMetadataParserTest < Minitest::Test
72
95
  assert_equal XMLSecurity::Document::RSA_SHA256, settings.security[:signature_method]
73
96
  end
74
97
 
98
+ it "merges results into given settings object" do
99
+ settings = OneLogin::RubySaml::Settings.new(:security => {
100
+ :digest_method => XMLSecurity::Document::SHA256,
101
+ :signature_method => XMLSecurity::Document::RSA_SHA256
102
+ })
103
+
104
+ OneLogin::RubySaml::IdpMetadataParser.new.parse(idp_metadata_descriptor, :settings => settings)
105
+
106
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
107
+ assert_equal XMLSecurity::Document::SHA256, settings.security[:digest_method]
108
+ assert_equal XMLSecurity::Document::RSA_SHA256, settings.security[:signature_method]
109
+ end
110
+ end
111
+
112
+ describe "parsing an IdP descriptor file into an Hash" do
113
+ it "extract settings details from xml" do
114
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
115
+
116
+ metadata = idp_metadata_parser.parse_to_hash(idp_metadata_descriptor)
117
+
118
+ assert_equal "https://hello.example.com/access/saml/idp.xml", metadata[:idp_entity_id]
119
+ assert_equal "https://hello.example.com/access/saml/login", metadata[:idp_sso_target_url]
120
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", metadata[:idp_cert_fingerprint]
121
+ assert_equal "https://hello.example.com/access/saml/logout", metadata[:idp_slo_target_url]
122
+ assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", metadata[:name_identifier_format]
123
+ assert_equal ["AuthToken", "SSOStartPage"], metadata[:idp_attribute_names]
124
+ end
125
+
126
+ it "extract certificate from md:KeyDescriptor[@use='signing']" do
127
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
128
+ idp_metadata = idp_metadata_descriptor
129
+ metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
130
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", metadata[:idp_cert_fingerprint]
131
+ end
132
+
133
+ it "extract certificate from md:KeyDescriptor[@use='encryption']" do
134
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
135
+ idp_metadata = idp_metadata_descriptor
136
+ idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
137
+ parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
138
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
139
+ end
140
+
141
+ it "extract certificate from md:KeyDescriptor" do
142
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
143
+ idp_metadata = idp_metadata_descriptor
144
+ idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
145
+ idp_metadata = idp_metadata.sub('<md:KeyDescriptor use="encryption">', '<md:KeyDescriptor>')
146
+ parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
147
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
148
+ end
149
+
150
+ it "extract SSO endpoint with no specific binding, it takes the first" do
151
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
152
+ idp_metadata = idp_metadata_descriptor3
153
+ metadata = idp_metadata_parser.parse_to_hash(idp_metadata)
154
+ assert_equal "https://idp.example.com/idp/profile/Shibboleth/SSO", metadata[:idp_sso_target_url]
155
+ end
156
+
157
+ it "extract SSO endpoint with specific binding" do
158
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
159
+ idp_metadata = idp_metadata_descriptor3
160
+ options = {}
161
+ options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST']
162
+ parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, options)
163
+ assert_equal "https://idp.example.com/idp/profile/SAML2/POST/SSO", parsed_metadata[:idp_sso_target_url]
164
+
165
+ options[:sso_binding] = ['urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
166
+ parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, options)
167
+ assert_equal "https://idp.example.com/idp/profile/SAML2/Redirect/SSO", parsed_metadata[:idp_sso_target_url]
168
+
169
+ options[:sso_binding] = ['invalid_binding', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']
170
+ parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, options)
171
+ assert_equal "https://idp.example.com/idp/profile/SAML2/Redirect/SSO", parsed_metadata[:idp_sso_target_url]
172
+ end
173
+
174
+ it "ignores a given :settings hash" do
175
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
176
+ idp_metadata = idp_metadata_descriptor
177
+ parsed_metadata = idp_metadata_parser.parse_to_hash(idp_metadata, {
178
+ :settings => {
179
+ :security => {
180
+ :digest_method => XMLSecurity::Document::SHA256,
181
+ :signature_method => XMLSecurity::Document::RSA_SHA256
182
+ }
183
+ }
184
+ })
185
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
186
+ assert_nil parsed_metadata[:security]
187
+ end
188
+ end
189
+
190
+ describe "parsing an IdP descriptor file with multiple signing certs" do
191
+ it "extract settings details from xml" do
192
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
193
+
194
+ settings = idp_metadata_parser.parse(idp_metadata_descriptor2)
195
+
196
+ assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
197
+ assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
198
+ assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
199
+ assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
200
+ assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
201
+
202
+ assert_nil settings.idp_cert_fingerprint
203
+ assert_nil settings.idp_cert
204
+ assert_equal 2, settings.idp_cert_multi.size
205
+ assert settings.idp_cert_multi.key?("signing")
206
+ assert_equal 2, settings.idp_cert_multi["signing"].size
207
+ assert settings.idp_cert_multi.key?("encryption")
208
+ assert_equal 1, settings.idp_cert_multi["encryption"].size
209
+ end
75
210
  end
76
211
 
77
212
  describe "download and parse IdP descriptor file" do
78
213
  before do
79
214
  mock_response = MockSuccessResponse.new
80
- mock_response.body = idp_metadata
215
+ mock_response.body = idp_metadata_descriptor
81
216
  @url = "https://example.com"
82
217
  uri = URI(@url)
83
218
 
@@ -90,10 +225,10 @@ class IdpMetadataParserTest < Minitest::Test
90
225
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
91
226
  settings = idp_metadata_parser.parse_remote(@url)
92
227
 
93
- assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
94
- assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
228
+ assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
229
+ assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
95
230
  assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
96
- assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
231
+ assert_equal "https://hello.example.com/access/saml/logout", settings.idp_slo_target_url
97
232
  assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
98
233
  assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
99
234
  assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
@@ -107,6 +242,39 @@ class IdpMetadataParserTest < Minitest::Test
107
242
  end
108
243
  end
109
244
 
245
+ describe "download and parse IdP descriptor file into an Hash" do
246
+ before do
247
+ mock_response = MockSuccessResponse.new
248
+ mock_response.body = idp_metadata_descriptor
249
+ @url = "https://example.com"
250
+ uri = URI(@url)
251
+
252
+ @http = Net::HTTP.new(uri.host, uri.port)
253
+ Net::HTTP.expects(:new).returns(@http)
254
+ @http.expects(:request).returns(mock_response)
255
+ end
256
+
257
+ it "extract settings from remote xml" do
258
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
259
+ parsed_metadata = idp_metadata_parser.parse_remote_to_hash(@url)
260
+
261
+ assert_equal "https://hello.example.com/access/saml/idp.xml", parsed_metadata[:idp_entity_id]
262
+ assert_equal "https://hello.example.com/access/saml/login", parsed_metadata[:idp_sso_target_url]
263
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", parsed_metadata[:idp_cert_fingerprint]
264
+ assert_equal "https://hello.example.com/access/saml/logout", parsed_metadata[:idp_slo_target_url]
265
+ assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", parsed_metadata[:name_identifier_format]
266
+ assert_equal ["AuthToken", "SSOStartPage"], parsed_metadata[:idp_attribute_names]
267
+ assert_equal OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
268
+ end
269
+
270
+ it "accept self signed certificate if insturcted" do
271
+ idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
272
+ idp_metadata_parser.parse_remote_to_hash(@url, false)
273
+
274
+ assert_equal OpenSSL::SSL::VERIFY_NONE, @http.verify_mode
275
+ end
276
+ end
277
+
110
278
  describe "download failure cases" do
111
279
  it "raises an exception when the url has no scheme" do
112
280
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
@@ -130,10 +298,38 @@ class IdpMetadataParserTest < Minitest::Test
130
298
  idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
131
299
 
132
300
  exception = assert_raises(OneLogin::RubySaml::HttpError) do
133
- idp_metadata_parser.parse_remote("https://example.hello.com/access/saml/idp.xml")
301
+ idp_metadata_parser.parse_remote("https://hello.example.com/access/saml/idp.xml")
134
302
  end
135
303
 
136
- assert_equal("Failed to fetch idp metadata", exception.message)
304
+ assert_match("Failed to fetch idp metadata", exception.message)
305
+ end
306
+ end
307
+
308
+ describe "parsing metadata with many entity descriptors" do
309
+ before do
310
+ @idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
311
+ @idp_metadata = idp_metadata_multiple_descriptors
312
+ @settings = @idp_metadata_parser.parse(@idp_metadata)
313
+ end
314
+
315
+ it "should find first descriptor" do
316
+ assert_equal "https://foo.example.com/access/saml/idp.xml", @settings.idp_entity_id
317
+ end
318
+
319
+ it "should find named descriptor" do
320
+ entity_id = "https://bar.example.com/access/saml/idp.xml"
321
+ settings = @idp_metadata_parser.parse(
322
+ @idp_metadata, :entity_id => entity_id
323
+ )
324
+ assert_equal entity_id, settings.idp_entity_id
325
+ end
326
+
327
+ it "should retreive data" do
328
+ assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", @settings.name_identifier_format
329
+ assert_equal "https://hello.example.com/access/saml/login", @settings.idp_sso_target_url
330
+ assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", @settings.idp_cert_fingerprint
331
+ assert_equal "https://hello.example.com/access/saml/logout", @settings.idp_slo_target_url
332
+ assert_equal ["AuthToken", "SSOStartPage"], @settings.idp_attribute_names
137
333
  end
138
334
  end
139
335
  end