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