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,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
|
|
@@ -47,15 +47,16 @@ module OneLogin
|
|
|
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,9 +15,11 @@ 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
|
|
22
24
|
namespaces = {
|
|
23
25
|
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
|
|
@@ -57,8 +59,14 @@ module OneLogin
|
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
root.attributes["ID"] = OneLogin::RubySaml::Utils.uuid
|
|
60
|
-
if settings.
|
|
61
|
-
root.attributes["entityID"] = settings.
|
|
62
|
+
if settings.sp_entity_id
|
|
63
|
+
root.attributes["entityID"] = settings.sp_entity_id
|
|
64
|
+
end
|
|
65
|
+
if valid_until
|
|
66
|
+
root.attributes["validUntil"] = valid_until.strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
67
|
+
end
|
|
68
|
+
if cache_duration
|
|
69
|
+
root.attributes["cacheDuration"] = "PT" + cache_duration.to_s + "S"
|
|
62
70
|
end
|
|
63
71
|
if settings.single_logout_service_url
|
|
64
72
|
sp_sso.add_element "md:SingleLogoutService", {
|
|
@@ -30,6 +30,15 @@ module OneLogin
|
|
|
30
30
|
|
|
31
31
|
attr_accessor :soft
|
|
32
32
|
|
|
33
|
+
# Response available options
|
|
34
|
+
# This is not a whitelist to allow people extending OneLogin::RubySaml:Response
|
|
35
|
+
# and pass custom options
|
|
36
|
+
AVAILABLE_OPTIONS = [
|
|
37
|
+
:allowed_clock_drift, :check_duplicated_attributes, :matches_request_id, :settings, :skip_audience, :skip_authnstatement, :skip_conditions,
|
|
38
|
+
:skip_destination, :skip_recipient_check, :skip_subject_confirmation
|
|
39
|
+
]
|
|
40
|
+
# TODO: Update the comment on initialize to describe every option
|
|
41
|
+
|
|
33
42
|
# Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class.
|
|
34
43
|
# @param response [String] A UUEncoded SAML response from the IdP.
|
|
35
44
|
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
|
|
@@ -38,6 +47,8 @@ module OneLogin
|
|
|
38
47
|
# or :matches_request_id that will validate that the response matches the ID of the request,
|
|
39
48
|
# or skip the subject confirmation validation with the :skip_subject_confirmation option
|
|
40
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
|
+
#
|
|
41
52
|
def initialize(response, options = {})
|
|
42
53
|
raise ArgumentError.new("Response cannot be nil") if response.nil?
|
|
43
54
|
|
|
@@ -162,9 +173,10 @@ module OneLogin
|
|
|
162
173
|
# identify the subject in an SP rather than email or other less opaque attributes
|
|
163
174
|
# NameQualifier, if present is prefixed with a "/" to the value
|
|
164
175
|
else
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
180
|
end
|
|
169
181
|
}
|
|
170
182
|
|
|
@@ -212,8 +224,8 @@ module OneLogin
|
|
|
212
224
|
"/p:Response/p:Status/p:StatusCode/p:StatusCode",
|
|
213
225
|
{ "p" => PROTOCOL }
|
|
214
226
|
)
|
|
215
|
-
statuses = nodes.collect do |
|
|
216
|
-
|
|
227
|
+
statuses = nodes.collect do |inner_node|
|
|
228
|
+
inner_node.attributes["Value"]
|
|
217
229
|
end
|
|
218
230
|
extra_code = statuses.join(" | ")
|
|
219
231
|
if extra_code
|
|
@@ -279,7 +291,6 @@ module OneLogin
|
|
|
279
291
|
raise ValidationError.new(error_msg)
|
|
280
292
|
end
|
|
281
293
|
|
|
282
|
-
doc = decrypted_document.nil? ? document : decrypted_document
|
|
283
294
|
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
|
|
284
295
|
unless issuer_assertion_nodes.size == 1
|
|
285
296
|
error_msg = "Issuer of the Assertion not found or multiple."
|
|
@@ -329,7 +340,29 @@ module OneLogin
|
|
|
329
340
|
# returns the allowed clock drift on timing validation
|
|
330
341
|
# @return [Integer]
|
|
331
342
|
def allowed_clock_drift
|
|
332
|
-
return options[:allowed_clock_drift]
|
|
343
|
+
return options[:allowed_clock_drift].to_f
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Checks if the SAML Response contains or not an EncryptedAssertion element
|
|
347
|
+
# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
|
|
348
|
+
#
|
|
349
|
+
def assertion_encrypted?
|
|
350
|
+
! REXML::XPath.first(
|
|
351
|
+
document,
|
|
352
|
+
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
353
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
|
354
|
+
).nil?
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def response_id
|
|
358
|
+
id(document)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def assertion_id
|
|
362
|
+
@assertion_id ||= begin
|
|
363
|
+
node = xpath_first_from_signed_assertion("")
|
|
364
|
+
node.nil? ? nil : node.attributes['ID']
|
|
365
|
+
end
|
|
333
366
|
end
|
|
334
367
|
|
|
335
368
|
private
|
|
@@ -426,7 +459,7 @@ module OneLogin
|
|
|
426
459
|
# @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
|
|
427
460
|
#
|
|
428
461
|
def validate_id
|
|
429
|
-
unless
|
|
462
|
+
unless response_id
|
|
430
463
|
return append_error("Missing ID attribute on SAML Response")
|
|
431
464
|
end
|
|
432
465
|
|
|
@@ -575,15 +608,18 @@ module OneLogin
|
|
|
575
608
|
end
|
|
576
609
|
|
|
577
610
|
# Validates the Audience, (If the Audience match the Service Provider EntityID)
|
|
611
|
+
# If the response was initialized with the :skip_audience option, this validation is skipped,
|
|
578
612
|
# If fails, the error is added to the errors array
|
|
579
613
|
# @return [Boolean] True if there is an Audience Element that match the Service Provider EntityID, otherwise False if soft=True
|
|
580
614
|
# @raise [ValidationError] if soft == false and validation fails
|
|
581
615
|
#
|
|
582
616
|
def validate_audience
|
|
583
|
-
return true if
|
|
617
|
+
return true if options[:skip_audience]
|
|
618
|
+
return true if audiences.empty? || settings.sp_entity_id.nil? || settings.sp_entity_id.empty?
|
|
584
619
|
|
|
585
|
-
unless audiences.include? settings.
|
|
586
|
-
|
|
620
|
+
unless audiences.include? settings.sp_entity_id
|
|
621
|
+
s = audiences.count > 1 ? 's' : '';
|
|
622
|
+
error_msg = "Invalid Audience#{s}. The audience#{s} #{audiences.join(',')}, did not match the expected audience #{settings.sp_entity_id}"
|
|
587
623
|
return append_error(error_msg)
|
|
588
624
|
end
|
|
589
625
|
|
|
@@ -615,10 +651,13 @@ module OneLogin
|
|
|
615
651
|
end
|
|
616
652
|
|
|
617
653
|
# Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
|
|
654
|
+
# (If the response was initialized with the :skip_conditions option, this validation is skipped)
|
|
618
655
|
# If fails, the error is added to the errors array
|
|
619
656
|
# @return [Boolean] True if there is a conditions element and is unique
|
|
620
657
|
#
|
|
621
658
|
def validate_one_conditions
|
|
659
|
+
return true if options[:skip_conditions]
|
|
660
|
+
|
|
622
661
|
conditions_nodes = xpath_from_signed_assertion('/a:Conditions')
|
|
623
662
|
unless conditions_nodes.size == 1
|
|
624
663
|
error_msg = "The Assertion must include one Conditions element"
|
|
@@ -633,6 +672,8 @@ module OneLogin
|
|
|
633
672
|
# @return [Boolean] True if there is a authnstatement element and is unique
|
|
634
673
|
#
|
|
635
674
|
def validate_one_authnstatement
|
|
675
|
+
return true if options[:skip_authnstatement]
|
|
676
|
+
|
|
636
677
|
authnstatement_nodes = xpath_from_signed_assertion('/a:AuthnStatement')
|
|
637
678
|
unless authnstatement_nodes.size == 1
|
|
638
679
|
error_msg = "The Assertion must include one AuthnStatement element"
|
|
@@ -694,7 +735,7 @@ module OneLogin
|
|
|
694
735
|
# this time validation is relaxed by the allowed_clock_drift value)
|
|
695
736
|
# If fails, the error is added to the errors array
|
|
696
737
|
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the response is invalid or not)
|
|
697
|
-
# @return [Boolean] True if the SessionNotOnOrAfter of the
|
|
738
|
+
# @return [Boolean] True if the SessionNotOnOrAfter of the AuthnStatement is valid, otherwise (when expired) False if soft=True
|
|
698
739
|
# @raise [ValidationError] if soft == false and validation fails
|
|
699
740
|
#
|
|
700
741
|
def validate_session_expiration(soft = true)
|
|
@@ -702,7 +743,7 @@ module OneLogin
|
|
|
702
743
|
|
|
703
744
|
now = Time.now.utc
|
|
704
745
|
unless (session_expires_at + allowed_clock_drift) > now
|
|
705
|
-
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the
|
|
746
|
+
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
|
|
706
747
|
return append_error(error_msg)
|
|
707
748
|
end
|
|
708
749
|
|
|
@@ -766,8 +807,8 @@ module OneLogin
|
|
|
766
807
|
return append_error("An empty NameID value found")
|
|
767
808
|
end
|
|
768
809
|
|
|
769
|
-
unless settings.
|
|
770
|
-
if name_id_spnamequalifier != settings.
|
|
810
|
+
unless settings.sp_entity_id.nil? || settings.sp_entity_id.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
|
|
811
|
+
if name_id_spnamequalifier != settings.sp_entity_id
|
|
771
812
|
return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
|
|
772
813
|
end
|
|
773
814
|
end
|
|
@@ -787,7 +828,7 @@ module OneLogin
|
|
|
787
828
|
# otherwise, review if the decrypted assertion contains a signature
|
|
788
829
|
sig_elements = REXML::XPath.match(
|
|
789
830
|
document,
|
|
790
|
-
"/p:Response[@ID=$id]/ds:Signature
|
|
831
|
+
"/p:Response[@ID=$id]/ds:Signature",
|
|
791
832
|
{ "p" => PROTOCOL, "ds" => DSIG },
|
|
792
833
|
{ 'id' => document.signed_element_id }
|
|
793
834
|
)
|
|
@@ -806,28 +847,59 @@ module OneLogin
|
|
|
806
847
|
end
|
|
807
848
|
|
|
808
849
|
if sig_elements.size != 1
|
|
850
|
+
if sig_elements.size == 0
|
|
851
|
+
append_error("Signed element id ##{doc.signed_element_id} is not found")
|
|
852
|
+
else
|
|
853
|
+
append_error("Signed element id ##{doc.signed_element_id} is found more than once")
|
|
854
|
+
end
|
|
809
855
|
return append_error(error_msg)
|
|
810
856
|
end
|
|
811
857
|
|
|
858
|
+
old_errors = @errors.clone
|
|
859
|
+
|
|
812
860
|
idp_certs = settings.get_idp_cert_multi
|
|
813
861
|
if idp_certs.nil? || idp_certs[:signing].empty?
|
|
814
862
|
opts = {}
|
|
815
863
|
opts[:fingerprint_alg] = settings.idp_cert_fingerprint_algorithm
|
|
816
|
-
|
|
864
|
+
idp_cert = settings.get_idp_cert
|
|
817
865
|
fingerprint = settings.get_fingerprint
|
|
866
|
+
opts[:cert] = idp_cert
|
|
818
867
|
|
|
819
|
-
|
|
868
|
+
if fingerprint && doc.validate_document(fingerprint, @soft, opts)
|
|
869
|
+
if settings.security[:check_idp_cert_expiration]
|
|
870
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
871
|
+
error_msg = "IdP x509 certificate expired"
|
|
872
|
+
return append_error(error_msg)
|
|
873
|
+
end
|
|
874
|
+
end
|
|
875
|
+
else
|
|
820
876
|
return append_error(error_msg)
|
|
821
877
|
end
|
|
822
878
|
else
|
|
823
879
|
valid = false
|
|
880
|
+
expired = false
|
|
824
881
|
idp_certs[:signing].each do |idp_cert|
|
|
825
|
-
valid = doc.validate_document_with_cert(idp_cert)
|
|
882
|
+
valid = doc.validate_document_with_cert(idp_cert, true)
|
|
826
883
|
if valid
|
|
884
|
+
if settings.security[:check_idp_cert_expiration]
|
|
885
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(idp_cert)
|
|
886
|
+
expired = true
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# At least one certificate is valid, restore the old accumulated errors
|
|
891
|
+
@errors = old_errors
|
|
827
892
|
break
|
|
828
893
|
end
|
|
894
|
+
|
|
895
|
+
end
|
|
896
|
+
if expired
|
|
897
|
+
error_msg = "IdP x509 certificate expired"
|
|
898
|
+
return append_error(error_msg)
|
|
829
899
|
end
|
|
830
900
|
unless valid
|
|
901
|
+
# Remove duplicated errors
|
|
902
|
+
@errors = @errors.uniq
|
|
831
903
|
return append_error(error_msg)
|
|
832
904
|
end
|
|
833
905
|
end
|
|
@@ -928,17 +1000,6 @@ module OneLogin
|
|
|
928
1000
|
XMLSecurity::SignedDocument.new(response_node.to_s)
|
|
929
1001
|
end
|
|
930
1002
|
|
|
931
|
-
# Checks if the SAML Response contains or not an EncryptedAssertion element
|
|
932
|
-
# @return [Boolean] True if the SAML Response contains an EncryptedAssertion element
|
|
933
|
-
#
|
|
934
|
-
def assertion_encrypted?
|
|
935
|
-
! REXML::XPath.first(
|
|
936
|
-
document,
|
|
937
|
-
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
938
|
-
{ "p" => PROTOCOL, "a" => ASSERTION }
|
|
939
|
-
).nil?
|
|
940
|
-
end
|
|
941
|
-
|
|
942
1003
|
# Decrypts an EncryptedAssertion element
|
|
943
1004
|
# @param encrypted_assertion_node [REXML::Element] The EncryptedAssertion element
|
|
944
1005
|
# @return [REXML::Document] The decrypted EncryptedAssertion element
|
|
@@ -22,6 +22,8 @@ module OneLogin
|
|
|
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
|
|
24
24
|
|
|
25
|
+
MAX_BYTE_SIZE = 250000
|
|
26
|
+
|
|
25
27
|
# @return [Nokogiri::XML::Schema] Gets the schema object of the SAML 2.0 Protocol schema
|
|
26
28
|
#
|
|
27
29
|
def self.schema
|
|
@@ -62,7 +64,7 @@ module OneLogin
|
|
|
62
64
|
# @param document [REXML::Document] The message that will be validated
|
|
63
65
|
# @param soft [Boolean] soft Enable or Disable the soft mode (In order to raise exceptions when the message is invalid or not)
|
|
64
66
|
# @return [Boolean] True if the XML is valid, otherwise False, if soft=True
|
|
65
|
-
# @raise [ValidationError] if soft == false and validation fails
|
|
67
|
+
# @raise [ValidationError] if soft == false and validation fails
|
|
66
68
|
#
|
|
67
69
|
def valid_saml?(document, soft = true)
|
|
68
70
|
begin
|
|
@@ -74,9 +76,9 @@ module OneLogin
|
|
|
74
76
|
raise ValidationError.new("XML load failed: #{error.message}")
|
|
75
77
|
end
|
|
76
78
|
|
|
77
|
-
SamlMessage.schema.validate(xml).map do |
|
|
79
|
+
SamlMessage.schema.validate(xml).map do |schema_error|
|
|
78
80
|
return false if soft
|
|
79
|
-
raise ValidationError.new("#{
|
|
81
|
+
raise ValidationError.new("#{schema_error.message}\n\n#{xml.to_s}")
|
|
80
82
|
end
|
|
81
83
|
end
|
|
82
84
|
|
|
@@ -89,6 +91,10 @@ module OneLogin
|
|
|
89
91
|
def decode_raw_saml(saml)
|
|
90
92
|
return saml unless base64_encoded?(saml)
|
|
91
93
|
|
|
94
|
+
if saml.bytesize > MAX_BYTE_SIZE
|
|
95
|
+
raise ValidationError.new("Encoded SAML Message exceeds " + MAX_BYTE_SIZE.to_s + " bytes, so was rejected")
|
|
96
|
+
end
|
|
97
|
+
|
|
92
98
|
decoded = decode(saml)
|
|
93
99
|
begin
|
|
94
100
|
inflate(decoded)
|
|
@@ -105,7 +111,7 @@ module OneLogin
|
|
|
105
111
|
def encode_raw_saml(saml, settings)
|
|
106
112
|
saml = deflate(saml) if settings.compress_request
|
|
107
113
|
|
|
108
|
-
CGI.escape(
|
|
114
|
+
CGI.escape(encode(saml))
|
|
109
115
|
end
|
|
110
116
|
|
|
111
117
|
# Base 64 decode method
|
|
@@ -121,7 +127,11 @@ module OneLogin
|
|
|
121
127
|
# @return [String] The encoded string
|
|
122
128
|
#
|
|
123
129
|
def encode(string)
|
|
124
|
-
Base64.
|
|
130
|
+
if Base64.respond_to?('strict_encode64')
|
|
131
|
+
Base64.strict_encode64(string)
|
|
132
|
+
else
|
|
133
|
+
Base64.encode64(string).gsub(/\n/, "")
|
|
134
|
+
end
|
|
125
135
|
end
|
|
126
136
|
|
|
127
137
|
# Check if a string is base64 encoded
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "xml_security"
|
|
2
2
|
require "onelogin/ruby-saml/attribute_service"
|
|
3
3
|
require "onelogin/ruby-saml/utils"
|
|
4
|
+
require "onelogin/ruby-saml/validation_error"
|
|
4
5
|
|
|
5
6
|
# Only supports SAML 2.0
|
|
6
7
|
module OneLogin
|
|
@@ -9,8 +10,15 @@ module OneLogin
|
|
|
9
10
|
# SAML2 Toolkit Settings
|
|
10
11
|
#
|
|
11
12
|
class Settings
|
|
12
|
-
def initialize(overrides = {})
|
|
13
|
-
|
|
13
|
+
def initialize(overrides = {}, keep_security_attributes = false)
|
|
14
|
+
if keep_security_attributes
|
|
15
|
+
security_attributes = overrides.delete(:security) || {}
|
|
16
|
+
config = DEFAULTS.merge(overrides)
|
|
17
|
+
config[:security] = DEFAULTS[:security].merge(security_attributes)
|
|
18
|
+
else
|
|
19
|
+
config = DEFAULTS.merge(overrides)
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
config.each do |k,v|
|
|
15
23
|
acc = "#{k.to_s}=".to_sym
|
|
16
24
|
if respond_to? acc
|
|
@@ -23,21 +31,24 @@ module OneLogin
|
|
|
23
31
|
|
|
24
32
|
# IdP Data
|
|
25
33
|
attr_accessor :idp_entity_id
|
|
26
|
-
|
|
27
|
-
attr_accessor :
|
|
34
|
+
|
|
35
|
+
attr_accessor :idp_sso_service_url
|
|
36
|
+
attr_accessor :idp_slo_service_url
|
|
37
|
+
attr_accessor :idp_slo_response_service_url
|
|
28
38
|
attr_accessor :idp_cert
|
|
29
39
|
attr_accessor :idp_cert_fingerprint
|
|
30
40
|
attr_accessor :idp_cert_fingerprint_algorithm
|
|
31
41
|
attr_accessor :idp_cert_multi
|
|
32
42
|
attr_accessor :idp_attribute_names
|
|
33
43
|
attr_accessor :idp_name_qualifier
|
|
44
|
+
attr_accessor :valid_until
|
|
34
45
|
# SP Data
|
|
35
|
-
attr_accessor :issuer
|
|
36
46
|
attr_accessor :assertion_consumer_service_url
|
|
37
47
|
attr_accessor :assertion_consumer_service_binding
|
|
38
48
|
attr_accessor :sp_name_qualifier
|
|
39
49
|
attr_accessor :name_identifier_format
|
|
40
50
|
attr_accessor :name_identifier_value
|
|
51
|
+
attr_accessor :name_identifier_value_requested
|
|
41
52
|
attr_accessor :sessionindex
|
|
42
53
|
attr_accessor :compress_request
|
|
43
54
|
attr_accessor :compress_response
|
|
@@ -59,6 +70,58 @@ module OneLogin
|
|
|
59
70
|
# Compability
|
|
60
71
|
attr_accessor :assertion_consumer_logout_service_url
|
|
61
72
|
attr_accessor :assertion_consumer_logout_service_binding
|
|
73
|
+
attr_accessor :issuer
|
|
74
|
+
attr_accessor :idp_sso_target_url
|
|
75
|
+
attr_accessor :idp_slo_target_url
|
|
76
|
+
|
|
77
|
+
# @return [String] IdP Single Sign On Service URL
|
|
78
|
+
#
|
|
79
|
+
def idp_sso_service_url
|
|
80
|
+
val = nil
|
|
81
|
+
if @idp_sso_service_url.nil?
|
|
82
|
+
if @idp_sso_target_url
|
|
83
|
+
val = @idp_sso_target_url
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
val = @idp_sso_service_url
|
|
87
|
+
end
|
|
88
|
+
val
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [String] IdP Single Logout Service URL
|
|
92
|
+
#
|
|
93
|
+
def idp_slo_service_url
|
|
94
|
+
val = nil
|
|
95
|
+
if @idp_slo_service_url.nil?
|
|
96
|
+
if @idp_slo_target_url
|
|
97
|
+
val = @idp_slo_target_url
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
val = @idp_slo_service_url
|
|
101
|
+
end
|
|
102
|
+
val
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @return [String] SP Entity ID
|
|
106
|
+
#
|
|
107
|
+
def sp_entity_id
|
|
108
|
+
val = nil
|
|
109
|
+
if @sp_entity_id.nil?
|
|
110
|
+
if @issuer
|
|
111
|
+
val = @issuer
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
val = @sp_entity_id
|
|
115
|
+
end
|
|
116
|
+
val
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Setter for SP Entity ID.
|
|
120
|
+
# @param val [String].
|
|
121
|
+
#
|
|
122
|
+
def sp_entity_id=(val)
|
|
123
|
+
@sp_entity_id = val
|
|
124
|
+
end
|
|
62
125
|
|
|
63
126
|
# @return [String] Single Logout Service URL.
|
|
64
127
|
#
|
|
@@ -158,7 +221,15 @@ module OneLogin
|
|
|
158
221
|
return nil if certificate.nil? || certificate.empty?
|
|
159
222
|
|
|
160
223
|
formatted_cert = OneLogin::RubySaml::Utils.format_cert(certificate)
|
|
161
|
-
OpenSSL::X509::Certificate.new(formatted_cert)
|
|
224
|
+
cert = OpenSSL::X509::Certificate.new(formatted_cert)
|
|
225
|
+
|
|
226
|
+
if security[:check_sp_cert_expiration]
|
|
227
|
+
if OneLogin::RubySaml::Utils.is_cert_expired(cert)
|
|
228
|
+
raise OneLogin::RubySaml::ValidationError.new("The SP certificate expired.")
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
cert
|
|
162
233
|
end
|
|
163
234
|
|
|
164
235
|
# @return [OpenSSL::X509::Certificate|nil] Build the New SP certificate from the settings (previously format it)
|
|
@@ -188,6 +259,7 @@ module OneLogin
|
|
|
188
259
|
:compress_request => true,
|
|
189
260
|
:compress_response => true,
|
|
190
261
|
:soft => true,
|
|
262
|
+
:double_quote_xml_attribute_values => false,
|
|
191
263
|
:security => {
|
|
192
264
|
:authn_requests_signed => false,
|
|
193
265
|
:logout_requests_signed => false,
|
|
@@ -198,9 +270,10 @@ module OneLogin
|
|
|
198
270
|
:metadata_signed => false,
|
|
199
271
|
:embed_sign => false,
|
|
200
272
|
:digest_method => XMLSecurity::Document::SHA1,
|
|
201
|
-
:signature_method => XMLSecurity::Document::RSA_SHA1
|
|
202
|
-
|
|
203
|
-
|
|
273
|
+
:signature_method => XMLSecurity::Document::RSA_SHA1,
|
|
274
|
+
:check_idp_cert_expiration => false,
|
|
275
|
+
:check_sp_cert_expiration => false
|
|
276
|
+
}.freeze
|
|
204
277
|
}.freeze
|
|
205
278
|
end
|
|
206
279
|
end
|