ruby-saml 1.8.0 → 1.13.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 +4 -4
- data/.github/workflows/test.yml +25 -0
- data/{changelog.md → CHANGELOG.md} +66 -1
- data/README.md +365 -209
- data/UPGRADING.md +149 -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 +25 -9
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +285 -184
- data/lib/onelogin/ruby-saml/logging.rb +4 -1
- data/lib/onelogin/ruby-saml/logoutrequest.rb +25 -10
- data/lib/onelogin/ruby-saml/logoutresponse.rb +33 -17
- data/lib/onelogin/ruby-saml/metadata.rb +62 -17
- data/lib/onelogin/ruby-saml/response.rb +89 -45
- data/lib/onelogin/ruby-saml/saml_message.rb +17 -8
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +124 -43
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +38 -11
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +42 -19
- data/lib/onelogin/ruby-saml/utils.rb +94 -12
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +44 -19
- data/ruby-saml.gemspec +16 -8
- metadata +44 -282
- data/.travis.yml +0 -26
- 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 -1586
- 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_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 -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
|
|
@@ -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
|
|
@@ -24,7 +24,7 @@ module OneLogin
|
|
|
24
24
|
# Constructs the Logout Response. A Logout Response Object that is an extension of the SamlMessage class.
|
|
25
25
|
# @param response [String] A UUEncoded logout response from the IdP.
|
|
26
26
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
27
|
-
# @param options [Hash] Extra parameters.
|
|
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
30
|
# :relax_signature_validation to accept signatures if no idp certificate registered on settings
|
|
@@ -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
|
|
@@ -146,7 +147,7 @@ module OneLogin
|
|
|
146
147
|
|
|
147
148
|
# Validates the Logout Response against the specified schema.
|
|
148
149
|
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
|
|
149
|
-
# @raise [ValidationError] if soft == false and validation fails
|
|
150
|
+
# @raise [ValidationError] if soft == false and validation fails
|
|
150
151
|
#
|
|
151
152
|
def validate_structure
|
|
152
153
|
unless valid_saml?(document, soft)
|
|
@@ -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")
|
|
@@ -205,7 +206,7 @@ module OneLogin
|
|
|
205
206
|
# Validates the Signature if it exists and the GET parameters are provided
|
|
206
207
|
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
|
|
207
208
|
# @raise [ValidationError] if soft == false and validation fails
|
|
208
|
-
#
|
|
209
|
+
#
|
|
209
210
|
def validate_signature
|
|
210
211
|
return true unless !options.nil?
|
|
211
212
|
return true unless options.has_key? :get_params
|
|
@@ -231,33 +232,48 @@ 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
|
-
idp_certs[:signing].each do |
|
|
250
|
+
idp_certs[:signing].each do |signing_idp_cert|
|
|
244
251
|
valid = OneLogin::RubySaml::Utils.verify_signature(
|
|
245
|
-
:cert =>
|
|
252
|
+
:cert => signing_idp_cert,
|
|
246
253
|
:sig_alg => options[:get_params]['SigAlg'],
|
|
247
254
|
:signature => options[:get_params]['Signature'],
|
|
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)
|
|
259
275
|
end
|
|
260
|
-
true
|
|
276
|
+
true
|
|
261
277
|
end
|
|
262
278
|
|
|
263
279
|
end
|
|
@@ -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?
|
|
@@ -171,9 +173,10 @@ module OneLogin
|
|
|
171
173
|
# identify the subject in an SP rather than email or other less opaque attributes
|
|
172
174
|
# NameQualifier, if present is prefixed with a "/" to the value
|
|
173
175
|
else
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect do |n|
|
|
177
|
+
base_path = n.attributes['NameQualifier'] ? "#{n.attributes['NameQualifier']}/" : ''
|
|
178
|
+
"#{base_path}#{Utils.element_text(n)}"
|
|
179
|
+
end
|
|
177
180
|
end
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -221,14 +224,13 @@ module OneLogin
|
|
|
221
224
|
"/p:Response/p:Status/p:StatusCode/p:StatusCode",
|
|
222
225
|
{ "p" => PROTOCOL }
|
|
223
226
|
)
|
|
224
|
-
statuses = nodes.collect do |
|
|
225
|
-
|
|
226
|
-
end
|
|
227
|
-
extra_code = statuses.join(" | ")
|
|
228
|
-
if extra_code
|
|
229
|
-
code = "#{code} | #{extra_code}"
|
|
227
|
+
statuses = nodes.collect do |inner_node|
|
|
228
|
+
inner_node.attributes["Value"]
|
|
230
229
|
end
|
|
230
|
+
|
|
231
|
+
code = [code, statuses].flatten.join(" | ")
|
|
231
232
|
end
|
|
233
|
+
|
|
232
234
|
code
|
|
233
235
|
end
|
|
234
236
|
end
|
|
@@ -288,7 +290,6 @@ module OneLogin
|
|
|
288
290
|
raise ValidationError.new(error_msg)
|
|
289
291
|
end
|
|
290
292
|
|
|
291
|
-
doc = decrypted_document.nil? ? document : decrypted_document
|
|
292
293
|
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
|
|
293
294
|
unless issuer_assertion_nodes.size == 1
|
|
294
295
|
error_msg = "Issuer of the Assertion not found or multiple."
|
|
@@ -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,18 @@ 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 audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
|
593
617
|
|
|
594
|
-
unless audiences.include? settings.
|
|
618
|
+
unless audiences.include? settings.sp_entity_id
|
|
595
619
|
s = audiences.count > 1 ? 's' : '';
|
|
596
|
-
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.
|
|
620
|
+
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
|
|
597
621
|
return append_error(error_msg)
|
|
598
622
|
end
|
|
599
623
|
|
|
@@ -668,13 +692,13 @@ module OneLogin
|
|
|
668
692
|
|
|
669
693
|
now = Time.now.utc
|
|
670
694
|
|
|
671
|
-
if not_before &&
|
|
672
|
-
error_msg = "Current time is earlier than NotBefore condition (#{
|
|
695
|
+
if not_before && now < (not_before - allowed_clock_drift)
|
|
696
|
+
error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
|
|
673
697
|
return append_error(error_msg)
|
|
674
698
|
end
|
|
675
699
|
|
|
676
|
-
if not_on_or_after && now >= (
|
|
677
|
-
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{
|
|
700
|
+
if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
|
|
701
|
+
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
702
|
return append_error(error_msg)
|
|
679
703
|
end
|
|
680
704
|
|
|
@@ -709,15 +733,15 @@ module OneLogin
|
|
|
709
733
|
# this time validation is relaxed by the allowed_clock_drift value)
|
|
710
734
|
# If fails, the error is added to the errors array
|
|
711
735
|
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
|
|
712
|
-
# @return [Boolean] True if the SessionNotOnOrAfter of the
|
|
736
|
+
# @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True
|
|
713
737
|
# @raise [ValidationError] if soft == false and validation fails
|
|
714
738
|
#
|
|
715
739
|
def validate_session_expiration(soft = true)
|
|
716
740
|
return true if session_expires_at.nil?
|
|
717
741
|
|
|
718
742
|
now = Time.now.utc
|
|
719
|
-
unless (session_expires_at + allowed_clock_drift)
|
|
720
|
-
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the
|
|
743
|
+
unless now < (session_expires_at + allowed_clock_drift)
|
|
744
|
+
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
|
|
721
745
|
return append_error(error_msg)
|
|
722
746
|
end
|
|
723
747
|
|
|
@@ -754,8 +778,8 @@ module OneLogin
|
|
|
754
778
|
|
|
755
779
|
attrs = confirmation_data_node.attributes
|
|
756
780
|
next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
|
|
757
|
-
(attrs.include? "
|
|
758
|
-
(attrs.include? "
|
|
781
|
+
(attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
|
|
782
|
+
(attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
|
|
759
783
|
(attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
|
|
760
784
|
|
|
761
785
|
valid_subject_confirmation = true
|
|
@@ -781,8 +805,8 @@ module OneLogin
|
|
|
781
805
|
return append_error("An empty NameID value found")
|
|
782
806
|
end
|
|
783
807
|
|
|
784
|
-
unless settings.
|
|
785
|
-
if name_id_spnamequalifier != settings.
|
|
808
|
+
unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
|
|
809
|
+
if name_id_spnamequalifier != settings.sp_entity_id
|
|
786
810
|
return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
|
|
787
811
|
end
|
|
788
812
|
end
|
|
@@ -802,7 +826,7 @@ module OneLogin
|
|
|
802
826
|
# otherwise, review if the decrypted assertion contains a signature
|
|
803
827
|
sig_elements = REXML::XPath.match(
|
|
804
828
|
document,
|
|
805
|
-
"/p:Response[@ID=$id]/ds:Signature
|
|
829
|
+
"/p:Response[@ID=$id]/ds:Signature",
|
|
806
830
|
{ "p" => PROTOCOL, "ds" => DSIG },
|
|
807
831
|
{ 'id' => document.signed_element_id }
|
|
808
832
|
)
|
|
@@ -821,28 +845,59 @@ module OneLogin
|
|
|
821
845
|
end
|
|
822
846
|
|
|
823
847
|
if sig_elements.size != 1
|
|
848
|
+
if sig_elements.size == 0
|
|
849
|
+
append_error("Signed element id ##{doc.signed_element_id} is not found")
|
|
850
|
+
else
|
|
851
|
+
append_error("Signed element id ##{doc.signed_element_id} is found more than once")
|
|
852
|
+
end
|
|
824
853
|
return append_error(error_msg)
|
|
825
854
|
end
|
|
826
855
|
|
|
856
|
+
old_errors = @errors.clone
|
|
857
|
+
|
|
827
858
|
idp_certs = settings.get_idp_cert_multi
|
|
828
859
|
if idp_certs.nil? || idp_certs[:signing].empty?
|
|
829
860
|
opts = {}
|
|
830
861
|
opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
|
|
831
|
-
|
|
862
|
+
idp_cert = settings.get_idp_cert
|
|
832
863
|
fingerprint = settings.get_fingerprint
|
|
864
|
+
opts[:cert] = idp_cert
|
|
833
865
|
|
|
834
|
-
|
|
866
|
+
if fingerprint && doc.validate_document(fingerprint, @soft, opts)
|
|
867
|
+
if settings.security[:check_idp_cert_expiration]
|
|
868
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
869
|
+
error_msg = "IdP x509 certificate expired"
|
|
870
|
+
return append_error(error_msg)
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
else
|
|
835
874
|
return append_error(error_msg)
|
|
836
875
|
end
|
|
837
876
|
else
|
|
838
877
|
valid = false
|
|
878
|
+
expired = false
|
|
839
879
|
idp_certs[:signing].each do |idp_cert|
|
|
840
|
-
valid = doc.validate_document_with_cert(idp_cert)
|
|
880
|
+
valid = doc.validate_document_with_cert(idp_cert, true)
|
|
841
881
|
if valid
|
|
882
|
+
if settings.security[:check_idp_cert_expiration]
|
|
883
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
884
|
+
expired = true
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# At least one certificate is valid, restore the old accumulated errors
|
|
889
|
+
@errors = old_errors
|
|
842
890
|
break
|
|
843
891
|
end
|
|
892
|
+
|
|
893
|
+
end
|
|
894
|
+
if expired
|
|
895
|
+
error_msg = "IdP x509 certificate expired"
|
|
896
|
+
return append_error(error_msg)
|
|
844
897
|
end
|
|
845
898
|
unless valid
|
|
899
|
+
# Remove duplicated errors
|
|
900
|
+
@errors = @errors.uniq
|
|
846
901
|
return append_error(error_msg)
|
|
847
902
|
end
|
|
848
903
|
end
|
|
@@ -943,17 +998,6 @@ module OneLogin
|
|
|
943
998
|
XMLSecurity::SignedDocument.new(response_node.to_s)
|
|
944
999
|
end
|
|
945
1000
|
|
|
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
1001
|
# Decrypts an EncryptedAssertion element
|
|
958
1002
|
# @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
|
|
959
1003
|
# @return [REXML::Document] The decrypted EncryptedAssertion element
|