ruby-saml 1.4.2 → 1.4.3

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.

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