ruby-saml 1.9.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +25 -0
- data/{changelog.md → CHANGELOG.md} +64 -1
- data/README.md +394 -211
- data/UPGRADING.md +149 -0
- data/lib/onelogin/ruby-saml/attributes.rb +24 -1
- data/lib/onelogin/ruby-saml/authrequest.rb +26 -10
- data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +285 -184
- data/lib/onelogin/ruby-saml/logging.rb +3 -3
- data/lib/onelogin/ruby-saml/logoutrequest.rb +26 -11
- data/lib/onelogin/ruby-saml/logoutresponse.rb +27 -11
- data/lib/onelogin/ruby-saml/metadata.rb +62 -17
- data/lib/onelogin/ruby-saml/response.rb +86 -37
- data/lib/onelogin/ruby-saml/saml_message.rb +14 -5
- data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
- data/lib/onelogin/ruby-saml/settings.rb +117 -41
- data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +33 -31
- data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +43 -20
- data/lib/onelogin/ruby-saml/utils.rb +101 -9
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +39 -13
- data/ruby-saml.gemspec +21 -8
- metadata +43 -284
- data/.travis.yml +0 -32
- data/test/certificates/certificate1 +0 -12
- data/test/certificates/certificate_without_head_foot +0 -1
- data/test/certificates/formatted_certificate +0 -14
- data/test/certificates/formatted_chained_certificate +0 -42
- data/test/certificates/formatted_private_key +0 -12
- data/test/certificates/formatted_rsa_private_key +0 -12
- data/test/certificates/invalid_certificate1 +0 -1
- data/test/certificates/invalid_certificate2 +0 -1
- data/test/certificates/invalid_certificate3 +0 -12
- data/test/certificates/invalid_chained_certificate1 +0 -1
- data/test/certificates/invalid_private_key1 +0 -1
- data/test/certificates/invalid_private_key2 +0 -1
- data/test/certificates/invalid_private_key3 +0 -10
- data/test/certificates/invalid_rsa_private_key1 +0 -1
- data/test/certificates/invalid_rsa_private_key2 +0 -1
- data/test/certificates/invalid_rsa_private_key3 +0 -10
- data/test/certificates/ruby-saml-2.crt +0 -15
- data/test/certificates/ruby-saml.crt +0 -14
- data/test/certificates/ruby-saml.key +0 -15
- data/test/idp_metadata_parser_test.rb +0 -579
- data/test/logging_test.rb +0 -62
- data/test/logout_requests/invalid_slo_request.xml +0 -6
- data/test/logout_requests/slo_request.xml +0 -4
- data/test/logout_requests/slo_request.xml.base64 +0 -1
- data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
- data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
- data/test/logout_requests/slo_request_with_session_index.xml +0 -5
- data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
- data/test/logoutrequest_test.rb +0 -226
- data/test/logoutresponse_test.rb +0 -402
- data/test/metadata/idp_descriptor.xml +0 -26
- data/test/metadata/idp_descriptor_2.xml +0 -56
- data/test/metadata/idp_descriptor_3.xml +0 -14
- data/test/metadata/idp_descriptor_4.xml +0 -72
- data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
- data/test/metadata/idp_metadata_multi_certs.xml +0 -75
- data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
- data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
- data/test/metadata/idp_multiple_descriptors.xml +0 -53
- data/test/metadata/no_idp_descriptor.xml +0 -21
- data/test/metadata_test.rb +0 -331
- data/test/request_test.rb +0 -323
- data/test/response_test.rb +0 -1619
- data/test/responses/adfs_response_sha1.xml +0 -46
- data/test/responses/adfs_response_sha256.xml +0 -46
- data/test/responses/adfs_response_sha384.xml +0 -46
- data/test/responses/adfs_response_sha512.xml +0 -46
- data/test/responses/adfs_response_xmlns.xml +0 -45
- data/test/responses/attackxee.xml +0 -13
- data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
- data/test/responses/invalids/empty_destination.xml.base64 +0 -1
- data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
- data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
- data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
- data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
- data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
- data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
- data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
- data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
- data/test/responses/invalids/no_conditions.xml.base64 +0 -1
- data/test/responses/invalids/no_id.xml.base64 +0 -1
- data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
- data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
- data/test/responses/invalids/no_nameid.xml.base64 +0 -1
- data/test/responses/invalids/no_saml2.xml.base64 +0 -1
- data/test/responses/invalids/no_signature.xml.base64 +0 -1
- data/test/responses/invalids/no_status.xml.base64 +0 -1
- data/test/responses/invalids/no_status_code.xml.base64 +0 -1
- data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
- data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
- data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
- data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
- data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
- data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
- data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
- data/test/responses/no_signature_ns.xml +0 -48
- data/test/responses/open_saml_response.xml +0 -56
- data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
- data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
- data/test/responses/response_double_status_code.xml.base64 +0 -1
- data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
- data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
- data/test/responses/response_eval.xml +0 -7
- data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
- data/test/responses/response_node_text_attack.xml.base64 +0 -1
- data/test/responses/response_node_text_attack2.xml.base64 +0 -1
- data/test/responses/response_node_text_attack3.xml.base64 +0 -1
- data/test/responses/response_unsigned_xml_base64 +0 -1
- data/test/responses/response_with_ampersands.xml +0 -139
- data/test/responses/response_with_ampersands.xml.base64 +0 -93
- data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
- data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
- data/test/responses/response_with_multiple_attribute_values.xml +0 -67
- data/test/responses/response_with_retrieval_method.xml +0 -26
- data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
- data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
- data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
- data/test/responses/response_with_signed_assertion_3.xml +0 -30
- data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
- data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
- data/test/responses/response_without_attributes.xml.base64 +0 -79
- data/test/responses/response_without_reference_uri.xml.base64 +0 -1
- data/test/responses/response_wrapped.xml.base64 +0 -150
- data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
- data/test/responses/signed_nameid_in_atts.xml +0 -47
- data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
- data/test/responses/simple_saml_php.xml +0 -71
- data/test/responses/starfield_response.xml.base64 +0 -1
- data/test/responses/test_sign.xml +0 -43
- data/test/responses/unsigned_encrypted_adfs.xml +0 -23
- data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
- data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
- data/test/responses/valid_response.xml.base64 +0 -1
- data/test/responses/valid_response_with_formatted_x509certificate.xml.base64 +0 -1
- data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
- data/test/saml_message_test.rb +0 -56
- data/test/settings_test.rb +0 -329
- data/test/slo_logoutrequest_test.rb +0 -448
- data/test/slo_logoutresponse_test.rb +0 -199
- data/test/test_helper.rb +0 -327
- data/test/utils_test.rb +0 -254
- data/test/xml_security_test.rb +0 -421
|
@@ -1,6 +1,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"
|
|
@@ -13,17 +11,43 @@ module OneLogin
|
|
|
13
11
|
|
|
14
12
|
# Auxiliary class to retrieve and parse the Identity Provider Metadata
|
|
15
13
|
#
|
|
14
|
+
# This class does not validate in any way the URL that is introduced,
|
|
15
|
+
# make sure to validate it properly before use it in a parse_remote method.
|
|
16
|
+
# Read the `Security warning` section of the README.md file to get more info
|
|
17
|
+
#
|
|
16
18
|
class IdpMetadataParser
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
module SamlMetadata
|
|
21
|
+
module Vocabulary
|
|
22
|
+
METADATA = "urn:oasis:names:tc:SAML:2.0:metadata".freeze
|
|
23
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze
|
|
24
|
+
NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*".freeze
|
|
25
|
+
SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion".freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
NAMESPACE = {
|
|
29
|
+
"md" => Vocabulary::METADATA,
|
|
30
|
+
"NameFormat" => Vocabulary::NAME_FORMAT,
|
|
31
|
+
"saml" => Vocabulary::SAML_ASSERTION,
|
|
32
|
+
"ds" => Vocabulary::DSIG
|
|
33
|
+
}.freeze
|
|
34
|
+
end
|
|
22
35
|
|
|
36
|
+
include SamlMetadata::Vocabulary
|
|
23
37
|
attr_reader :document
|
|
24
38
|
attr_reader :response
|
|
25
39
|
attr_reader :options
|
|
26
40
|
|
|
41
|
+
# fetch IdP descriptors from a metadata document
|
|
42
|
+
def self.get_idps(metadata_document, only_entity_id=nil)
|
|
43
|
+
path = "//md:EntityDescriptor#{only_entity_id && '[@entityID="' + only_entity_id + '"]'}/md:IDPSSODescriptor"
|
|
44
|
+
REXML::XPath.match(
|
|
45
|
+
metadata_document,
|
|
46
|
+
path,
|
|
47
|
+
SamlMetadata::NAMESPACE
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
27
51
|
# Parse the Identity Provider metadata and update the settings with the
|
|
28
52
|
# IdP values
|
|
29
53
|
#
|
|
@@ -32,9 +56,10 @@ module OneLogin
|
|
|
32
56
|
#
|
|
33
57
|
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
|
|
34
58
|
# @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides.
|
|
35
|
-
# @option options [
|
|
36
|
-
# @option options [Array<String>, nil] :
|
|
37
|
-
# @option options [String, nil] :
|
|
59
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
|
|
60
|
+
# @option options [String, 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.
|
|
61
|
+
# @option options [String, 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.
|
|
62
|
+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
|
|
38
63
|
#
|
|
39
64
|
# @return [OneLogin::RubySaml::Settings]
|
|
40
65
|
#
|
|
@@ -50,16 +75,35 @@ module OneLogin
|
|
|
50
75
|
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
|
|
51
76
|
#
|
|
52
77
|
# @param options [Hash] options used for parsing the metadata
|
|
53
|
-
# @option options [
|
|
54
|
-
# @option options [Array<String>, nil] :
|
|
55
|
-
# @option options [String, nil] :
|
|
78
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
|
|
79
|
+
# @option options [String, 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.
|
|
80
|
+
# @option options [String, 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.
|
|
81
|
+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
|
|
56
82
|
#
|
|
57
83
|
# @return [Hash]
|
|
58
84
|
#
|
|
59
85
|
# @raise [HttpError] Failure to fetch remote IdP metadata
|
|
60
86
|
def parse_remote_to_hash(url, validate_cert = true, options = {})
|
|
87
|
+
parse_remote_to_array(url, validate_cert, options)[0]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Parse all Identity Provider metadata and return the results as Array
|
|
91
|
+
#
|
|
92
|
+
# @param url [String] Url where the XML of the Identity Provider Metadata is published.
|
|
93
|
+
# @param validate_cert [Boolean] If true and the URL is HTTPs, the cert of the domain is checked.
|
|
94
|
+
#
|
|
95
|
+
# @param options [Hash] options used for parsing the metadata
|
|
96
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned.
|
|
97
|
+
# @option options [String, 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.
|
|
98
|
+
# @option options [String, 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.
|
|
99
|
+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Hash>]
|
|
102
|
+
#
|
|
103
|
+
# @raise [HttpError] Failure to fetch remote IdP metadata
|
|
104
|
+
def parse_remote_to_array(url, validate_cert = true, options = {})
|
|
61
105
|
idp_metadata = get_idp_metadata(url, validate_cert)
|
|
62
|
-
|
|
106
|
+
parse_to_array(idp_metadata, options)
|
|
63
107
|
end
|
|
64
108
|
|
|
65
109
|
# Parse the Identity Provider metadata and update the settings with the IdP values
|
|
@@ -68,14 +112,27 @@ module OneLogin
|
|
|
68
112
|
#
|
|
69
113
|
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object or an hash for Settings overrides
|
|
70
114
|
# @option options [OneLogin::RubySaml::Settings, Hash] :settings the OneLogin::RubySaml::Settings object which gets the parsed metadata merged into or an hash for Settings overrides.
|
|
71
|
-
# @option options [
|
|
72
|
-
# @option options [Array<String>, nil] :
|
|
73
|
-
# @option options [String, nil] :
|
|
115
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
|
|
116
|
+
# @option options [String, 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.
|
|
117
|
+
# @option options [String, 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.
|
|
118
|
+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
|
|
74
119
|
#
|
|
75
120
|
# @return [OneLogin::RubySaml::Settings]
|
|
76
121
|
def parse(idp_metadata, options = {})
|
|
77
122
|
parsed_metadata = parse_to_hash(idp_metadata, options)
|
|
78
123
|
|
|
124
|
+
unless parsed_metadata[:cache_duration].nil?
|
|
125
|
+
cache_valid_until_timestamp = OneLogin::RubySaml::Utils.parse_duration(parsed_metadata[:cache_duration])
|
|
126
|
+
unless cache_valid_until_timestamp.nil?
|
|
127
|
+
if parsed_metadata[:valid_until].nil? || cache_valid_until_timestamp < Time.parse(parsed_metadata[:valid_until], Time.now.utc).to_i
|
|
128
|
+
parsed_metadata[:valid_until] = Time.at(cache_valid_until_timestamp).utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
# Remove the cache_duration because on the settings
|
|
133
|
+
# we only gonna suppot valid_until
|
|
134
|
+
parsed_metadata.delete(:cache_duration)
|
|
135
|
+
|
|
79
136
|
settings = options[:settings]
|
|
80
137
|
|
|
81
138
|
if settings.nil?
|
|
@@ -92,34 +149,41 @@ module OneLogin
|
|
|
92
149
|
# @param idp_metadata [String]
|
|
93
150
|
#
|
|
94
151
|
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
|
|
95
|
-
# @option options [
|
|
96
|
-
# @option options [Array<String>, nil] :
|
|
97
|
-
# @option options [String, nil] :
|
|
152
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, the first entity descriptor is used.
|
|
153
|
+
# @option options [String, 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.
|
|
154
|
+
# @option options [String, 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.
|
|
155
|
+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
|
|
98
156
|
#
|
|
99
157
|
# @return [Hash]
|
|
100
158
|
def parse_to_hash(idp_metadata, options = {})
|
|
159
|
+
parse_to_array(idp_metadata, options)[0]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Parse all Identity Provider metadata and return the results as Array
|
|
163
|
+
#
|
|
164
|
+
# @param idp_metadata [String]
|
|
165
|
+
#
|
|
166
|
+
# @param options [Hash] options used for parsing the metadata and the returned Settings instance
|
|
167
|
+
# @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When omitted, all found IdPs are returned.
|
|
168
|
+
# @option options [String, 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.
|
|
169
|
+
# @option options [String, 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.
|
|
170
|
+
# @option options [String, Array<String>, nil] :name_id_format an ordered list of NameIDFormats to detect a desired value. The first NameIDFormat in the list that is included in the metadata will be used.
|
|
171
|
+
#
|
|
172
|
+
# @return [Array<Hash>]
|
|
173
|
+
def parse_to_array(idp_metadata, options = {})
|
|
174
|
+
parse_to_idp_metadata_array(idp_metadata, options).map { |idp_md| idp_md.to_hash(options) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_to_idp_metadata_array(idp_metadata, options = {})
|
|
101
178
|
@document = REXML::Document.new(idp_metadata)
|
|
102
179
|
@options = options
|
|
103
|
-
@entity_descriptor = nil
|
|
104
|
-
@certificates = nil
|
|
105
|
-
@fingerprint = nil
|
|
106
180
|
|
|
107
|
-
|
|
181
|
+
idpsso_descriptors = self.class.get_idps(@document, options[:entity_id])
|
|
182
|
+
if !idpsso_descriptors.any?
|
|
108
183
|
raise ArgumentError.new("idp_metadata must contain an IDPSSODescriptor element")
|
|
109
184
|
end
|
|
110
185
|
|
|
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
|
|
186
|
+
idpsso_descriptors.map {|id| IdpMetadata.new(id, id.parent.attributes["entityID"])}
|
|
123
187
|
end
|
|
124
188
|
|
|
125
189
|
private
|
|
@@ -147,7 +211,8 @@ module OneLogin
|
|
|
147
211
|
end
|
|
148
212
|
|
|
149
213
|
get = Net::HTTP::Get.new(uri.request_uri)
|
|
150
|
-
|
|
214
|
+
get.basic_auth uri.user, uri.password if uri.user
|
|
215
|
+
@response = http.request(get)
|
|
151
216
|
return response.body if response.is_a? Net::HTTPSuccess
|
|
152
217
|
|
|
153
218
|
raise OneLogin::RubySaml::HttpError.new(
|
|
@@ -155,130 +220,149 @@ module OneLogin
|
|
|
155
220
|
)
|
|
156
221
|
end
|
|
157
222
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
document,
|
|
161
|
-
entity_descriptor_path,
|
|
162
|
-
namespace
|
|
163
|
-
)
|
|
164
|
-
end
|
|
223
|
+
class IdpMetadata
|
|
224
|
+
attr_reader :idpsso_descriptor, :entity_id
|
|
165
225
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
path << "[@entityID=\"#{entity_id}\"]"
|
|
171
|
-
end
|
|
226
|
+
def initialize(idpsso_descriptor, entity_id)
|
|
227
|
+
@idpsso_descriptor = idpsso_descriptor
|
|
228
|
+
@entity_id = entity_id
|
|
229
|
+
end
|
|
172
230
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
231
|
+
def to_hash(options = {})
|
|
232
|
+
sso_binding = options[:sso_binding]
|
|
233
|
+
slo_binding = options[:slo_binding]
|
|
234
|
+
{
|
|
235
|
+
:idp_entity_id => @entity_id,
|
|
236
|
+
:name_identifier_format => idp_name_id_format(options[:name_id_format]),
|
|
237
|
+
:idp_sso_service_url => single_signon_service_url(sso_binding),
|
|
238
|
+
:idp_sso_service_binding => single_signon_service_binding(sso_binding),
|
|
239
|
+
:idp_slo_service_url => single_logout_service_url(slo_binding),
|
|
240
|
+
:idp_slo_service_binding => single_logout_service_binding(slo_binding),
|
|
241
|
+
:idp_slo_response_service_url => single_logout_response_service_url(slo_binding),
|
|
242
|
+
:idp_attribute_names => attribute_names,
|
|
243
|
+
:idp_cert => nil,
|
|
244
|
+
:idp_cert_fingerprint => nil,
|
|
245
|
+
:idp_cert_multi => nil,
|
|
246
|
+
:valid_until => valid_until,
|
|
247
|
+
:cache_duration => cache_duration,
|
|
248
|
+
}.tap do |response_hash|
|
|
249
|
+
merge_certificates_into(response_hash) unless certificates.nil?
|
|
250
|
+
end
|
|
180
251
|
end
|
|
181
|
-
end
|
|
182
252
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
253
|
+
# @return [String|nil] 'validUntil' attribute of metadata
|
|
254
|
+
#
|
|
255
|
+
def valid_until
|
|
256
|
+
root = @idpsso_descriptor.root
|
|
257
|
+
root.attributes['validUntil'] if root && root.attributes
|
|
258
|
+
end
|
|
188
259
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
namespace
|
|
196
|
-
)
|
|
197
|
-
Utils.element_text(node)
|
|
198
|
-
end
|
|
260
|
+
# @return [String|nil] 'cacheDuration' attribute of metadata
|
|
261
|
+
#
|
|
262
|
+
def cache_duration
|
|
263
|
+
root = @idpsso_descriptor.root
|
|
264
|
+
root.attributes['cacheDuration'] if root && root.attributes
|
|
265
|
+
end
|
|
199
266
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
values = nodes.map(&:value)
|
|
211
|
-
binding_priority.detect{ |binding| values.include? binding }
|
|
212
|
-
else
|
|
213
|
-
nodes.first.value if nodes.any?
|
|
267
|
+
# @param name_id_priority [String|Array<String>] The prioritized list of NameIDFormat values to select. Will select first value if nil.
|
|
268
|
+
# @return [String|nil] IdP NameIDFormat value if exists
|
|
269
|
+
#
|
|
270
|
+
def idp_name_id_format(name_id_priority = nil)
|
|
271
|
+
nodes = REXML::XPath.match(
|
|
272
|
+
@idpsso_descriptor,
|
|
273
|
+
"md:NameIDFormat",
|
|
274
|
+
SamlMetadata::NAMESPACE
|
|
275
|
+
)
|
|
276
|
+
first_ranked_text(nodes, name_id_priority)
|
|
214
277
|
end
|
|
215
|
-
end
|
|
216
278
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
|
|
226
|
-
namespace
|
|
279
|
+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
|
|
280
|
+
# @return [String|nil] SingleSignOnService binding if exists
|
|
281
|
+
#
|
|
282
|
+
def single_signon_service_binding(binding_priority = nil)
|
|
283
|
+
nodes = REXML::XPath.match(
|
|
284
|
+
@idpsso_descriptor,
|
|
285
|
+
"md:SingleSignOnService/@Binding",
|
|
286
|
+
SamlMetadata::NAMESPACE
|
|
227
287
|
)
|
|
228
|
-
|
|
288
|
+
first_ranked_value(nodes, binding_priority)
|
|
229
289
|
end
|
|
230
|
-
end
|
|
231
290
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
values = nodes.map(&:value)
|
|
243
|
-
binding_priority.detect{ |binding| values.include? binding }
|
|
244
|
-
else
|
|
245
|
-
nodes.first.value if nodes.any?
|
|
291
|
+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
|
|
292
|
+
# @return [String|nil] SingleLogoutService binding if exists
|
|
293
|
+
#
|
|
294
|
+
def single_logout_service_binding(binding_priority = nil)
|
|
295
|
+
nodes = REXML::XPath.match(
|
|
296
|
+
@idpsso_descriptor,
|
|
297
|
+
"md:SingleLogoutService/@Binding",
|
|
298
|
+
SamlMetadata::NAMESPACE
|
|
299
|
+
)
|
|
300
|
+
first_ranked_value(nodes, binding_priority)
|
|
246
301
|
end
|
|
247
|
-
end
|
|
248
302
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
303
|
+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
|
|
304
|
+
# @return [String|nil] SingleSignOnService endpoint if exists
|
|
305
|
+
#
|
|
306
|
+
def single_signon_service_url(binding_priority = nil)
|
|
307
|
+
binding = single_signon_service_binding(binding_priority)
|
|
308
|
+
return if binding.nil?
|
|
309
|
+
|
|
255
310
|
node = REXML::XPath.first(
|
|
256
|
-
|
|
257
|
-
"md:
|
|
258
|
-
|
|
311
|
+
@idpsso_descriptor,
|
|
312
|
+
"md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
|
|
313
|
+
SamlMetadata::NAMESPACE
|
|
259
314
|
)
|
|
260
|
-
|
|
315
|
+
node.value if node
|
|
261
316
|
end
|
|
262
|
-
end
|
|
263
317
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
318
|
+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
|
|
319
|
+
# @return [String|nil] SingleLogoutService endpoint if exists
|
|
320
|
+
#
|
|
321
|
+
def single_logout_service_url(binding_priority = nil)
|
|
322
|
+
binding = single_logout_service_binding(binding_priority)
|
|
323
|
+
return if binding.nil?
|
|
324
|
+
|
|
325
|
+
node = REXML::XPath.first(
|
|
326
|
+
@idpsso_descriptor,
|
|
327
|
+
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
|
|
328
|
+
SamlMetadata::NAMESPACE
|
|
272
329
|
)
|
|
330
|
+
node.value if node
|
|
331
|
+
end
|
|
273
332
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
333
|
+
# @param binding_priority [String|Array<String>] The prioritized list of Binding values to select. Will select first value if nil.
|
|
334
|
+
# @return [String|nil] SingleLogoutService response url if exists
|
|
335
|
+
#
|
|
336
|
+
def single_logout_response_service_url(binding_priority = nil)
|
|
337
|
+
binding = single_logout_service_binding(binding_priority)
|
|
338
|
+
return if binding.nil?
|
|
339
|
+
|
|
340
|
+
node = REXML::XPath.first(
|
|
341
|
+
@idpsso_descriptor,
|
|
342
|
+
"md:SingleLogoutService[@Binding=\"#{binding}\"]/@ResponseLocation",
|
|
343
|
+
SamlMetadata::NAMESPACE
|
|
278
344
|
)
|
|
345
|
+
node.value if node
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# @return [String|nil] Unformatted Certificate if exists
|
|
349
|
+
#
|
|
350
|
+
def certificates
|
|
351
|
+
@certificates ||= begin
|
|
352
|
+
signing_nodes = REXML::XPath.match(
|
|
353
|
+
@idpsso_descriptor,
|
|
354
|
+
"md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
355
|
+
SamlMetadata::NAMESPACE
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
encryption_nodes = REXML::XPath.match(
|
|
359
|
+
@idpsso_descriptor,
|
|
360
|
+
"md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
|
|
361
|
+
SamlMetadata::NAMESPACE
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
return nil if signing_nodes.empty? && encryption_nodes.empty?
|
|
279
365
|
|
|
280
|
-
certs = nil
|
|
281
|
-
unless signing_nodes.empty? && encryption_nodes.empty?
|
|
282
366
|
certs = {}
|
|
283
367
|
unless signing_nodes.empty?
|
|
284
368
|
certs['signing'] = []
|
|
@@ -293,71 +377,88 @@ module OneLogin
|
|
|
293
377
|
certs['encryption'] << Utils.element_text(cert_node)
|
|
294
378
|
end
|
|
295
379
|
end
|
|
380
|
+
certs
|
|
296
381
|
end
|
|
297
|
-
certs
|
|
298
382
|
end
|
|
299
|
-
end
|
|
300
383
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
384
|
+
# @return [String|nil] the fingerpint of the X509Certificate if it exists
|
|
385
|
+
#
|
|
386
|
+
def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1)
|
|
387
|
+
@fingerprint ||= begin
|
|
388
|
+
return unless certificate
|
|
389
|
+
|
|
306
390
|
cert = OpenSSL::X509::Certificate.new(Base64.decode64(certificate))
|
|
307
391
|
|
|
308
392
|
fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new
|
|
309
393
|
fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
|
|
310
394
|
end
|
|
311
395
|
end
|
|
312
|
-
end
|
|
313
396
|
|
|
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
|
|
397
|
+
# @return [Array] the names of all SAML attributes if any exist
|
|
398
|
+
#
|
|
399
|
+
def attribute_names
|
|
400
|
+
nodes = REXML::XPath.match(
|
|
401
|
+
@idpsso_descriptor ,
|
|
402
|
+
"saml:Attribute/@Name",
|
|
403
|
+
SamlMetadata::NAMESPACE
|
|
404
|
+
)
|
|
405
|
+
nodes.map(&:value)
|
|
406
|
+
end
|
|
333
407
|
|
|
334
|
-
|
|
335
|
-
|
|
408
|
+
def merge_certificates_into(parsed_metadata)
|
|
409
|
+
if (certificates.size == 1 &&
|
|
336
410
|
(certificates_has_one('signing') || certificates_has_one('encryption'))) ||
|
|
337
411
|
(certificates_has_one('signing') && certificates_has_one('encryption') &&
|
|
338
412
|
certificates["signing"][0] == certificates["encryption"][0])
|
|
339
413
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
414
|
+
if certificates.key?("signing")
|
|
415
|
+
parsed_metadata[:idp_cert] = certificates["signing"][0]
|
|
416
|
+
parsed_metadata[:idp_cert_fingerprint] = fingerprint(
|
|
417
|
+
parsed_metadata[:idp_cert],
|
|
418
|
+
parsed_metadata[:idp_cert_fingerprint_algorithm]
|
|
419
|
+
)
|
|
420
|
+
else
|
|
421
|
+
parsed_metadata[:idp_cert] = certificates["encryption"][0]
|
|
422
|
+
parsed_metadata[:idp_cert_fingerprint] = fingerprint(
|
|
423
|
+
parsed_metadata[:idp_cert],
|
|
424
|
+
parsed_metadata[:idp_cert_fingerprint_algorithm]
|
|
425
|
+
)
|
|
426
|
+
end
|
|
352
427
|
end
|
|
353
|
-
|
|
428
|
+
|
|
354
429
|
# symbolize keys of certificates and pass it on
|
|
355
430
|
parsed_metadata[:idp_cert_multi] = Hash[certificates.map { |k, v| [k.to_sym, v] }]
|
|
356
431
|
end
|
|
357
|
-
end
|
|
358
432
|
|
|
359
|
-
|
|
360
|
-
|
|
433
|
+
def certificates_has_one(key)
|
|
434
|
+
certificates.key?(key) && certificates[key].size == 1
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
private
|
|
438
|
+
|
|
439
|
+
def first_ranked_text(nodes, priority = nil)
|
|
440
|
+
return unless nodes.any?
|
|
441
|
+
|
|
442
|
+
priority = Array(priority)
|
|
443
|
+
if priority.any?
|
|
444
|
+
values = nodes.map(&:text)
|
|
445
|
+
priority.detect { |candidate| values.include?(candidate) }
|
|
446
|
+
else
|
|
447
|
+
nodes.first.text
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def first_ranked_value(nodes, priority = nil)
|
|
452
|
+
return unless nodes.any?
|
|
453
|
+
|
|
454
|
+
priority = Array(priority)
|
|
455
|
+
if priority.any?
|
|
456
|
+
values = nodes.map(&:value)
|
|
457
|
+
priority.detect { |candidate| values.include?(candidate) }
|
|
458
|
+
else
|
|
459
|
+
nodes.first.value
|
|
460
|
+
end
|
|
461
|
+
end
|
|
361
462
|
end
|
|
362
463
|
|
|
363
464
|
def merge_parsed_metadata_into(settings, parsed_metadata)
|
|
@@ -8,9 +8,9 @@ module OneLogin
|
|
|
8
8
|
|
|
9
9
|
def self.logger
|
|
10
10
|
@logger ||= begin
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
(defined?(::Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
|
12
|
+
DEFAULT_LOGGER
|
|
13
|
+
end
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def self.logger=(logger)
|