ruby-saml 1.9.0 → 1.14.0
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/.github/workflows/test.yml +25 -0
- data/{changelog.md → CHANGELOG.md} +64 -1
- data/README.md +394 -211
- data/UPGRADING.md +149 -0
- data/lib/onelogin/ruby-saml/attributes.rb +24 -1
- data/lib/onelogin/ruby-saml/authrequest.rb +26 -10
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +285 -184
- data/lib/onelogin/ruby-saml/logging.rb +3 -3
- data/lib/onelogin/ruby-saml/logoutrequest.rb +26 -11
- data/lib/onelogin/ruby-saml/logoutresponse.rb +27 -11
- data/lib/onelogin/ruby-saml/metadata.rb +62 -17
- data/lib/onelogin/ruby-saml/response.rb +86 -37
- data/lib/onelogin/ruby-saml/saml_message.rb +14 -5
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +117 -41
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +33 -31
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +43 -20
- data/lib/onelogin/ruby-saml/utils.rb +101 -9
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +39 -13
- data/ruby-saml.gemspec +21 -8
- metadata +43 -284
- data/.travis.yml +0 -32
- 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 -579
- 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 -226
- 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_descriptor_4.xml +0 -72
- 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 -323
- data/test/response_test.rb +0 -1619
- 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_node_text_attack2.xml.base64 +0 -1
- data/test/responses/response_node_text_attack3.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_with_formatted_x509certificate.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 -329
- data/test/slo_logoutrequest_test.rb +0 -448
- data/test/slo_logoutresponse_test.rb +0 -199
- data/test/test_helper.rb +0 -327
- data/test/utils_test.rb +0 -254
- data/test/xml_security_test.rb +0 -421
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "onelogin/ruby-saml/logging"
|
|
2
2
|
require "onelogin/ruby-saml/saml_message"
|
|
3
3
|
require "onelogin/ruby-saml/utils"
|
|
4
|
+
require "onelogin/ruby-saml/setting_error"
|
|
4
5
|
|
|
5
6
|
# Only supports SAML 2.0
|
|
6
7
|
module OneLogin
|
|
@@ -11,7 +12,7 @@ module OneLogin
|
|
|
11
12
|
class Logoutrequest < SamlMessage
|
|
12
13
|
|
|
13
14
|
# Logout Request ID
|
|
14
|
-
|
|
15
|
+
attr_accessor :uuid
|
|
15
16
|
|
|
16
17
|
# Initializes the Logout Request. A Logoutrequest Object that is an extension of the SamlMessage class.
|
|
17
18
|
# Asigns an ID, a random uuid.
|
|
@@ -20,6 +21,10 @@ module OneLogin
|
|
|
20
21
|
@uuid = OneLogin::RubySaml::Utils.uuid
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
def request_id
|
|
25
|
+
@uuid
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
# Creates the Logout Request string.
|
|
24
29
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
25
30
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
|
@@ -27,13 +32,14 @@ module OneLogin
|
|
|
27
32
|
#
|
|
28
33
|
def create(settings, params={})
|
|
29
34
|
params = create_params(settings, params)
|
|
30
|
-
params_prefix = (settings.
|
|
35
|
+
params_prefix = (settings.idp_slo_service_url =~ /\?/) ? '&' : '?'
|
|
31
36
|
saml_request = CGI.escape(params.delete("SAMLRequest"))
|
|
32
37
|
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
|
|
33
38
|
params.each_pair do |key, value|
|
|
34
39
|
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
|
35
40
|
end
|
|
36
|
-
|
|
41
|
+
raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty?
|
|
42
|
+
@logout_url = settings.idp_slo_service_url + request_params
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
# Creates the Get parameters for the logout request.
|
|
@@ -64,8 +70,8 @@ module OneLogin
|
|
|
64
70
|
base64_request = encode(request)
|
|
65
71
|
request_params = {"SAMLRequest" => base64_request}
|
|
66
72
|
|
|
67
|
-
if settings.
|
|
68
|
-
params['SigAlg']
|
|
73
|
+
if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_requests_signed] && settings.private_key
|
|
74
|
+
params['SigAlg'] = settings.security[:signature_method]
|
|
69
75
|
url_string = OneLogin::RubySaml::Utils.build_query(
|
|
70
76
|
:type => 'SAMLRequest',
|
|
71
77
|
:data => base64_request,
|
|
@@ -89,6 +95,11 @@ module OneLogin
|
|
|
89
95
|
# @return [String] The SAMLRequest String.
|
|
90
96
|
#
|
|
91
97
|
def create_logout_request_xml_doc(settings)
|
|
98
|
+
document = create_xml_document(settings)
|
|
99
|
+
sign_document(document, settings)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def create_xml_document(settings)
|
|
92
103
|
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
93
104
|
|
|
94
105
|
request_doc = XMLSecurity::Document.new
|
|
@@ -98,11 +109,11 @@ module OneLogin
|
|
|
98
109
|
root.attributes['ID'] = uuid
|
|
99
110
|
root.attributes['IssueInstant'] = time
|
|
100
111
|
root.attributes['Version'] = "2.0"
|
|
101
|
-
root.attributes['Destination'] = settings.
|
|
112
|
+
root.attributes['Destination'] = settings.idp_slo_service_url unless settings.idp_slo_service_url.nil? or settings.idp_slo_service_url.empty?
|
|
102
113
|
|
|
103
|
-
if settings.
|
|
114
|
+
if settings.sp_entity_id
|
|
104
115
|
issuer = root.add_element "saml:Issuer"
|
|
105
|
-
issuer.text = settings.
|
|
116
|
+
issuer.text = settings.sp_entity_id
|
|
106
117
|
end
|
|
107
118
|
|
|
108
119
|
nameid = root.add_element "saml:NameID"
|
|
@@ -122,14 +133,18 @@ module OneLogin
|
|
|
122
133
|
sessionindex.text = settings.sessionindex
|
|
123
134
|
end
|
|
124
135
|
|
|
136
|
+
request_doc
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def sign_document(document, settings)
|
|
125
140
|
# embed signature
|
|
126
|
-
if settings.
|
|
141
|
+
if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && settings.security[:logout_requests_signed] && settings.private_key && settings.certificate
|
|
127
142
|
private_key = settings.get_sp_key
|
|
128
143
|
cert = settings.get_sp_cert
|
|
129
|
-
|
|
144
|
+
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
130
145
|
end
|
|
131
146
|
|
|
132
|
-
|
|
147
|
+
document
|
|
133
148
|
end
|
|
134
149
|
end
|
|
135
150
|
end
|
|
@@ -43,19 +43,20 @@ module OneLogin
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
@options = options
|
|
46
|
-
@response = decode_raw_saml(response)
|
|
46
|
+
@response = decode_raw_saml(response, settings)
|
|
47
47
|
@document = XMLSecurity::SignedDocument.new(@response)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def response_id
|
|
51
|
+
id(document)
|
|
52
|
+
end
|
|
53
|
+
|
|
50
54
|
# Checks if the Status has the "Success" code
|
|
51
55
|
# @return [Boolean] True if the StatusCode is Sucess
|
|
52
56
|
# @raise [ValidationError] if soft == false and validation fails
|
|
53
57
|
#
|
|
54
58
|
def success?
|
|
55
|
-
|
|
56
|
-
return append_error("Bad status code. Expected <urn:oasis:names:tc:SAML:2.0:status:Success>, but was: <#@status_code>")
|
|
57
|
-
end
|
|
58
|
-
true
|
|
59
|
+
return status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# @return [String|nil] Gets the InResponseTo attribute from the Logout Response if exists.
|
|
@@ -65,7 +66,7 @@ module OneLogin
|
|
|
65
66
|
node = REXML::XPath.first(
|
|
66
67
|
document,
|
|
67
68
|
"/p:LogoutResponse",
|
|
68
|
-
{ "p" => PROTOCOL
|
|
69
|
+
{ "p" => PROTOCOL }
|
|
69
70
|
)
|
|
70
71
|
node.nil? ? nil : node.attributes['InResponseTo']
|
|
71
72
|
end
|
|
@@ -88,7 +89,7 @@ module OneLogin
|
|
|
88
89
|
#
|
|
89
90
|
def status_code
|
|
90
91
|
@status_code ||= begin
|
|
91
|
-
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL
|
|
92
|
+
node = REXML::XPath.first(document, "/p:LogoutResponse/p:Status/p:StatusCode", { "p" => PROTOCOL })
|
|
92
93
|
node.nil? ? nil : node.attributes["Value"]
|
|
93
94
|
end
|
|
94
95
|
end
|
|
@@ -98,7 +99,7 @@ module OneLogin
|
|
|
98
99
|
node = REXML::XPath.first(
|
|
99
100
|
document,
|
|
100
101
|
"/p:LogoutResponse/p:Status/p:StatusMessage",
|
|
101
|
-
{ "p" => PROTOCOL
|
|
102
|
+
{ "p" => PROTOCOL }
|
|
102
103
|
)
|
|
103
104
|
Utils.element_text(node)
|
|
104
105
|
end
|
|
@@ -166,7 +167,7 @@ module OneLogin
|
|
|
166
167
|
|
|
167
168
|
return append_error("No settings on logout response") if settings.nil?
|
|
168
169
|
|
|
169
|
-
return append_error("No
|
|
170
|
+
return append_error("No sp_entity_id in settings of the logout response") if settings.sp_entity_id.nil?
|
|
170
171
|
|
|
171
172
|
if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil? && settings.idp_cert_multi.nil?
|
|
172
173
|
return append_error("No fingerprint or certificate on settings of the logout response")
|
|
@@ -211,7 +212,7 @@ module OneLogin
|
|
|
211
212
|
return true unless options.has_key? :get_params
|
|
212
213
|
return true unless options[:get_params].has_key? 'Signature'
|
|
213
214
|
|
|
214
|
-
options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params])
|
|
215
|
+
options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding])
|
|
215
216
|
|
|
216
217
|
if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil?
|
|
217
218
|
options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg'])
|
|
@@ -231,13 +232,19 @@ module OneLogin
|
|
|
231
232
|
:raw_sig_alg => options[:raw_get_params]['SigAlg']
|
|
232
233
|
)
|
|
233
234
|
|
|
235
|
+
expired = false
|
|
234
236
|
if idp_certs.nil? || idp_certs[:signing].empty?
|
|
235
237
|
valid = OneLogin::RubySaml::Utils.verify_signature(
|
|
236
|
-
:cert =>
|
|
238
|
+
:cert => idp_cert,
|
|
237
239
|
:sig_alg => options[:get_params]['SigAlg'],
|
|
238
240
|
:signature => options[:get_params]['Signature'],
|
|
239
241
|
:query_string => query_string
|
|
240
242
|
)
|
|
243
|
+
if valid && settings.security[:check_idp_cert_expiration]
|
|
244
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
245
|
+
expired = true
|
|
246
|
+
end
|
|
247
|
+
end
|
|
241
248
|
else
|
|
242
249
|
valid = false
|
|
243
250
|
idp_certs[:signing].each do |signing_idp_cert|
|
|
@@ -248,11 +255,20 @@ module OneLogin
|
|
|
248
255
|
:query_string => query_string
|
|
249
256
|
)
|
|
250
257
|
if valid
|
|
258
|
+
if settings.security[:check_idp_cert_expiration]
|
|
259
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(signing_idp_cert)
|
|
260
|
+
expired = true
|
|
261
|
+
end
|
|
262
|
+
end
|
|
251
263
|
break
|
|
252
264
|
end
|
|
253
265
|
end
|
|
254
266
|
end
|
|
255
267
|
|
|
268
|
+
if expired
|
|
269
|
+
error_msg = "IdP x509 certificate expired"
|
|
270
|
+
return append_error(error_msg)
|
|
271
|
+
end
|
|
256
272
|
unless valid
|
|
257
273
|
error_msg = "Invalid Signature on Logout Response"
|
|
258
274
|
return append_error(error_msg)
|
|
@@ -15,25 +15,56 @@ module OneLogin
|
|
|
15
15
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
16
16
|
# @param pretty_print [Boolean] Pretty print or not the response
|
|
17
17
|
# (No pretty print if you gonna validate the signature)
|
|
18
|
+
# @param valid_until [DateTime] Metadata's valid time
|
|
19
|
+
# @param cache_duration [Integer] Duration of the cache in seconds
|
|
18
20
|
# @return [String] XML Metadata of the Service Provider
|
|
19
21
|
#
|
|
20
|
-
def generate(settings, pretty_print=false)
|
|
22
|
+
def generate(settings, pretty_print=false, valid_until=nil, cache_duration=nil)
|
|
21
23
|
meta_doc = XMLSecurity::Document.new
|
|
24
|
+
add_xml_declaration(meta_doc)
|
|
25
|
+
root = add_root_element(meta_doc, settings, valid_until, cache_duration)
|
|
26
|
+
sp_sso = add_sp_sso_element(root, settings)
|
|
27
|
+
add_sp_certificates(sp_sso, settings)
|
|
28
|
+
add_sp_service_elements(sp_sso, settings)
|
|
29
|
+
add_extras(root, settings)
|
|
30
|
+
embed_signature(meta_doc, settings)
|
|
31
|
+
output_xml(meta_doc, pretty_print)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
protected
|
|
35
|
+
|
|
36
|
+
def add_xml_declaration(meta_doc)
|
|
37
|
+
meta_doc << REXML::XMLDecl.new('1.0', 'UTF-8')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_root_element(meta_doc, settings, valid_until, cache_duration)
|
|
22
41
|
namespaces = {
|
|
23
42
|
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
|
|
24
43
|
}
|
|
44
|
+
|
|
25
45
|
if settings.attribute_consuming_service.configured?
|
|
26
46
|
namespaces["xmlns:saml"] = "urn:oasis:names:tc:SAML:2.0:assertion"
|
|
27
47
|
end
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
|
|
49
|
+
root = meta_doc.add_element("md:EntityDescriptor", namespaces)
|
|
50
|
+
root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
|
|
51
|
+
root.attributes["entityID"] = settings.sp_entity_id if settings.sp_entity_id
|
|
52
|
+
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z') if valid_until
|
|
53
|
+
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S" if cache_duration
|
|
54
|
+
root
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_sp_sso_element(root, settings)
|
|
58
|
+
root.add_element "md:SPSSODescriptor", {
|
|
30
59
|
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
|
|
31
60
|
"AuthnRequestsSigned" => settings.security[:authn_requests_signed],
|
|
32
61
|
"WantAssertionsSigned" => settings.security[:want_assertions_signed],
|
|
33
62
|
}
|
|
63
|
+
end
|
|
34
64
|
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
# Add KeyDescriptor if messages will be signed / encrypted
|
|
66
|
+
# with SP certificate, and new SP certificate if any
|
|
67
|
+
def add_sp_certificates(sp_sso, settings)
|
|
37
68
|
cert = settings.get_sp_cert
|
|
38
69
|
cert_new = settings.get_sp_cert_new
|
|
39
70
|
|
|
@@ -56,10 +87,10 @@ module OneLogin
|
|
|
56
87
|
end
|
|
57
88
|
end
|
|
58
89
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
sp_sso
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_sp_service_elements(sp_sso, settings)
|
|
63
94
|
if settings.single_logout_service_url
|
|
64
95
|
sp_sso.add_element "md:SingleLogoutService", {
|
|
65
96
|
"Binding" => settings.single_logout_service_binding,
|
|
@@ -67,10 +98,12 @@ module OneLogin
|
|
|
67
98
|
"ResponseLocation" => settings.single_logout_service_url
|
|
68
99
|
}
|
|
69
100
|
end
|
|
101
|
+
|
|
70
102
|
if settings.name_identifier_format
|
|
71
103
|
nameid = sp_sso.add_element "md:NameIDFormat"
|
|
72
104
|
nameid.text = settings.name_identifier_format
|
|
73
105
|
end
|
|
106
|
+
|
|
74
107
|
if settings.assertion_consumer_service_url
|
|
75
108
|
sp_sso.add_element "md:AssertionConsumerService", {
|
|
76
109
|
"Binding" => settings.assertion_consumer_service_binding,
|
|
@@ -109,15 +142,27 @@ module OneLogin
|
|
|
109
142
|
# <md:RoleDescriptor xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:query="urn:oasis:names:tc:SAML:metadata:ext:query" xsi:type="query:AttributeQueryDescriptorType" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
|
|
110
143
|
# <md:XACMLAuthzDecisionQueryDescriptor WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"/>
|
|
111
144
|
|
|
112
|
-
|
|
145
|
+
sp_sso
|
|
146
|
+
end
|
|
113
147
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
# can be overridden in subclass
|
|
149
|
+
def add_extras(root, _settings)
|
|
150
|
+
root
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def embed_signature(meta_doc, settings)
|
|
154
|
+
return unless settings.security[:metadata_signed]
|
|
155
|
+
|
|
156
|
+
private_key = settings.get_sp_key
|
|
157
|
+
cert = settings.get_sp_cert
|
|
158
|
+
return unless private_key && cert
|
|
159
|
+
|
|
160
|
+
meta_doc.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def output_xml(meta_doc, pretty_print)
|
|
164
|
+
ret = ''
|
|
119
165
|
|
|
120
|
-
ret = ""
|
|
121
166
|
# pretty print the XML so IdP administrators can easily see what the SP supports
|
|
122
167
|
if pretty_print
|
|
123
168
|
meta_doc.write(ret, 1)
|
|
@@ -125,7 +170,7 @@ module OneLogin
|
|
|
125
170
|
ret = meta_doc.to_s
|
|
126
171
|
end
|
|
127
172
|
|
|
128
|
-
|
|
173
|
+
ret
|
|
129
174
|
end
|
|
130
175
|
end
|
|
131
176
|
end
|
|
@@ -34,7 +34,7 @@ module OneLogin
|
|
|
34
34
|
# This is not a whitelist to allow people extending OneLogin::RubySaml:Response
|
|
35
35
|
# and pass custom options
|
|
36
36
|
AVAILABLE_OPTIONS = [
|
|
37
|
-
:allowed_clock_drift, :check_duplicated_attributes, :matches_request_id, :settings, :skip_authnstatement, :skip_conditions,
|
|
37
|
+
:allowed_clock_drift, :check_duplicated_attributes, :matches_request_id, :settings, :skip_audience, :skip_authnstatement, :skip_conditions,
|
|
38
38
|
:skip_destination, :skip_recipient_check, :skip_subject_confirmation
|
|
39
39
|
]
|
|
40
40
|
# TODO: Update the comment on initialize to describe every option
|
|
@@ -47,6 +47,8 @@ module OneLogin
|
|
|
47
47
|
# or :matches_request_id that will validate that the response matches the ID of the request,
|
|
48
48
|
# or skip the subject confirmation validation with the :skip_subject_confirmation option
|
|
49
49
|
# or skip the recipient validation of the subject confirmation element with :skip_recipient_check option
|
|
50
|
+
# or skip the audience validation with :skip_audience option
|
|
51
|
+
#
|
|
50
52
|
def initialize(response, options = {})
|
|
51
53
|
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
|
52
54
|
|
|
@@ -61,7 +63,7 @@ module OneLogin
|
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
@response = decode_raw_saml(response)
|
|
66
|
+
@response = decode_raw_saml(response, settings)
|
|
65
67
|
@document = XMLSecurity::SignedDocument.new(@response, @errors)
|
|
66
68
|
|
|
67
69
|
if assertion_encrypted?
|
|
@@ -225,11 +227,10 @@ module OneLogin
|
|
|
225
227
|
statuses = nodes.collect do |inner_node|
|
|
226
228
|
inner_node.attributes["Value"]
|
|
227
229
|
end
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
code = "#{code} | #{extra_code}"
|
|
231
|
-
end
|
|
230
|
+
|
|
231
|
+
code = [code, statuses].flatten.join(" | ")
|
|
232
232
|
end
|
|
233
|
+
|
|
233
234
|
code
|
|
234
235
|
end
|
|
235
236
|
end
|
|
@@ -336,9 +337,31 @@ module OneLogin
|
|
|
336
337
|
end
|
|
337
338
|
|
|
338
339
|
# returns the allowed clock drift on timing validation
|
|
339
|
-
# @return [
|
|
340
|
+
# @return [Float]
|
|
340
341
|
def allowed_clock_drift
|
|
341
|
-
|
|
342
|
+
options[:allowed_clock_drift].to_f.abs + Float::EPSILON
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Checks if the SAML Response contains or not an EncryptedAssertion element
|
|
346
|
+
# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
|
|
347
|
+
#
|
|
348
|
+
def assertion_encrypted?
|
|
349
|
+
! REXML::XPath.first(
|
|
350
|
+
document,
|
|
351
|
+
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
352
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
|
353
|
+
).nil?
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def response_id
|
|
357
|
+
id(document)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def assertion_id
|
|
361
|
+
@assertion_id ||= begin
|
|
362
|
+
node = xpath_first_from_signed_assertion("")
|
|
363
|
+
node.nil? ? nil : node.attributes['ID']
|
|
364
|
+
end
|
|
342
365
|
end
|
|
343
366
|
|
|
344
367
|
private
|
|
@@ -353,7 +376,6 @@ module OneLogin
|
|
|
353
376
|
return false unless validate_response_state
|
|
354
377
|
|
|
355
378
|
validations = [
|
|
356
|
-
:validate_response_state,
|
|
357
379
|
:validate_version,
|
|
358
380
|
:validate_id,
|
|
359
381
|
:validate_success_status,
|
|
@@ -435,7 +457,7 @@ module OneLogin
|
|
|
435
457
|
# @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
|
|
436
458
|
#
|
|
437
459
|
def validate_id
|
|
438
|
-
unless
|
|
460
|
+
unless response_id
|
|
439
461
|
return append_error("Missing ID attribute on SAML Response")
|
|
440
462
|
end
|
|
441
463
|
|
|
@@ -584,16 +606,23 @@ module OneLogin
|
|
|
584
606
|
end
|
|
585
607
|
|
|
586
608
|
# Validates the Audience, (If the Audience match the Service Provider EntityID)
|
|
609
|
+
# If the response was initialized with the :skip_audience option, this validation is skipped,
|
|
587
610
|
# If fails, the error is added to the errors array
|
|
588
611
|
# @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
|
|
589
612
|
# @raise [ValidationError] if soft == false and validation fails
|
|
590
613
|
#
|
|
591
614
|
def validate_audience
|
|
592
|
-
return true if
|
|
615
|
+
return true if options[:skip_audience]
|
|
616
|
+
return true if settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
|
617
|
+
|
|
618
|
+
if audiences.empty?
|
|
619
|
+
return true unless settings.security[:strict_audience_validation]
|
|
620
|
+
return append_error("Invalid Audiences. The <AudienceRestriction> element contained only empty <Audience> elements. Expected audience #{settings.sp_entity_id}.")
|
|
621
|
+
end
|
|
593
622
|
|
|
594
|
-
unless audiences.include? settings.
|
|
623
|
+
unless audiences.include? settings.sp_entity_id
|
|
595
624
|
s = audiences.count > 1 ? 's' : '';
|
|
596
|
-
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.
|
|
625
|
+
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
|
|
597
626
|
return append_error(error_msg)
|
|
598
627
|
end
|
|
599
628
|
|
|
@@ -668,13 +697,13 @@ module OneLogin
|
|
|
668
697
|
|
|
669
698
|
now = Time.now.utc
|
|
670
699
|
|
|
671
|
-
if not_before &&
|
|
672
|
-
error_msg = "Current time is earlier than NotBefore condition (#{
|
|
700
|
+
if not_before && now < (not_before - allowed_clock_drift)
|
|
701
|
+
error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
|
|
673
702
|
return append_error(error_msg)
|
|
674
703
|
end
|
|
675
704
|
|
|
676
|
-
if not_on_or_after && now >= (
|
|
677
|
-
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{
|
|
705
|
+
if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
|
|
706
|
+
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
|
|
678
707
|
return append_error(error_msg)
|
|
679
708
|
end
|
|
680
709
|
|
|
@@ -716,7 +745,7 @@ module OneLogin
|
|
|
716
745
|
return true if session_expires_at.nil?
|
|
717
746
|
|
|
718
747
|
now = Time.now.utc
|
|
719
|
-
unless (session_expires_at + allowed_clock_drift)
|
|
748
|
+
unless now < (session_expires_at + allowed_clock_drift)
|
|
720
749
|
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
|
|
721
750
|
return append_error(error_msg)
|
|
722
751
|
end
|
|
@@ -754,8 +783,8 @@ module OneLogin
|
|
|
754
783
|
|
|
755
784
|
attrs = confirmation_data_node.attributes
|
|
756
785
|
next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
|
|
757
|
-
(attrs.include? "
|
|
758
|
-
(attrs.include? "
|
|
786
|
+
(attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
|
|
787
|
+
(attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
|
|
759
788
|
(attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
|
|
760
789
|
|
|
761
790
|
valid_subject_confirmation = true
|
|
@@ -781,8 +810,8 @@ module OneLogin
|
|
|
781
810
|
return append_error("An empty NameID value found")
|
|
782
811
|
end
|
|
783
812
|
|
|
784
|
-
unless settings.
|
|
785
|
-
if name_id_spnamequalifier != settings.
|
|
813
|
+
unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
|
|
814
|
+
if name_id_spnamequalifier != settings.sp_entity_id
|
|
786
815
|
return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
|
|
787
816
|
end
|
|
788
817
|
end
|
|
@@ -802,7 +831,7 @@ module OneLogin
|
|
|
802
831
|
# otherwise, review if the decrypted assertion contains a signature
|
|
803
832
|
sig_elements = REXML::XPath.match(
|
|
804
833
|
document,
|
|
805
|
-
"/p:Response[@ID=$id]/ds:Signature
|
|
834
|
+
"/p:Response[@ID=$id]/ds:Signature",
|
|
806
835
|
{ "p" => PROTOCOL, "ds" => DSIG },
|
|
807
836
|
{ 'id' => document.signed_element_id }
|
|
808
837
|
)
|
|
@@ -821,28 +850,59 @@ module OneLogin
|
|
|
821
850
|
end
|
|
822
851
|
|
|
823
852
|
if sig_elements.size != 1
|
|
853
|
+
if sig_elements.size == 0
|
|
854
|
+
append_error("Signed element id ##{doc.signed_element_id} is not found")
|
|
855
|
+
else
|
|
856
|
+
append_error("Signed element id ##{doc.signed_element_id} is found more than once")
|
|
857
|
+
end
|
|
824
858
|
return append_error(error_msg)
|
|
825
859
|
end
|
|
826
860
|
|
|
861
|
+
old_errors = @errors.clone
|
|
862
|
+
|
|
827
863
|
idp_certs = settings.get_idp_cert_multi
|
|
828
864
|
if idp_certs.nil? || idp_certs[:signing].empty?
|
|
829
865
|
opts = {}
|
|
830
866
|
opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
|
|
831
|
-
|
|
867
|
+
idp_cert = settings.get_idp_cert
|
|
832
868
|
fingerprint = settings.get_fingerprint
|
|
869
|
+
opts[:cert] = idp_cert
|
|
833
870
|
|
|
834
|
-
|
|
871
|
+
if fingerprint && doc.validate_document(fingerprint, @soft, opts)
|
|
872
|
+
if settings.security[:check_idp_cert_expiration]
|
|
873
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
874
|
+
error_msg = "IdP x509 certificate expired"
|
|
875
|
+
return append_error(error_msg)
|
|
876
|
+
end
|
|
877
|
+
end
|
|
878
|
+
else
|
|
835
879
|
return append_error(error_msg)
|
|
836
880
|
end
|
|
837
881
|
else
|
|
838
882
|
valid = false
|
|
883
|
+
expired = false
|
|
839
884
|
idp_certs[:signing].each do |idp_cert|
|
|
840
|
-
valid = doc.validate_document_with_cert(idp_cert)
|
|
885
|
+
valid = doc.validate_document_with_cert(idp_cert, true)
|
|
841
886
|
if valid
|
|
887
|
+
if settings.security[:check_idp_cert_expiration]
|
|
888
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
889
|
+
expired = true
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
# At least one certificate is valid, restore the old accumulated errors
|
|
894
|
+
@errors = old_errors
|
|
842
895
|
break
|
|
843
896
|
end
|
|
897
|
+
|
|
898
|
+
end
|
|
899
|
+
if expired
|
|
900
|
+
error_msg = "IdP x509 certificate expired"
|
|
901
|
+
return append_error(error_msg)
|
|
844
902
|
end
|
|
845
903
|
unless valid
|
|
904
|
+
# Remove duplicated errors
|
|
905
|
+
@errors = @errors.uniq
|
|
846
906
|
return append_error(error_msg)
|
|
847
907
|
end
|
|
848
908
|
end
|
|
@@ -943,17 +1003,6 @@ module OneLogin
|
|
|
943
1003
|
XMLSecurity::SignedDocument.new(response_node.to_s)
|
|
944
1004
|
end
|
|
945
1005
|
|
|
946
|
-
# Checks if the SAML Response contains or not an EncryptedAssertion element
|
|
947
|
-
# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
|
|
948
|
-
#
|
|
949
|
-
def assertion_encrypted?
|
|
950
|
-
! REXML::XPath.first(
|
|
951
|
-
document,
|
|
952
|
-
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
953
|
-
{ "p" => PROTOCOL, "a" => ASSERTION }
|
|
954
|
-
).nil?
|
|
955
|
-
end
|
|
956
|
-
|
|
957
1006
|
# Decrypts an EncryptedAssertion element
|
|
958
1007
|
# @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
|
|
959
1008
|
# @return [REXML::Document] The decrypted EncryptedAssertion element
|
|
@@ -16,8 +16,8 @@ module OneLogin
|
|
|
16
16
|
class SamlMessage
|
|
17
17
|
include REXML
|
|
18
18
|
|
|
19
|
-
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
|
20
|
-
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
|
19
|
+
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion".freeze
|
|
20
|
+
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol".freeze
|
|
21
21
|
|
|
22
22
|
BASE64_FORMAT = %r(\A([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?\Z)
|
|
23
23
|
@@mutex = Mutex.new
|
|
@@ -86,9 +86,14 @@ module OneLogin
|
|
|
86
86
|
# @param saml [String] The deflated and encoded SAML Message
|
|
87
87
|
# @return [String] The plain SAML Message
|
|
88
88
|
#
|
|
89
|
-
def decode_raw_saml(saml)
|
|
89
|
+
def decode_raw_saml(saml, settings = nil)
|
|
90
90
|
return saml unless base64_encoded?(saml)
|
|
91
91
|
|
|
92
|
+
settings = OneLogin::RubySaml::Settings.new if settings.nil?
|
|
93
|
+
if saml.bytesize > settings.message_max_bytesize
|
|
94
|
+
raise ValidationError.new("Encoded SAML Message exceeds " + settings.message_max_bytesize.to_s + " bytes, so was rejected")
|
|
95
|
+
end
|
|
96
|
+
|
|
92
97
|
decoded = decode(saml)
|
|
93
98
|
begin
|
|
94
99
|
inflate(decoded)
|
|
@@ -105,7 +110,7 @@ module OneLogin
|
|
|
105
110
|
def encode_raw_saml(saml, settings)
|
|
106
111
|
saml = deflate(saml) if settings.compress_request
|
|
107
112
|
|
|
108
|
-
CGI.escape(
|
|
113
|
+
CGI.escape(encode(saml))
|
|
109
114
|
end
|
|
110
115
|
|
|
111
116
|
# Base 64 decode method
|
|
@@ -121,7 +126,11 @@ module OneLogin
|
|
|
121
126
|
# @return [String] The encoded string
|
|
122
127
|
#
|
|
123
128
|
def encode(string)
|
|
124
|
-
Base64.
|
|
129
|
+
if Base64.respond_to?('strict_encode64')
|
|
130
|
+
Base64.strict_encode64(string)
|
|
131
|
+
else
|
|
132
|
+
Base64.encode64(string).gsub(/\n/, "")
|
|
133
|
+
end
|
|
125
134
|
end
|
|
126
135
|
|
|
127
136
|
# Check if a string is base64 encoded
|