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
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
require "base64"
|
|
2
|
-
require "zlib"
|
|
3
|
-
require "cgi"
|
|
4
2
|
require "net/http"
|
|
5
3
|
require "net/https"
|
|
6
4
|
require "rexml/document"
|
|
@@ -15,15 +13,37 @@ module OneLogin
|
|
|
15
13
|
#
|
|
16
14
|
class IdpMetadataParser
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
module SamlMetadata
|
|
17
|
+
module Vocabulary
|
|
18
|
+
METADATA = "urn:oasis:names:tc:SAML:2.0:metadata".freeze
|
|
19
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze
|
|
20
|
+
NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*".freeze
|
|
21
|
+
SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion".freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
NAMESPACE = {
|
|
25
|
+
"md" => Vocabulary::METADATA,
|
|
26
|
+
"NameFormat" => Vocabulary::NAME_FORMAT,
|
|
27
|
+
"saml" => Vocabulary::SAML_ASSERTION,
|
|
28
|
+
"ds" => Vocabulary::DSIG
|
|
29
|
+
}.freeze
|
|
30
|
+
end
|
|
22
31
|
|
|
32
|
+
include SamlMetadata::Vocabulary
|
|
23
33
|
attr_reader :document
|
|
24
34
|
attr_reader :response
|
|
25
35
|
attr_reader :options
|
|
26
36
|
|
|
37
|
+
# fetch IdP descriptors from a metadata document
|
|
38
|
+
def self.get_idps(metadata_document, only_entity_id=nil)
|
|
39
|
+
path = "//md:EntityDescriptor#{only_entity_id && '[@entityID="' + only_entity_id + '"]'}/md:IDPSSODescriptor"
|
|
40
|
+
REXML::XPath.match(
|
|
41
|
+
metadata_document,
|
|
42
|
+
path,
|
|
43
|
+
SamlMetadata::NAMESPACE
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
27
47
|
# Parse the Identity Provider metadata and update the settings with the
|
|
28
48
|
# IdP values
|
|
29
49
|
#
|
|
@@ -58,8 +78,25 @@ module OneLogin
|
|
|
58
78
|
#
|
|
59
79
|
# @raise [HttpError] Failure to fetch remote IdP metadata
|
|
60
80
|
def parse_remote_to_hash(url, validate_cert = true, options = {})
|
|
81
|
+
parse_remote_to_array(url, validate_cert, options)[0]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Parse all Identity Provider metadata and return the results as Array
|
|
85
|
+
#
|
|
86
|
+
# @param url [String] Url where the XML of the Identity Provider Metadata is published.
|
|
87
|
+
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
|
|
88
|
+
#
|
|
89
|
+
# @param options [Hash] options used for parsing the metadata
|
|
90
|
+
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
|
|
91
|
+
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
|
|
92
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, all found IdPs are returned.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Hash>]
|
|
95
|
+
#
|
|
96
|
+
# @raise [HttpError] Failure to fetch remote IdP metadata
|
|
97
|
+
def parse_remote_to_array(url, validate_cert = true, options = {})
|
|
61
98
|
idp_metadata = get_idp_metadata(url, validate_cert)
|
|
62
|
-
|
|
99
|
+
parse_to_array(idp_metadata, options)
|
|
63
100
|
end
|
|
64
101
|
|
|
65
102
|
# Parse the Identity Provider metadata and update the settings with the IdP values
|
|
@@ -76,6 +113,16 @@ module OneLogin
|
|
|
76
113
|
def parse(idp_metadata, options = {})
|
|
77
114
|
parsed_metadata = parse_to_hash(idp_metadata, options)
|
|
78
115
|
|
|
116
|
+
unless parsed_metadata[:cache_duration].nil?
|
|
117
|
+
cache_valid_until_timestamp = OneLogin::RubySaml::Utils.parse_duration(parsed_metadata[:cache_duration])
|
|
118
|
+
if parsed_metadata[:valid_until].nil? || cache_valid_until_timestamp < Time.parse(parsed_metadata[:valid_until], Time.now.utc).to_i
|
|
119
|
+
parsed_metadata[:valid_until] = Time.at(cache_valid_until_timestamp).utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
# Remove the cache_duration because on the settings
|
|
123
|
+
# we only gonna suppot valid_until
|
|
124
|
+
parsed_metadata.delete(:cache_duration)
|
|
125
|
+
|
|
79
126
|
settings = options[:settings]
|
|
80
127
|
|
|
81
128
|
if settings.nil?
|
|
@@ -98,28 +145,35 @@ module OneLogin
|
|
|
98
145
|
#
|
|
99
146
|
# @return [Hash]
|
|
100
147
|
def parse_to_hash(idp_metadata, options = {})
|
|
148
|
+
parse_to_array(idp_metadata, options)[0]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Parse all Identity Provider metadata and return the results as Array
|
|
152
|
+
#
|
|
153
|
+
# @param idp_metadata [String]
|
|
154
|
+
#
|
|
155
|
+
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
|
|
156
|
+
# @option options [Array<String>, nil] :sso_binding an ordered list of bindings to detect the single signon URL. The first binding in the list that is included in the metadata will be used.
|
|
157
|
+
# @option options [Array<String>, nil] :slo_binding an ordered list of bindings to detect the single logout URL. The first binding in the list that is included in the metadata will be used.
|
|
158
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, all found IdPs are returned.
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<Hash>]
|
|
161
|
+
def parse_to_array(idp_metadata, options = {})
|
|
162
|
+
parse_to_idp_metadata_array(idp_metadata, options).map{|idp_md| idp_md.to_hash(options)}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_to_idp_metadata_array(idp_metadata, options = {})
|
|
101
166
|
@document = REXML::Document.new(idp_metadata)
|
|
102
167
|
@options = options
|
|
103
|
-
@entity_descriptor = nil
|
|
104
168
|
|
|
105
|
-
|
|
169
|
+
idpsso_descriptors = self.class.get_idps(@document, options[:entity_id])
|
|
170
|
+
if !idpsso_descriptors.any?
|
|
106
171
|
raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element")
|
|
107
172
|
end
|
|
108
173
|
|
|
109
|
-
{
|
|
110
|
-
:idp_entity_id => idp_entity_id,
|
|
111
|
-
:name_identifier_format => idp_name_id_format,
|
|
112
|
-
:idp_sso_target_url => single_signon_service_url(options),
|
|
113
|
-
:idp_slo_target_url => single_logout_service_url(options),
|
|
114
|
-
:idp_attribute_names => attribute_names,
|
|
115
|
-
:idp_cert => nil,
|
|
116
|
-
:idp_cert_fingerprint => nil,
|
|
117
|
-
:idp_cert_multi => nil
|
|
118
|
-
}.tap do |response_hash|
|
|
119
|
-
merge_certificates_into(response_hash) unless certificates.nil?
|
|
120
|
-
end
|
|
174
|
+
return idpsso_descriptors.map{|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
|
|
121
175
|
end
|
|
122
|
-
|
|
176
|
+
|
|
123
177
|
private
|
|
124
178
|
|
|
125
179
|
# Retrieve the remote IdP metadata from the URL or a cached copy.
|
|
@@ -145,7 +199,8 @@ module OneLogin
|
|
|
145
199
|
end
|
|
146
200
|
|
|
147
201
|
get = Net::HTTP::Get.new(uri.request_uri)
|
|
148
|
-
|
|
202
|
+
get.basic_auth uri.user, uri.password if uri.user
|
|
203
|
+
@response = http.request(get)
|
|
149
204
|
return response.body if response.is_a? Net::HTTPSuccess
|
|
150
205
|
|
|
151
206
|
raise OneLogin::RubySaml::HttpError.new(
|
|
@@ -153,130 +208,154 @@ module OneLogin
|
|
|
153
208
|
)
|
|
154
209
|
end
|
|
155
210
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
211
|
+
class IdpMetadata
|
|
212
|
+
attr_reader :idpsso_descriptor, :entity_id
|
|
213
|
+
|
|
214
|
+
def initialize(idpsso_descriptor, entity_id)
|
|
215
|
+
@idpsso_descriptor = idpsso_descriptor
|
|
216
|
+
@entity_id = entity_id
|
|
217
|
+
end
|
|
163
218
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
219
|
+
def to_hash(options = {})
|
|
220
|
+
{
|
|
221
|
+
:idp_entity_id => @entity_id,
|
|
222
|
+
:name_identifier_format => idp_name_id_format,
|
|
223
|
+
:idp_sso_service_url => single_signon_service_url(options),
|
|
224
|
+
:idp_slo_service_url => single_logout_service_url(options),
|
|
225
|
+
:idp_slo_response_service_url => single_logout_response_service_url(options),
|
|
226
|
+
:idp_attribute_names => attribute_names,
|
|
227
|
+
:idp_cert => nil,
|
|
228
|
+
:idp_cert_fingerprint => nil,
|
|
229
|
+
:idp_cert_multi => nil,
|
|
230
|
+
:valid_until => valid_until,
|
|
231
|
+
:cache_duration => cache_duration,
|
|
232
|
+
}.tap do |response_hash|
|
|
233
|
+
merge_certificates_into(response_hash) unless certificates.nil?
|
|
234
|
+
end
|
|
235
|
+
end
|
|
170
236
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
237
|
+
# @return [String|nil] IdP Name ID Format value if exists
|
|
238
|
+
#
|
|
239
|
+
def idp_name_id_format
|
|
240
|
+
node = REXML::XPath.first(
|
|
241
|
+
@idpsso_descriptor,
|
|
242
|
+
"md:NameIDFormat",
|
|
243
|
+
SamlMetadata::NAMESPACE
|
|
177
244
|
)
|
|
245
|
+
Utils.element_text(node)
|
|
178
246
|
end
|
|
179
|
-
end
|
|
180
247
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
248
|
+
# @return [String|nil] 'validUntil' attribute of metadata
|
|
249
|
+
#
|
|
250
|
+
def valid_until
|
|
251
|
+
root = @idpsso_descriptor.root
|
|
252
|
+
root.attributes['validUntil'] if root && root.attributes
|
|
253
|
+
end
|
|
186
254
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
namespace
|
|
194
|
-
)
|
|
195
|
-
Utils.element_text(node)
|
|
196
|
-
end
|
|
255
|
+
# @return [String|nil] 'cacheDuration' attribute of metadata
|
|
256
|
+
#
|
|
257
|
+
def cache_duration
|
|
258
|
+
root = @idpsso_descriptor.root
|
|
259
|
+
root.attributes['cacheDuration'] if root && root.attributes
|
|
260
|
+
end
|
|
197
261
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
262
|
+
# @param binding_priority [Array]
|
|
263
|
+
# @return [String|nil] SingleSignOnService binding if exists
|
|
264
|
+
#
|
|
265
|
+
def single_signon_service_binding(binding_priority = nil)
|
|
266
|
+
nodes = REXML::XPath.match(
|
|
267
|
+
@idpsso_descriptor,
|
|
268
|
+
"md:SingleSignOnService/@Binding",
|
|
269
|
+
SamlMetadata::NAMESPACE
|
|
270
|
+
)
|
|
271
|
+
if binding_priority
|
|
272
|
+
values = nodes.map(&:value)
|
|
273
|
+
binding_priority.detect{ |binding| values.include? binding }
|
|
274
|
+
else
|
|
275
|
+
nodes.first.value if nodes.any?
|
|
276
|
+
end
|
|
212
277
|
end
|
|
213
|
-
end
|
|
214
278
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
279
|
+
# @param options [Hash]
|
|
280
|
+
# @return [String|nil] SingleSignOnService endpoint if exists
|
|
281
|
+
#
|
|
282
|
+
def single_signon_service_url(options = {})
|
|
283
|
+
binding = single_signon_service_binding(options[:sso_binding])
|
|
284
|
+
return if binding.nil?
|
|
285
|
+
|
|
221
286
|
node = REXML::XPath.first(
|
|
222
|
-
|
|
223
|
-
"md:
|
|
224
|
-
|
|
287
|
+
@idpsso_descriptor,
|
|
288
|
+
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
|
|
289
|
+
SamlMetadata::NAMESPACE
|
|
225
290
|
)
|
|
226
291
|
return node.value if node
|
|
227
292
|
end
|
|
228
|
-
end
|
|
229
293
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
294
|
+
# @param binding_priority [Array]
|
|
295
|
+
# @return [String|nil] SingleLogoutService binding if exists
|
|
296
|
+
#
|
|
297
|
+
def single_logout_service_binding(binding_priority = nil)
|
|
298
|
+
nodes = REXML::XPath.match(
|
|
299
|
+
@idpsso_descriptor,
|
|
300
|
+
"md:SingleLogoutService/@Binding",
|
|
301
|
+
SamlMetadata::NAMESPACE
|
|
302
|
+
)
|
|
303
|
+
if binding_priority
|
|
304
|
+
values = nodes.map(&:value)
|
|
305
|
+
binding_priority.detect{ |binding| values.include? binding }
|
|
306
|
+
else
|
|
307
|
+
nodes.first.value if nodes.any?
|
|
308
|
+
end
|
|
244
309
|
end
|
|
245
|
-
end
|
|
246
310
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
311
|
+
# @param options [Hash]
|
|
312
|
+
# @return [String|nil] SingleLogoutService endpoint if exists
|
|
313
|
+
#
|
|
314
|
+
def single_logout_service_url(options = {})
|
|
315
|
+
binding = single_logout_service_binding(options[:slo_binding])
|
|
316
|
+
return if binding.nil?
|
|
317
|
+
|
|
253
318
|
node = REXML::XPath.first(
|
|
254
|
-
|
|
255
|
-
"md:
|
|
256
|
-
|
|
319
|
+
@idpsso_descriptor,
|
|
320
|
+
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
|
|
321
|
+
SamlMetadata::NAMESPACE
|
|
257
322
|
)
|
|
258
323
|
return node.value if node
|
|
259
324
|
end
|
|
260
|
-
end
|
|
261
325
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
"md:IDPSSODescriptor/md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
269
|
-
namespace
|
|
270
|
-
)
|
|
326
|
+
# @param options [Hash]
|
|
327
|
+
# @return [String|nil] SingleLogoutService response url if exists
|
|
328
|
+
#
|
|
329
|
+
def single_logout_response_service_url(options = {})
|
|
330
|
+
binding = single_logout_service_binding(options[:slo_binding])
|
|
331
|
+
return if binding.nil?
|
|
271
332
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"md:
|
|
275
|
-
|
|
333
|
+
node = REXML::XPath.first(
|
|
334
|
+
@idpsso_descriptor,
|
|
335
|
+
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation",
|
|
336
|
+
SamlMetadata::NAMESPACE
|
|
276
337
|
)
|
|
338
|
+
return node.value if node
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# @return [String|nil] Unformatted Certificate if exists
|
|
342
|
+
#
|
|
343
|
+
def certificates
|
|
344
|
+
@certificates ||= begin
|
|
345
|
+
signing_nodes = REXML::XPath.match(
|
|
346
|
+
@idpsso_descriptor,
|
|
347
|
+
"md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
348
|
+
SamlMetadata::NAMESPACE
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
encryption_nodes = REXML::XPath.match(
|
|
352
|
+
@idpsso_descriptor,
|
|
353
|
+
"md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
354
|
+
SamlMetadata::NAMESPACE
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return nil if signing_nodes.empty? && encryption_nodes.empty?
|
|
277
358
|
|
|
278
|
-
certs = nil
|
|
279
|
-
unless signing_nodes.empty? && encryption_nodes.empty?
|
|
280
359
|
certs = {}
|
|
281
360
|
unless signing_nodes.empty?
|
|
282
361
|
certs['signing'] = []
|
|
@@ -291,71 +370,62 @@ module OneLogin
|
|
|
291
370
|
certs['encryption'] << Utils.element_text(cert_node)
|
|
292
371
|
end
|
|
293
372
|
end
|
|
373
|
+
certs
|
|
294
374
|
end
|
|
295
|
-
certs
|
|
296
375
|
end
|
|
297
|
-
end
|
|
298
376
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
377
|
+
# @return [String|nil] the fingerpint of the X509Certificate if it exists
|
|
378
|
+
#
|
|
379
|
+
def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1)
|
|
380
|
+
@fingerprint ||= begin
|
|
381
|
+
return unless certificate
|
|
382
|
+
|
|
304
383
|
cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
|
|
305
384
|
|
|
306
385
|
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new
|
|
307
386
|
fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
|
308
387
|
end
|
|
309
388
|
end
|
|
310
|
-
end
|
|
311
389
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def namespace
|
|
324
|
-
{
|
|
325
|
-
"md" => METADATA,
|
|
326
|
-
"NameFormat" => NAME_FORMAT,
|
|
327
|
-
"saml" => SAML_ASSERTION,
|
|
328
|
-
"ds" => DSIG
|
|
329
|
-
}
|
|
330
|
-
end
|
|
390
|
+
# @return [Array] the names of all SAML attributes if any exist
|
|
391
|
+
#
|
|
392
|
+
def attribute_names
|
|
393
|
+
nodes = REXML::XPath.match(
|
|
394
|
+
@idpsso_descriptor ,
|
|
395
|
+
"saml:Attribute/@Name",
|
|
396
|
+
SamlMetadata::NAMESPACE
|
|
397
|
+
)
|
|
398
|
+
nodes.map(&:value)
|
|
399
|
+
end
|
|
331
400
|
|
|
332
|
-
|
|
333
|
-
|
|
401
|
+
def merge_certificates_into(parsed_metadata)
|
|
402
|
+
if (certificates.size == 1 &&
|
|
334
403
|
(certificates_has_one('signing') || certificates_has_one('encryption'))) ||
|
|
335
404
|
(certificates_has_one('signing') && certificates_has_one('encryption') &&
|
|
336
405
|
certificates["signing"][0] == certificates["encryption"][0])
|
|
337
406
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
407
|
+
if certificates.key?("signing")
|
|
408
|
+
parsed_metadata[:idp_cert] = certificates["signing"][0]
|
|
409
|
+
parsed_metadata[:idp_cert_fingerprint] = fingerprint(
|
|
410
|
+
parsed_metadata[:idp_cert],
|
|
411
|
+
parsed_metadata[:idp_cert_fingerprint_algorithm]
|
|
412
|
+
)
|
|
413
|
+
else
|
|
414
|
+
parsed_metadata[:idp_cert] = certificates["encryption"][0]
|
|
415
|
+
parsed_metadata[:idp_cert_fingerprint] = fingerprint(
|
|
416
|
+
parsed_metadata[:idp_cert],
|
|
417
|
+
parsed_metadata[:idp_cert_fingerprint_algorithm]
|
|
418
|
+
)
|
|
419
|
+
end
|
|
344
420
|
else
|
|
345
|
-
|
|
346
|
-
parsed_metadata[:
|
|
347
|
-
parsed_metadata[:idp_cert],
|
|
348
|
-
parsed_metadata[:idp_cert_fingerprint_algorithm]
|
|
349
|
-
)
|
|
421
|
+
# symbolize keys of certificates and pass it on
|
|
422
|
+
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
|
|
350
423
|
end
|
|
351
|
-
else
|
|
352
|
-
# symbolize keys of certificates and pass it on
|
|
353
|
-
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
|
|
354
424
|
end
|
|
355
|
-
end
|
|
356
425
|
|
|
357
|
-
|
|
358
|
-
|
|
426
|
+
def certificates_has_one(key)
|
|
427
|
+
certificates.key?(key) && certificates[key].size == 1
|
|
428
|
+
end
|
|
359
429
|
end
|
|
360
430
|
|
|
361
431
|
def merge_parsed_metadata_into(settings, parsed_metadata)
|
|
@@ -7,7 +7,10 @@ module OneLogin
|
|
|
7
7
|
DEFAULT_LOGGER = ::Logger.new(STDOUT)
|
|
8
8
|
|
|
9
9
|
def self.logger
|
|
10
|
-
@logger
|
|
10
|
+
@logger ||= begin
|
|
11
|
+
(defined?(::Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
|
12
|
+
DEFAULT_LOGGER
|
|
13
|
+
end
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def self.logger=(logger)
|
|
@@ -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.
|
|
@@ -47,6 +53,11 @@ module OneLogin
|
|
|
47
53
|
# conflicts so this line will solve them.
|
|
48
54
|
relay_state = params[:RelayState] || params['RelayState']
|
|
49
55
|
|
|
56
|
+
if relay_state.nil?
|
|
57
|
+
params.delete(:RelayState)
|
|
58
|
+
params.delete('RelayState')
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
request_doc = create_logout_request_xml_doc(settings)
|
|
51
62
|
request_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
|
|
52
63
|
|
|
@@ -84,6 +95,11 @@ module OneLogin
|
|
|
84
95
|
# @return [String] The SAMLRequest String.
|
|
85
96
|
#
|
|
86
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)
|
|
87
103
|
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
88
104
|
|
|
89
105
|
request_doc = XMLSecurity::Document.new
|
|
@@ -93,11 +109,11 @@ module OneLogin
|
|
|
93
109
|
root.attributes['ID'] = uuid
|
|
94
110
|
root.attributes['IssueInstant'] = time
|
|
95
111
|
root.attributes['Version'] = "2.0"
|
|
96
|
-
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?
|
|
97
113
|
|
|
98
|
-
if settings.
|
|
114
|
+
if settings.sp_entity_id
|
|
99
115
|
issuer = root.add_element "saml:Issuer"
|
|
100
|
-
issuer.text = settings.
|
|
116
|
+
issuer.text = settings.sp_entity_id
|
|
101
117
|
end
|
|
102
118
|
|
|
103
119
|
nameid = root.add_element "saml:NameID"
|
|
@@ -117,14 +133,18 @@ module OneLogin
|
|
|
117
133
|
sessionindex.text = settings.sessionindex
|
|
118
134
|
end
|
|
119
135
|
|
|
136
|
+
request_doc
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def sign_document(document, settings)
|
|
120
140
|
# embed signature
|
|
121
141
|
if settings.security[:logout_requests_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
|
|
122
142
|
private_key = settings.get_sp_key
|
|
123
143
|
cert = settings.get_sp_cert
|
|
124
|
-
|
|
144
|
+
document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
|
|
125
145
|
end
|
|
126
146
|
|
|
127
|
-
|
|
147
|
+
document
|
|
128
148
|
end
|
|
129
149
|
end
|
|
130
150
|
end
|