ruby-saml 1.9.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +30 -14
- data/README.md +108 -22
- data/changelog.md +38 -0
- data/lib/onelogin/ruby-saml/attributes.rb +24 -1
- data/lib/onelogin/ruby-saml/authrequest.rb +23 -6
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +239 -171
- data/lib/onelogin/ruby-saml/logging.rb +3 -3
- data/lib/onelogin/ruby-saml/logoutrequest.rb +20 -5
- data/lib/onelogin/ruby-saml/logoutresponse.rb +25 -9
- data/lib/onelogin/ruby-saml/metadata.rb +11 -3
- data/lib/onelogin/ruby-saml/response.rb +67 -21
- data/lib/onelogin/ruby-saml/saml_message.rb +12 -2
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +73 -7
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +20 -1
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +38 -16
- data/lib/onelogin/ruby-saml/utils.rb +74 -1
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +34 -6
- data/ruby-saml.gemspec +15 -7
- metadata +36 -278
- data/test/certificates/certificate1 +0 -12
- data/test/certificates/certificate_without_head_foot +0 -1
- data/test/certificates/formatted_certificate +0 -14
- data/test/certificates/formatted_chained_certificate +0 -42
- data/test/certificates/formatted_private_key +0 -12
- data/test/certificates/formatted_rsa_private_key +0 -12
- data/test/certificates/invalid_certificate1 +0 -1
- data/test/certificates/invalid_certificate2 +0 -1
- data/test/certificates/invalid_certificate3 +0 -12
- data/test/certificates/invalid_chained_certificate1 +0 -1
- data/test/certificates/invalid_private_key1 +0 -1
- data/test/certificates/invalid_private_key2 +0 -1
- data/test/certificates/invalid_private_key3 +0 -10
- data/test/certificates/invalid_rsa_private_key1 +0 -1
- data/test/certificates/invalid_rsa_private_key2 +0 -1
- data/test/certificates/invalid_rsa_private_key3 +0 -10
- data/test/certificates/ruby-saml-2.crt +0 -15
- data/test/certificates/ruby-saml.crt +0 -14
- data/test/certificates/ruby-saml.key +0 -15
- data/test/idp_metadata_parser_test.rb +0 -579
- data/test/logging_test.rb +0 -62
- data/test/logout_requests/invalid_slo_request.xml +0 -6
- data/test/logout_requests/slo_request.xml +0 -4
- data/test/logout_requests/slo_request.xml.base64 +0 -1
- data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
- data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
- data/test/logout_requests/slo_request_with_session_index.xml +0 -5
- data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
- data/test/logoutrequest_test.rb +0 -226
- data/test/logoutresponse_test.rb +0 -402
- data/test/metadata/idp_descriptor.xml +0 -26
- data/test/metadata/idp_descriptor_2.xml +0 -56
- data/test/metadata/idp_descriptor_3.xml +0 -14
- data/test/metadata/idp_descriptor_4.xml +0 -72
- data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
- data/test/metadata/idp_metadata_multi_certs.xml +0 -75
- data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
- data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
- data/test/metadata/idp_multiple_descriptors.xml +0 -53
- data/test/metadata/no_idp_descriptor.xml +0 -21
- data/test/metadata_test.rb +0 -331
- data/test/request_test.rb +0 -323
- data/test/response_test.rb +0 -1619
- data/test/responses/adfs_response_sha1.xml +0 -46
- data/test/responses/adfs_response_sha256.xml +0 -46
- data/test/responses/adfs_response_sha384.xml +0 -46
- data/test/responses/adfs_response_sha512.xml +0 -46
- data/test/responses/adfs_response_xmlns.xml +0 -45
- data/test/responses/attackxee.xml +0 -13
- data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
- data/test/responses/invalids/empty_destination.xml.base64 +0 -1
- data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
- data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
- data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
- data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
- data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
- data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
- data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
- data/test/responses/invalids/no_conditions.xml.base64 +0 -1
- data/test/responses/invalids/no_id.xml.base64 +0 -1
- data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
- data/test/responses/invalids/no_nameid.xml.base64 +0 -1
- data/test/responses/invalids/no_saml2.xml.base64 +0 -1
- data/test/responses/invalids/no_signature.xml.base64 +0 -1
- data/test/responses/invalids/no_status.xml.base64 +0 -1
- data/test/responses/invalids/no_status_code.xml.base64 +0 -1
- data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
- data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
- data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
- data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
- data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
- data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
- data/test/responses/no_signature_ns.xml +0 -48
- data/test/responses/open_saml_response.xml +0 -56
- data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
- data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
- data/test/responses/response_double_status_code.xml.base64 +0 -1
- data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
- data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
- data/test/responses/response_eval.xml +0 -7
- data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
- data/test/responses/response_node_text_attack2.xml.base64 +0 -1
- data/test/responses/response_node_text_attack3.xml.base64 +0 -1
- data/test/responses/response_unsigned_xml_base64 +0 -1
- data/test/responses/response_with_ampersands.xml +0 -139
- data/test/responses/response_with_ampersands.xml.base64 +0 -93
- data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
- data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
- data/test/responses/response_with_multiple_attribute_values.xml +0 -67
- data/test/responses/response_with_retrieval_method.xml +0 -26
- data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
- data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
- data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
- data/test/responses/response_with_signed_assertion_3.xml +0 -30
- data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
- data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
- data/test/responses/response_without_attributes.xml.base64 +0 -79
- data/test/responses/response_without_reference_uri.xml.base64 +0 -1
- data/test/responses/response_wrapped.xml.base64 +0 -150
- data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
- data/test/responses/signed_nameid_in_atts.xml +0 -47
- data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
- data/test/responses/simple_saml_php.xml +0 -71
- data/test/responses/starfield_response.xml.base64 +0 -1
- data/test/responses/test_sign.xml +0 -43
- data/test/responses/unsigned_encrypted_adfs.xml +0 -23
- data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
- data/test/responses/valid_response.xml.base64 +0 -1
- data/test/responses/valid_response_with_formatted_x509certificate.xml.base64 +0 -1
- data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
- data/test/saml_message_test.rb +0 -56
- data/test/settings_test.rb +0 -329
- data/test/slo_logoutrequest_test.rb +0 -448
- data/test/slo_logoutresponse_test.rb +0 -199
- data/test/test_helper.rb +0 -327
- data/test/utils_test.rb +0 -254
- data/test/xml_security_test.rb +0 -421
|
@@ -3,6 +3,7 @@ require "rexml/document"
|
|
|
3
3
|
require "onelogin/ruby-saml/logging"
|
|
4
4
|
require "onelogin/ruby-saml/saml_message"
|
|
5
5
|
require "onelogin/ruby-saml/utils"
|
|
6
|
+
require "onelogin/ruby-saml/setting_error"
|
|
6
7
|
|
|
7
8
|
# Only supports SAML 2.0
|
|
8
9
|
module OneLogin
|
|
@@ -23,6 +24,10 @@ module OneLogin
|
|
|
23
24
|
@uuid = OneLogin::RubySaml::Utils.uuid
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def request_id
|
|
28
|
+
@uuid
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
# Creates the AuthNRequest string.
|
|
27
32
|
# @param settings [OneLogin::RubySaml::Settings|nil] Toolkit settings
|
|
28
33
|
# @param params [Hash] Some extra parameters to be added in the GET for example the RelayState
|
|
@@ -30,14 +35,14 @@ module OneLogin
|
|
|
30
35
|
#
|
|
31
36
|
def create(settings, params = {})
|
|
32
37
|
params = create_params(settings, params)
|
|
33
|
-
params_prefix = (settings.
|
|
38
|
+
params_prefix = (settings.idp_sso_service_url =~ /\?/) ? '&' : '?'
|
|
34
39
|
saml_request = CGI.escape(params.delete("SAMLRequest"))
|
|
35
40
|
request_params = "#{params_prefix}SAMLRequest=#{saml_request}"
|
|
36
41
|
params.each_pair do |key, value|
|
|
37
42
|
request_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
|
|
38
43
|
end
|
|
39
|
-
raise "Invalid settings,
|
|
40
|
-
@login_url = settings.
|
|
44
|
+
raise SettingError.new "Invalid settings, idp_sso_service_url is not set!" if settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty?
|
|
45
|
+
@login_url = settings.idp_sso_service_url + request_params
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
# Creates the Get parameters for the request.
|
|
@@ -107,7 +112,7 @@ module OneLogin
|
|
|
107
112
|
root.attributes['ID'] = uuid
|
|
108
113
|
root.attributes['IssueInstant'] = time
|
|
109
114
|
root.attributes['Version'] = "2.0"
|
|
110
|
-
root.attributes['Destination'] = settings.
|
|
115
|
+
root.attributes['Destination'] = settings.idp_sso_service_url unless settings.idp_sso_service_url.nil? or settings.idp_sso_service_url.empty?
|
|
111
116
|
root.attributes['IsPassive'] = settings.passive unless settings.passive.nil?
|
|
112
117
|
root.attributes['ProtocolBinding'] = settings.protocol_binding unless settings.protocol_binding.nil?
|
|
113
118
|
root.attributes["AttributeConsumingServiceIndex"] = settings.attributes_index unless settings.attributes_index.nil?
|
|
@@ -117,10 +122,22 @@ module OneLogin
|
|
|
117
122
|
if settings.assertion_consumer_service_url != nil
|
|
118
123
|
root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url
|
|
119
124
|
end
|
|
120
|
-
if settings.
|
|
125
|
+
if settings.sp_entity_id != nil
|
|
121
126
|
issuer = root.add_element "saml:Issuer"
|
|
122
|
-
issuer.text = settings.
|
|
127
|
+
issuer.text = settings.sp_entity_id
|
|
123
128
|
end
|
|
129
|
+
|
|
130
|
+
if settings.name_identifier_value_requested != nil
|
|
131
|
+
subject = root.add_element "saml:Subject"
|
|
132
|
+
|
|
133
|
+
nameid = subject.add_element "saml:NameID"
|
|
134
|
+
nameid.attributes['Format'] = settings.name_identifier_format if settings.name_identifier_format
|
|
135
|
+
nameid.text = settings.name_identifier_value_requested
|
|
136
|
+
|
|
137
|
+
subject_confirmation = subject.add_element "saml:SubjectConfirmation"
|
|
138
|
+
subject_confirmation.attributes['Method'] = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
|
139
|
+
end
|
|
140
|
+
|
|
124
141
|
if settings.name_identifier_format != nil
|
|
125
142
|
root.add_element "samlp:NameIDPolicy", {
|
|
126
143
|
# Might want to make AllowCreate a setting?
|
|
@@ -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,30 +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
|
-
@certificates = nil
|
|
105
|
-
@fingerprint = nil
|
|
106
168
|
|
|
107
|
-
|
|
169
|
+
idpsso_descriptors = self.class.get_idps(@document, options[:entity_id])
|
|
170
|
+
if !idpsso_descriptors.any?
|
|
108
171
|
raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element")
|
|
109
172
|
end
|
|
110
173
|
|
|
111
|
-
{
|
|
112
|
-
:idp_entity_id => idp_entity_id,
|
|
113
|
-
:name_identifier_format => idp_name_id_format,
|
|
114
|
-
:idp_sso_target_url => single_signon_service_url(options),
|
|
115
|
-
:idp_slo_target_url => single_logout_service_url(options),
|
|
116
|
-
:idp_attribute_names => attribute_names,
|
|
117
|
-
:idp_cert => nil,
|
|
118
|
-
:idp_cert_fingerprint => nil,
|
|
119
|
-
:idp_cert_multi => nil
|
|
120
|
-
}.tap do |response_hash|
|
|
121
|
-
merge_certificates_into(response_hash) unless certificates.nil?
|
|
122
|
-
end
|
|
174
|
+
return idpsso_descriptors.map{|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
|
|
123
175
|
end
|
|
124
|
-
|
|
176
|
+
|
|
125
177
|
private
|
|
126
178
|
|
|
127
179
|
# Retrieve the remote IdP metadata from the URL or a cached copy.
|
|
@@ -147,7 +199,8 @@ module OneLogin
|
|
|
147
199
|
end
|
|
148
200
|
|
|
149
201
|
get = Net::HTTP::Get.new(uri.request_uri)
|
|
150
|
-
|
|
202
|
+
get.basic_auth uri.user, uri.password if uri.user
|
|
203
|
+
@response = http.request(get)
|
|
151
204
|
return response.body if response.is_a? Net::HTTPSuccess
|
|
152
205
|
|
|
153
206
|
raise OneLogin::RubySaml::HttpError.new(
|
|
@@ -155,130 +208,154 @@ module OneLogin
|
|
|
155
208
|
)
|
|
156
209
|
end
|
|
157
210
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
165
218
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
236
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
179
244
|
)
|
|
245
|
+
Utils.element_text(node)
|
|
180
246
|
end
|
|
181
|
-
end
|
|
182
247
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
188
254
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
namespace
|
|
196
|
-
)
|
|
197
|
-
Utils.element_text(node)
|
|
198
|
-
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
|
|
199
261
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
214
277
|
end
|
|
215
|
-
end
|
|
216
278
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
|
|
223
286
|
node = REXML::XPath.first(
|
|
224
|
-
|
|
225
|
-
"md:
|
|
226
|
-
|
|
287
|
+
@idpsso_descriptor,
|
|
288
|
+
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
|
|
289
|
+
SamlMetadata::NAMESPACE
|
|
227
290
|
)
|
|
228
291
|
return node.value if node
|
|
229
292
|
end
|
|
230
|
-
end
|
|
231
293
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
246
309
|
end
|
|
247
|
-
end
|
|
248
310
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
|
|
255
318
|
node = REXML::XPath.first(
|
|
256
|
-
|
|
257
|
-
"md:
|
|
258
|
-
|
|
319
|
+
@idpsso_descriptor,
|
|
320
|
+
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
|
|
321
|
+
SamlMetadata::NAMESPACE
|
|
259
322
|
)
|
|
260
323
|
return node.value if node
|
|
261
324
|
end
|
|
262
|
-
end
|
|
263
325
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"md:IDPSSODescriptor/md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
271
|
-
namespace
|
|
272
|
-
)
|
|
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?
|
|
273
332
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"md:
|
|
277
|
-
|
|
333
|
+
node = REXML::XPath.first(
|
|
334
|
+
@idpsso_descriptor,
|
|
335
|
+
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation",
|
|
336
|
+
SamlMetadata::NAMESPACE
|
|
278
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?
|
|
279
358
|
|
|
280
|
-
certs = nil
|
|
281
|
-
unless signing_nodes.empty? && encryption_nodes.empty?
|
|
282
359
|
certs = {}
|
|
283
360
|
unless signing_nodes.empty?
|
|
284
361
|
certs['signing'] = []
|
|
@@ -293,71 +370,62 @@ module OneLogin
|
|
|
293
370
|
certs['encryption'] << Utils.element_text(cert_node)
|
|
294
371
|
end
|
|
295
372
|
end
|
|
373
|
+
certs
|
|
296
374
|
end
|
|
297
|
-
certs
|
|
298
375
|
end
|
|
299
|
-
end
|
|
300
376
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
+
|
|
306
383
|
cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
|
|
307
384
|
|
|
308
385
|
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new
|
|
309
386
|
fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
|
310
387
|
end
|
|
311
388
|
end
|
|
312
|
-
end
|
|
313
389
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def namespace
|
|
326
|
-
{
|
|
327
|
-
"md" => METADATA,
|
|
328
|
-
"NameFormat" => NAME_FORMAT,
|
|
329
|
-
"saml" => SAML_ASSERTION,
|
|
330
|
-
"ds" => DSIG
|
|
331
|
-
}
|
|
332
|
-
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
|
|
333
400
|
|
|
334
|
-
|
|
335
|
-
|
|
401
|
+
def merge_certificates_into(parsed_metadata)
|
|
402
|
+
if (certificates.size == 1 &&
|
|
336
403
|
(certificates_has_one('signing') || certificates_has_one('encryption'))) ||
|
|
337
404
|
(certificates_has_one('signing') && certificates_has_one('encryption') &&
|
|
338
405
|
certificates["signing"][0] == certificates["encryption"][0])
|
|
339
406
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
346
420
|
else
|
|
347
|
-
|
|
348
|
-
parsed_metadata[:
|
|
349
|
-
parsed_metadata[:idp_cert],
|
|
350
|
-
parsed_metadata[:idp_cert_fingerprint_algorithm]
|
|
351
|
-
)
|
|
421
|
+
# symbolize keys of certificates and pass it on
|
|
422
|
+
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
|
|
352
423
|
end
|
|
353
|
-
else
|
|
354
|
-
# symbolize keys of certificates and pass it on
|
|
355
|
-
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
|
|
356
424
|
end
|
|
357
|
-
end
|
|
358
425
|
|
|
359
|
-
|
|
360
|
-
|
|
426
|
+
def certificates_has_one(key)
|
|
427
|
+
certificates.key?(key) && certificates[key].size == 1
|
|
428
|
+
end
|
|
361
429
|
end
|
|
362
430
|
|
|
363
431
|
def merge_parsed_metadata_into(settings, parsed_metadata)
|