ruby-saml 1.7.2 → 1.12.2
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.
- checksums.yaml +5 -5
- data/.travis.yml +37 -15
- data/README.md +127 -25
- data/changelog.md +61 -0
- data/lib/onelogin/ruby-saml/attribute_service.rb +1 -1
- data/lib/onelogin/ruby-saml/attributes.rb +24 -1
- data/lib/onelogin/ruby-saml/authrequest.rb +29 -6
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +239 -169
- data/lib/onelogin/ruby-saml/logging.rb +4 -1
- data/lib/onelogin/ruby-saml/logoutrequest.rb +27 -7
- data/lib/onelogin/ruby-saml/logoutresponse.rb +32 -16
- data/lib/onelogin/ruby-saml/metadata.rb +11 -3
- data/lib/onelogin/ruby-saml/response.rb +91 -30
- data/lib/onelogin/ruby-saml/saml_message.rb +15 -5
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +82 -9
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +26 -7
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +46 -18
- data/lib/onelogin/ruby-saml/utils.rb +87 -10
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +39 -12
- data/ruby-saml.gemspec +16 -8
- metadata +40 -274
- data/test/certificates/certificate1 +0 -12
- data/test/certificates/certificate_without_head_foot +0 -1
- data/test/certificates/formatted_certificate +0 -14
- data/test/certificates/formatted_chained_certificate +0 -42
- data/test/certificates/formatted_private_key +0 -12
- data/test/certificates/formatted_rsa_private_key +0 -12
- data/test/certificates/invalid_certificate1 +0 -1
- data/test/certificates/invalid_certificate2 +0 -1
- data/test/certificates/invalid_certificate3 +0 -12
- data/test/certificates/invalid_chained_certificate1 +0 -1
- data/test/certificates/invalid_private_key1 +0 -1
- data/test/certificates/invalid_private_key2 +0 -1
- data/test/certificates/invalid_private_key3 +0 -10
- data/test/certificates/invalid_rsa_private_key1 +0 -1
- data/test/certificates/invalid_rsa_private_key2 +0 -1
- data/test/certificates/invalid_rsa_private_key3 +0 -10
- data/test/certificates/ruby-saml-2.crt +0 -15
- data/test/certificates/ruby-saml.crt +0 -14
- data/test/certificates/ruby-saml.key +0 -15
- data/test/idp_metadata_parser_test.rb +0 -568
- data/test/logging_test.rb +0 -62
- data/test/logout_requests/invalid_slo_request.xml +0 -6
- data/test/logout_requests/slo_request.xml +0 -4
- data/test/logout_requests/slo_request.xml.base64 +0 -1
- data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
- data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
- data/test/logout_requests/slo_request_with_session_index.xml +0 -5
- data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
- data/test/logoutrequest_test.rb +0 -212
- data/test/logoutresponse_test.rb +0 -402
- data/test/metadata/idp_descriptor.xml +0 -26
- data/test/metadata/idp_descriptor_2.xml +0 -56
- data/test/metadata/idp_descriptor_3.xml +0 -14
- data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
- data/test/metadata/idp_metadata_multi_certs.xml +0 -75
- data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
- data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
- data/test/metadata/idp_multiple_descriptors.xml +0 -53
- data/test/metadata/no_idp_descriptor.xml +0 -21
- data/test/metadata_test.rb +0 -331
- data/test/request_test.rb +0 -296
- data/test/response_test.rb +0 -1535
- data/test/responses/adfs_response_sha1.xml +0 -46
- data/test/responses/adfs_response_sha256.xml +0 -46
- data/test/responses/adfs_response_sha384.xml +0 -46
- data/test/responses/adfs_response_sha512.xml +0 -46
- data/test/responses/adfs_response_xmlns.xml +0 -45
- data/test/responses/attackxee.xml +0 -13
- data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
- data/test/responses/invalids/empty_destination.xml.base64 +0 -1
- data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
- data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
- data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
- data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
- data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
- data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
- data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
- data/test/responses/invalids/no_conditions.xml.base64 +0 -1
- data/test/responses/invalids/no_id.xml.base64 +0 -1
- data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
- data/test/responses/invalids/no_nameid.xml.base64 +0 -1
- data/test/responses/invalids/no_saml2.xml.base64 +0 -1
- data/test/responses/invalids/no_signature.xml.base64 +0 -1
- data/test/responses/invalids/no_status.xml.base64 +0 -1
- data/test/responses/invalids/no_status_code.xml.base64 +0 -1
- data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
- data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
- data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
- data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
- data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
- data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
- data/test/responses/no_signature_ns.xml +0 -48
- data/test/responses/open_saml_response.xml +0 -56
- data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
- data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
- data/test/responses/response_double_status_code.xml.base64 +0 -1
- data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
- data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
- data/test/responses/response_eval.xml +0 -7
- data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
- data/test/responses/response_unsigned_xml_base64 +0 -1
- data/test/responses/response_with_ampersands.xml +0 -139
- data/test/responses/response_with_ampersands.xml.base64 +0 -93
- data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
- data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
- data/test/responses/response_with_multiple_attribute_values.xml +0 -67
- data/test/responses/response_with_retrieval_method.xml +0 -26
- data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
- data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
- data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
- data/test/responses/response_with_signed_assertion_3.xml +0 -30
- data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
- data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
- data/test/responses/response_without_attributes.xml.base64 +0 -79
- data/test/responses/response_without_reference_uri.xml.base64 +0 -1
- data/test/responses/response_wrapped.xml.base64 +0 -150
- data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
- data/test/responses/signed_nameid_in_atts.xml +0 -47
- data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
- data/test/responses/simple_saml_php.xml +0 -71
- data/test/responses/starfield_response.xml.base64 +0 -1
- data/test/responses/test_sign.xml +0 -43
- data/test/responses/unsigned_encrypted_adfs.xml +0 -23
- data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
- data/test/responses/valid_response.xml.base64 +0 -1
- data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
- data/test/saml_message_test.rb +0 -56
- data/test/settings_test.rb +0 -301
- data/test/slo_logoutrequest_test.rb +0 -448
- data/test/slo_logoutresponse_test.rb +0 -185
- data/test/test_helper.rb +0 -323
- data/test/utils_test.rb +0 -254
- data/test/xml_security_test.rb +0 -421
|
@@ -24,9 +24,9 @@ module OneLogin
|
|
|
24
24
|
|
|
25
25
|
# Constructs the Logout Request. A Logout Request Object that is an extension of the SamlMessage class.
|
|
26
26
|
# @param request [String] A UUEncoded Logout Request from the IdP.
|
|
27
|
-
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
|
|
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
|
+
# Or :relax_signature_validation to accept signatures if no idp certificate registered on settings
|
|
30
30
|
#
|
|
31
31
|
# @raise [ArgumentError] If Request is nil
|
|
32
32
|
#
|
|
@@ -47,6 +47,10 @@ module OneLogin
|
|
|
47
47
|
@document = REXML::Document.new(@request)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def request_id
|
|
51
|
+
id(document)
|
|
52
|
+
end
|
|
53
|
+
|
|
50
54
|
# Validates the Logout Request with the default values (soft = true)
|
|
51
55
|
# @param collect_errors [Boolean] Stop validation when first error appears or keep validating.
|
|
52
56
|
# @return [Boolean] TRUE if the Logout Request is valid
|
|
@@ -192,7 +196,7 @@ module OneLogin
|
|
|
192
196
|
|
|
193
197
|
# Validates the Logout Request against the specified schema.
|
|
194
198
|
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
|
|
195
|
-
# @raise [ValidationError] if soft == false and validation fails
|
|
199
|
+
# @raise [ValidationError] if soft == false and validation fails
|
|
196
200
|
#
|
|
197
201
|
def validate_structure
|
|
198
202
|
unless valid_saml?(document, soft)
|
|
@@ -202,7 +206,7 @@ module OneLogin
|
|
|
202
206
|
true
|
|
203
207
|
end
|
|
204
208
|
|
|
205
|
-
# Validates that the Logout Request provided in the initialization is not empty,
|
|
209
|
+
# Validates that the Logout Request provided in the initialization is not empty,
|
|
206
210
|
# @return [Boolean] True if the required info is found, otherwise False if soft=True
|
|
207
211
|
# @raise [ValidationError] if soft == false and validation fails
|
|
208
212
|
#
|
|
@@ -280,28 +284,43 @@ module OneLogin
|
|
|
280
284
|
:raw_sig_alg => options[:raw_get_params]['SigAlg']
|
|
281
285
|
)
|
|
282
286
|
|
|
287
|
+
expired = false
|
|
283
288
|
if idp_certs.nil? || idp_certs[:signing].empty?
|
|
284
289
|
valid = OneLogin::RubySaml::Utils.verify_signature(
|
|
285
|
-
:cert =>
|
|
290
|
+
:cert => idp_cert,
|
|
286
291
|
:sig_alg => options[:get_params]['SigAlg'],
|
|
287
292
|
:signature => options[:get_params]['Signature'],
|
|
288
293
|
:query_string => query_string
|
|
289
294
|
)
|
|
295
|
+
if valid && settings.security[:check_idp_cert_expiration]
|
|
296
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
297
|
+
expired = true
|
|
298
|
+
end
|
|
299
|
+
end
|
|
290
300
|
else
|
|
291
301
|
valid = false
|
|
292
|
-
idp_certs[:signing].each do |
|
|
302
|
+
idp_certs[:signing].each do |signing_idp_cert|
|
|
293
303
|
valid = OneLogin::RubySaml::Utils.verify_signature(
|
|
294
|
-
:cert =>
|
|
304
|
+
:cert => signing_idp_cert,
|
|
295
305
|
:sig_alg => options[:get_params]['SigAlg'],
|
|
296
306
|
:signature => options[:get_params]['Signature'],
|
|
297
307
|
:query_string => query_string
|
|
298
308
|
)
|
|
299
309
|
if valid
|
|
310
|
+
if settings.security[:check_idp_cert_expiration]
|
|
311
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert)
|
|
312
|
+
expired = true
|
|
313
|
+
end
|
|
314
|
+
end
|
|
300
315
|
break
|
|
301
316
|
end
|
|
302
317
|
end
|
|
303
318
|
end
|
|
304
319
|
|
|
320
|
+
if expired
|
|
321
|
+
error_msg = "IdP x509 certificate expired"
|
|
322
|
+
return append_error(error_msg)
|
|
323
|
+
end
|
|
305
324
|
unless valid
|
|
306
325
|
return append_error("Invalid Signature on Logout Request")
|
|
307
326
|
end
|
|
@@ -2,6 +2,7 @@ require "onelogin/ruby-saml/logging"
|
|
|
2
2
|
|
|
3
3
|
require "onelogin/ruby-saml/saml_message"
|
|
4
4
|
require "onelogin/ruby-saml/utils"
|
|
5
|
+
require "onelogin/ruby-saml/setting_error"
|
|
5
6
|
|
|
6
7
|
# Only supports SAML 2.0
|
|
7
8
|
module OneLogin
|
|
@@ -21,23 +22,30 @@ module OneLogin
|
|
|
21
22
|
@uuid = OneLogin::RubySaml::Utils.uuid
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
def response_id
|
|
26
|
+
@uuid
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
# Creates the Logout Response string.
|
|
25
30
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
26
31
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
|
27
32
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
|
28
33
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
|
34
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
|
29
35
|
# @return [String] Logout Request string that includes the SAMLRequest
|
|
30
36
|
#
|
|
31
|
-
def create(settings, request_id = nil, logout_message = nil, params = {})
|
|
32
|
-
params = create_params(settings, request_id, logout_message, params)
|
|
33
|
-
params_prefix = (settings.
|
|
37
|
+
def create(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
|
|
38
|
+
params = create_params(settings, request_id, logout_message, params, logout_status_code)
|
|
39
|
+
params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?'
|
|
40
|
+
url = settings.idp_slo_response_service_url || settings.idp_slo_service_url
|
|
34
41
|
saml_response = CGI.escape(params.delete("SAMLResponse"))
|
|
35
42
|
response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
|
|
36
43
|
params.each_pair do |key, value|
|
|
37
44
|
response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
|
38
45
|
end
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty?
|
|
48
|
+
@logout_url = url + response_params
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
# Creates the Get parameters for the logout response.
|
|
@@ -45,15 +53,21 @@ module OneLogin
|
|
|
45
53
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
|
46
54
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
|
47
55
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
|
56
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
|
48
57
|
# @return [Hash] Parameters
|
|
49
58
|
#
|
|
50
|
-
def create_params(settings, request_id = nil, logout_message = nil, params = {})
|
|
59
|
+
def create_params(settings, request_id = nil, logout_message = nil, params = {}, logout_status_code = nil)
|
|
51
60
|
# The method expects :RelayState but sometimes we get 'RelayState' instead.
|
|
52
61
|
# Based on the HashWithIndifferentAccess value in Rails we could experience
|
|
53
62
|
# conflicts so this line will solve them.
|
|
54
63
|
relay_state = params[:RelayState] || params['RelayState']
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
if relay_state.nil?
|
|
66
|
+
params.delete(:RelayState)
|
|
67
|
+
params.delete('RelayState')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
|
|
57
71
|
response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
|
58
72
|
|
|
59
73
|
response = ""
|
|
@@ -89,46 +103,60 @@ module OneLogin
|
|
|
89
103
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
90
104
|
# @param request_id [String] The ID of the LogoutRequest sent by this SP to the IdP. That ID will be placed as the InResponseTo in the logout response
|
|
91
105
|
# @param logout_message [String] The Message to be placed as StatusMessage in the logout response
|
|
106
|
+
# @param logout_status_code [String] The StatusCode to be placed as StatusMessage in the logout response
|
|
92
107
|
# @return [String] The SAMLResponse String.
|
|
93
108
|
#
|
|
94
|
-
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil)
|
|
109
|
+
def create_logout_response_xml_doc(settings, request_id = nil, logout_message = nil, logout_status_code = nil)
|
|
110
|
+
document = create_xml_document(settings, request_id, logout_message, logout_status_code)
|
|
111
|
+
sign_document(document, settings)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create_xml_document(settings, request_id = nil, logout_message = nil, status_code = nil)
|
|
95
115
|
time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
96
116
|
|
|
97
117
|
response_doc = XMLSecurity::Document.new
|
|
98
118
|
response_doc.uuid = uuid
|
|
99
119
|
|
|
120
|
+
destination = settings.idp_slo_response_service_url || settings.idp_slo_service_url
|
|
121
|
+
|
|
122
|
+
|
|
100
123
|
root = response_doc.add_element 'samlp:LogoutResponse', { 'xmlns:samlp' => 'urn:oasis:names:tc:SAML:2.0:protocol', "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
101
124
|
root.attributes['ID'] = uuid
|
|
102
125
|
root.attributes['IssueInstant'] = time
|
|
103
126
|
root.attributes['Version'] = '2.0'
|
|
104
127
|
root.attributes['InResponseTo'] = request_id unless request_id.nil?
|
|
105
|
-
root.attributes['Destination'] =
|
|
128
|
+
root.attributes['Destination'] = destination unless destination.nil? or destination.empty?
|
|
106
129
|
|
|
107
|
-
if settings.
|
|
130
|
+
if settings.sp_entity_id != nil
|
|
108
131
|
issuer = root.add_element "saml:Issuer"
|
|
109
|
-
issuer.text = settings.
|
|
132
|
+
issuer.text = settings.sp_entity_id
|
|
110
133
|
end
|
|
111
|
-
|
|
112
|
-
# add
|
|
134
|
+
|
|
135
|
+
# add status
|
|
113
136
|
status = root.add_element 'samlp:Status'
|
|
114
137
|
|
|
115
|
-
#
|
|
116
|
-
status_code
|
|
117
|
-
|
|
138
|
+
# status code
|
|
139
|
+
status_code ||= 'urn:oasis:names:tc:SAML:2.0:status:Success'
|
|
140
|
+
status_code_elem = status.add_element 'samlp:StatusCode'
|
|
141
|
+
status_code_elem.attributes['Value'] = status_code
|
|
118
142
|
|
|
119
|
-
#
|
|
143
|
+
# status message
|
|
120
144
|
logout_message ||= 'Successfully Signed Out'
|
|
121
145
|
status_message = status.add_element 'samlp:StatusMessage'
|
|
122
146
|
status_message.text = logout_message
|
|
123
147
|
|
|
148
|
+
response_doc
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def sign_document(document, settings)
|
|
124
152
|
# embed signature
|
|
125
153
|
if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
|
126
154
|
private_key = settings.get_sp_key
|
|
127
155
|
cert = settings.get_sp_cert
|
|
128
|
-
|
|
156
|
+
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
129
157
|
end
|
|
130
158
|
|
|
131
|
-
|
|
159
|
+
document
|
|
132
160
|
end
|
|
133
161
|
|
|
134
162
|
end
|
|
@@ -3,6 +3,7 @@ if RUBY_VERSION < '1.9'
|
|
|
3
3
|
else
|
|
4
4
|
require 'securerandom'
|
|
5
5
|
end
|
|
6
|
+
require "openssl"
|
|
6
7
|
|
|
7
8
|
module OneLogin
|
|
8
9
|
module RubySaml
|
|
@@ -14,6 +15,61 @@ module OneLogin
|
|
|
14
15
|
|
|
15
16
|
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
|
16
17
|
XENC = "http://www.w3.org/2001/04/xmlenc#"
|
|
18
|
+
DURATION_FORMAT = %r(^(-?)P(?:(?:(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?)|(?:(\d+)W))$)
|
|
19
|
+
|
|
20
|
+
# Checks if the x509 cert provided is expired
|
|
21
|
+
#
|
|
22
|
+
# @param cert [Certificate] The x509 certificate
|
|
23
|
+
#
|
|
24
|
+
def self.is_cert_expired(cert)
|
|
25
|
+
if cert.is_a?(String)
|
|
26
|
+
cert = OpenSSL::X509::Certificate.new(cert)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return cert.not_after < Time.now
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Interprets a ISO8601 duration value relative to a given timestamp.
|
|
33
|
+
#
|
|
34
|
+
# @param duration [String] The duration, as a string.
|
|
35
|
+
# @param timestamp [Integer] The unix timestamp we should apply the
|
|
36
|
+
# duration to. Optional, default to the
|
|
37
|
+
# current time.
|
|
38
|
+
#
|
|
39
|
+
# @return [Integer] The new timestamp, after the duration is applied.
|
|
40
|
+
#
|
|
41
|
+
def self.parse_duration(duration, timestamp=Time.now.utc)
|
|
42
|
+
matches = duration.match(DURATION_FORMAT)
|
|
43
|
+
|
|
44
|
+
if matches.nil?
|
|
45
|
+
raise Exception.new("Invalid ISO 8601 duration")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
durYears = matches[2].to_i
|
|
49
|
+
durMonths = matches[3].to_i
|
|
50
|
+
durDays = matches[4].to_i
|
|
51
|
+
durHours = matches[5].to_i
|
|
52
|
+
durMinutes = matches[6].to_i
|
|
53
|
+
durSeconds = matches[7].to_f
|
|
54
|
+
durWeeks = matches[8].to_i
|
|
55
|
+
|
|
56
|
+
if matches[1] == "-"
|
|
57
|
+
durYears = -durYears
|
|
58
|
+
durMonths = -durMonths
|
|
59
|
+
durDays = -durDays
|
|
60
|
+
durHours = -durHours
|
|
61
|
+
durMinutes = -durMinutes
|
|
62
|
+
durSeconds = -durSeconds
|
|
63
|
+
durWeeks = -durWeeks
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
initial_datetime = Time.at(timestamp).utc.to_datetime
|
|
67
|
+
final_datetime = initial_datetime.next_year(durYears)
|
|
68
|
+
final_datetime = final_datetime.next_month(durMonths)
|
|
69
|
+
final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
|
|
70
|
+
final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
|
|
71
|
+
return final_timestamp
|
|
72
|
+
end
|
|
17
73
|
|
|
18
74
|
# Return a properly formatted x509 certificate
|
|
19
75
|
#
|
|
@@ -22,7 +78,11 @@ module OneLogin
|
|
|
22
78
|
#
|
|
23
79
|
def self.format_cert(cert)
|
|
24
80
|
# don't try to format an encoded certificate or if is empty or nil
|
|
25
|
-
|
|
81
|
+
if cert.respond_to?(:ascii_only?)
|
|
82
|
+
return cert if cert.nil? || cert.empty? || !cert.ascii_only?
|
|
83
|
+
else
|
|
84
|
+
return cert if cert.nil? || cert.empty? || cert.match(/\x0d/)
|
|
85
|
+
end
|
|
26
86
|
|
|
27
87
|
if cert.scan(/BEGIN CERTIFICATE/).length > 1
|
|
28
88
|
formatted_cert = []
|
|
@@ -32,7 +92,9 @@ module OneLogin
|
|
|
32
92
|
formatted_cert.join("\n")
|
|
33
93
|
else
|
|
34
94
|
cert = cert.gsub(/\-{5}\s?(BEGIN|END) CERTIFICATE\s?\-{5}/, "")
|
|
35
|
-
cert = cert.gsub(
|
|
95
|
+
cert = cert.gsub(/\r/, "")
|
|
96
|
+
cert = cert.gsub(/\n/, "")
|
|
97
|
+
cert = cert.gsub(/\s/, "")
|
|
36
98
|
cert = cert.scan(/.{1,64}/)
|
|
37
99
|
cert = cert.join("\n")
|
|
38
100
|
"-----BEGIN CERTIFICATE-----\n#{cert}\n-----END CERTIFICATE-----"
|
|
@@ -51,7 +113,9 @@ module OneLogin
|
|
|
51
113
|
# is this an rsa key?
|
|
52
114
|
rsa_key = key.match("RSA PRIVATE KEY")
|
|
53
115
|
key = key.gsub(/\-{5}\s?(BEGIN|END)( RSA)? PRIVATE KEY\s?\-{5}/, "")
|
|
54
|
-
key = key.gsub(
|
|
116
|
+
key = key.gsub(/\n/, "")
|
|
117
|
+
key = key.gsub(/\r/, "")
|
|
118
|
+
key = key.gsub(/\s/, "")
|
|
55
119
|
key = key.scan(/.{1,64}/)
|
|
56
120
|
key = key.join("\n")
|
|
57
121
|
key_label = rsa_key ? "RSA PRIVATE KEY" : "PRIVATE KEY"
|
|
@@ -98,7 +162,7 @@ module OneLogin
|
|
|
98
162
|
# @param rawparams [Hash] Raw GET Parameters
|
|
99
163
|
# @param params [Hash] GET Parameters
|
|
100
164
|
# @return [Hash] New raw parameters
|
|
101
|
-
#
|
|
165
|
+
#
|
|
102
166
|
def self.prepare_raw_get_params(rawparams, params)
|
|
103
167
|
rawparams ||= {}
|
|
104
168
|
|
|
@@ -107,7 +171,7 @@ module OneLogin
|
|
|
107
171
|
end
|
|
108
172
|
if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil?
|
|
109
173
|
rawparams['SAMLResponse'] = CGI.escape(params['SAMLResponse'])
|
|
110
|
-
end
|
|
174
|
+
end
|
|
111
175
|
if rawparams['RelayState'].nil? && !params['RelayState'].nil?
|
|
112
176
|
rawparams['RelayState'] = CGI.escape(params['RelayState'])
|
|
113
177
|
end
|
|
@@ -136,16 +200,16 @@ module OneLogin
|
|
|
136
200
|
# @param status_code [String] StatusCode value
|
|
137
201
|
# @param status_message [Strig] StatusMessage value
|
|
138
202
|
# @return [String] The status error message
|
|
139
|
-
def self.status_error_msg(error_msg,
|
|
140
|
-
unless
|
|
141
|
-
if
|
|
142
|
-
status_codes =
|
|
203
|
+
def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
|
|
204
|
+
unless raw_status_code.nil?
|
|
205
|
+
if raw_status_code.include? "|"
|
|
206
|
+
status_codes = raw_status_code.split(' | ')
|
|
143
207
|
values = status_codes.collect do |status_code|
|
|
144
208
|
status_code.split(':').last
|
|
145
209
|
end
|
|
146
210
|
printable_code = values.join(" => ")
|
|
147
211
|
else
|
|
148
|
-
printable_code =
|
|
212
|
+
printable_code = raw_status_code.split(':').last
|
|
149
213
|
end
|
|
150
214
|
error_msg << ', was ' + printable_code
|
|
151
215
|
end
|
|
@@ -232,6 +296,9 @@ module OneLogin
|
|
|
232
296
|
when 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' then cipher = OpenSSL::Cipher.new('AES-128-CBC').decrypt
|
|
233
297
|
when 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' then cipher = OpenSSL::Cipher.new('AES-192-CBC').decrypt
|
|
234
298
|
when 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' then cipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
|
|
299
|
+
when 'http://www.w3.org/2009/xmlenc11#aes128-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(128, :GCM).decrypt
|
|
300
|
+
when 'http://www.w3.org/2009/xmlenc11#aes192-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(192, :GCM).decrypt
|
|
301
|
+
when 'http://www.w3.org/2009/xmlenc11#aes256-gcm' then auth_cipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt
|
|
235
302
|
when 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' then rsa = symmetric_key
|
|
236
303
|
when 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' then oaep = symmetric_key
|
|
237
304
|
end
|
|
@@ -242,6 +309,16 @@ module OneLogin
|
|
|
242
309
|
cipher.padding, cipher.key, cipher.iv = 0, symmetric_key, cipher_text[0..iv_len-1]
|
|
243
310
|
assertion_plaintext = cipher.update(data)
|
|
244
311
|
assertion_plaintext << cipher.final
|
|
312
|
+
elsif auth_cipher
|
|
313
|
+
iv_len, text_len, tag_len = auth_cipher.iv_len, cipher_text.length, 16
|
|
314
|
+
data = cipher_text[iv_len..text_len-1-tag_len]
|
|
315
|
+
auth_cipher.padding = 0
|
|
316
|
+
auth_cipher.key = symmetric_key
|
|
317
|
+
auth_cipher.iv = cipher_text[0..iv_len-1]
|
|
318
|
+
auth_cipher.auth_data = ''
|
|
319
|
+
auth_cipher.auth_tag = cipher_text[text_len-tag_len..-1]
|
|
320
|
+
assertion_plaintext = auth_cipher.update(data)
|
|
321
|
+
assertion_plaintext << auth_cipher.final
|
|
245
322
|
elsif rsa
|
|
246
323
|
rsa.private_decrypt(cipher_text)
|
|
247
324
|
elsif oaep
|
data/lib/xml_security.rb
CHANGED
|
@@ -91,7 +91,7 @@ module XMLSecurity
|
|
|
91
91
|
ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
|
|
92
92
|
INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
attr_writer :uuid
|
|
95
95
|
|
|
96
96
|
def uuid
|
|
97
97
|
@uuid ||= begin
|
|
@@ -187,7 +187,7 @@ module XMLSecurity
|
|
|
187
187
|
class SignedDocument < BaseDocument
|
|
188
188
|
include OneLogin::RubySaml::ErrorHandling
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
attr_writer :signed_element_id
|
|
191
191
|
|
|
192
192
|
def initialize(response, errors = [])
|
|
193
193
|
super(response)
|
|
@@ -211,8 +211,8 @@ module XMLSecurity
|
|
|
211
211
|
cert_text = Base64.decode64(base64_cert)
|
|
212
212
|
begin
|
|
213
213
|
cert = OpenSSL::X509::Certificate.new(cert_text)
|
|
214
|
-
rescue OpenSSL::X509::CertificateError =>
|
|
215
|
-
return append_error("Certificate Error", soft)
|
|
214
|
+
rescue OpenSSL::X509::CertificateError => _e
|
|
215
|
+
return append_error("Document Certificate Error", soft)
|
|
216
216
|
end
|
|
217
217
|
|
|
218
218
|
if options[:fingerprint_alg]
|
|
@@ -224,7 +224,6 @@ module XMLSecurity
|
|
|
224
224
|
|
|
225
225
|
# check cert matches registered idp cert
|
|
226
226
|
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
|
|
227
|
-
@errors << "Fingerprint mismatch"
|
|
228
227
|
return append_error("Fingerprint mismatch", soft)
|
|
229
228
|
end
|
|
230
229
|
else
|
|
@@ -241,7 +240,7 @@ module XMLSecurity
|
|
|
241
240
|
validate_signature(base64_cert, soft)
|
|
242
241
|
end
|
|
243
242
|
|
|
244
|
-
def validate_document_with_cert(idp_cert)
|
|
243
|
+
def validate_document_with_cert(idp_cert, soft = true)
|
|
245
244
|
# get cert from response
|
|
246
245
|
cert_element = REXML::XPath.first(
|
|
247
246
|
self,
|
|
@@ -254,13 +253,13 @@ module XMLSecurity
|
|
|
254
253
|
cert_text = Base64.decode64(base64_cert)
|
|
255
254
|
begin
|
|
256
255
|
cert = OpenSSL::X509::Certificate.new(cert_text)
|
|
257
|
-
rescue OpenSSL::X509::CertificateError =>
|
|
258
|
-
return append_error("Certificate Error", soft)
|
|
256
|
+
rescue OpenSSL::X509::CertificateError => _e
|
|
257
|
+
return append_error("Document Certificate Error", soft)
|
|
259
258
|
end
|
|
260
259
|
|
|
261
260
|
# check saml response cert matches provided idp cert
|
|
262
261
|
if idp_cert.to_pem != cert.to_pem
|
|
263
|
-
return
|
|
262
|
+
return append_error("Certificate of the Signature element does not match provided certificate", soft)
|
|
264
263
|
end
|
|
265
264
|
else
|
|
266
265
|
base64_cert = Base64.encode64(idp_cert.to_pem)
|
|
@@ -318,15 +317,17 @@ module XMLSecurity
|
|
|
318
317
|
|
|
319
318
|
# check digests
|
|
320
319
|
ref = REXML::XPath.first(sig_element, "//ds:Reference", {"ds"=>DSIG})
|
|
321
|
-
uri = ref.attributes.get_attribute("URI").value
|
|
322
320
|
|
|
323
321
|
hashed_element = document.at_xpath("//*[@ID=$id]", nil, { 'id' => extract_signed_element_id })
|
|
324
|
-
|
|
322
|
+
|
|
325
323
|
canon_algorithm = canon_algorithm REXML::XPath.first(
|
|
326
324
|
ref,
|
|
327
325
|
'//ds:CanonicalizationMethod',
|
|
328
326
|
{ "ds" => DSIG }
|
|
329
327
|
)
|
|
328
|
+
|
|
329
|
+
canon_algorithm = process_transforms(ref, canon_algorithm)
|
|
330
|
+
|
|
330
331
|
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
|
331
332
|
|
|
332
333
|
digest_algorithm = algorithm(REXML::XPath.first(
|
|
@@ -343,7 +344,6 @@ module XMLSecurity
|
|
|
343
344
|
digest_value = Base64.decode64(OneLogin::RubySaml::Utils.element_text(encoded_digest_value))
|
|
344
345
|
|
|
345
346
|
unless digests_match?(hash, digest_value)
|
|
346
|
-
@errors << "Digest mismatch"
|
|
347
347
|
return append_error("Digest mismatch", soft)
|
|
348
348
|
end
|
|
349
349
|
|
|
@@ -361,6 +361,33 @@ module XMLSecurity
|
|
|
361
361
|
|
|
362
362
|
private
|
|
363
363
|
|
|
364
|
+
def process_transforms(ref, canon_algorithm)
|
|
365
|
+
transforms = REXML::XPath.match(
|
|
366
|
+
ref,
|
|
367
|
+
"//ds:Transforms/ds:Transform",
|
|
368
|
+
{ "ds" => DSIG }
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
transforms.each do |transform_element|
|
|
372
|
+
if transform_element.attributes && transform_element.attributes["Algorithm"]
|
|
373
|
+
algorithm = transform_element.attributes["Algorithm"]
|
|
374
|
+
case algorithm
|
|
375
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
|
|
376
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
|
|
377
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_0
|
|
378
|
+
when "http://www.w3.org/2006/12/xml-c14n11",
|
|
379
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments"
|
|
380
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_1_1
|
|
381
|
+
when "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
382
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments"
|
|
383
|
+
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
canon_algorithm
|
|
389
|
+
end
|
|
390
|
+
|
|
364
391
|
def digests_match?(hash, digest_value)
|
|
365
392
|
hash == digest_value
|
|
366
393
|
end
|
data/ruby-saml.gemspec
CHANGED
|
@@ -15,36 +15,44 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
"LICENSE",
|
|
16
16
|
"README.md"
|
|
17
17
|
]
|
|
18
|
-
s.files = `git ls-files`.split("\
|
|
19
|
-
s.homepage = %q{
|
|
20
|
-
s.rubyforge_project = %q{http://www.rubygems.org/gems/ruby-saml}
|
|
18
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
19
|
+
s.homepage = %q{https://github.com/onelogin/ruby-saml}
|
|
21
20
|
s.rdoc_options = ["--charset=UTF-8"]
|
|
22
21
|
s.require_paths = ["lib"]
|
|
23
22
|
s.rubygems_version = %q{1.3.7}
|
|
24
23
|
s.required_ruby_version = '>= 1.8.7'
|
|
25
24
|
s.summary = %q{SAML Ruby Tookit}
|
|
26
|
-
s.test_files = `git ls-files test/*`.split("\n")
|
|
27
25
|
|
|
28
26
|
# Because runtime dependencies are determined at build time, we cannot make
|
|
29
27
|
# Nokogiri's version dependent on the Ruby version, even though we would
|
|
30
28
|
# have liked to constrain Ruby 1.8.7 to install only the 1.5.x versions.
|
|
31
29
|
if defined?(JRUBY_VERSION)
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if JRUBY_VERSION < '9.2.0.0'
|
|
31
|
+
s.add_runtime_dependency('nokogiri', '>= 1.8.2', '<= 1.8.5')
|
|
32
|
+
s.add_runtime_dependency('jruby-openssl', '>= 0.9.8')
|
|
33
|
+
s.add_runtime_dependency('json', '< 2.3.0')
|
|
34
|
+
else
|
|
35
|
+
s.add_runtime_dependency('nokogiri', '>= 1.8.2')
|
|
36
|
+
end
|
|
34
37
|
elsif RUBY_VERSION < '1.9'
|
|
35
38
|
s.add_runtime_dependency('uuid')
|
|
36
39
|
s.add_runtime_dependency('nokogiri', '<= 1.5.11')
|
|
37
40
|
elsif RUBY_VERSION < '2.1'
|
|
38
41
|
s.add_runtime_dependency('nokogiri', '>= 1.5.10', '<= 1.6.8.1')
|
|
42
|
+
s.add_runtime_dependency('json', '< 2.3.0')
|
|
43
|
+
elsif RUBY_VERSION < '2.3'
|
|
44
|
+
s.add_runtime_dependency('nokogiri', '>= 1.9.1', '<= 1.10.0')
|
|
39
45
|
else
|
|
40
|
-
s.add_runtime_dependency('nokogiri', '>= 1.5
|
|
46
|
+
s.add_runtime_dependency('nokogiri', '>= 1.10.5')
|
|
47
|
+
s.add_runtime_dependency('rexml')
|
|
41
48
|
end
|
|
42
49
|
|
|
50
|
+
s.add_development_dependency('coveralls')
|
|
43
51
|
s.add_development_dependency('minitest', '~> 5.5')
|
|
44
52
|
s.add_development_dependency('mocha', '~> 0.14')
|
|
45
53
|
s.add_development_dependency('rake', '~> 10')
|
|
46
54
|
s.add_development_dependency('shoulda', '~> 2.11')
|
|
47
|
-
s.add_development_dependency('simplecov'
|
|
55
|
+
s.add_development_dependency('simplecov')
|
|
48
56
|
s.add_development_dependency('systemu', '~> 2')
|
|
49
57
|
s.add_development_dependency('timecop', '<= 0.6.0')
|
|
50
58
|
|