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.
- checksums.yaml +4 -4
- data/README.md +70 -13
- data/changelog.md +14 -1
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +209 -97
- data/lib/onelogin/ruby-saml/logoutrequest.rb +2 -1
- data/lib/onelogin/ruby-saml/logoutresponse.rb +31 -8
- data/lib/onelogin/ruby-saml/metadata.rb +20 -14
- data/lib/onelogin/ruby-saml/response.rb +32 -15
- data/lib/onelogin/ruby-saml/saml_message.rb +1 -2
- data/lib/onelogin/ruby-saml/settings.rb +39 -1
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +29 -7
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/schemas/xmldsig-core-schema.xsd +1 -1
- data/lib/xml_security.rb +25 -0
- data/test/certificates/ruby-saml-2.crt +15 -0
- data/test/idp_metadata_parser_test.rb +211 -15
- data/test/logoutresponse_test.rb +60 -0
- data/test/metadata/idp_descriptor.xml +26 -0
- data/test/metadata/idp_descriptor_2.xml +56 -0
- data/test/metadata/idp_descriptor_3.xml +14 -0
- data/test/metadata/idp_multiple_descriptors.xml +53 -0
- data/test/metadata_test.rb +70 -2
- data/test/response_test.rb +289 -243
- data/test/settings_test.rb +105 -22
- data/test/slo_logoutrequest_test.rb +66 -0
- data/test/test_helper.rb +23 -3
- metadata +13 -5
- data/test/responses/idp_descriptor.xml +0 -3
@@ -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.
|
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
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
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
|
-
|
812
|
-
|
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}
|
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
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
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")
|
@@ -188,7 +188,7 @@
|
|
188
188
|
<complexType name="X509IssuerSerialType">
|
189
189
|
<sequence>
|
190
190
|
<element name="X509IssuerName" type="string"/>
|
191
|
-
<element name="X509SerialNumber" type="
|
191
|
+
<element name="X509SerialNumber" type="string"/>
|
192
192
|
</sequence>
|
193
193
|
</complexType>
|
194
194
|
|
data/lib/xml_security.rb
CHANGED
@@ -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(
|
24
|
+
settings = idp_metadata_parser.parse(idp_metadata_descriptor)
|
25
25
|
|
26
|
-
assert_equal "https://example.
|
27
|
-
assert_equal "https://example.
|
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.
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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.
|
94
|
-
assert_equal "https://example.
|
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.
|
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.
|
301
|
+
idp_metadata_parser.parse_remote("https://hello.example.com/access/saml/idp.xml")
|
134
302
|
end
|
135
303
|
|
136
|
-
|
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
|