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.
Files changed (159) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +25 -0
  3. data/{changelog.md → CHANGELOG.md} +64 -1
  4. data/README.md +394 -211
  5. data/UPGRADING.md +149 -0
  6. data/lib/onelogin/ruby-saml/attributes.rb +24 -1
  7. data/lib/onelogin/ruby-saml/authrequest.rb +26 -10
  8. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +285 -184
  9. data/lib/onelogin/ruby-saml/logging.rb +3 -3
  10. data/lib/onelogin/ruby-saml/logoutrequest.rb +26 -11
  11. data/lib/onelogin/ruby-saml/logoutresponse.rb +27 -11
  12. data/lib/onelogin/ruby-saml/metadata.rb +62 -17
  13. data/lib/onelogin/ruby-saml/response.rb +86 -37
  14. data/lib/onelogin/ruby-saml/saml_message.rb +14 -5
  15. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  16. data/lib/onelogin/ruby-saml/settings.rb +117 -41
  17. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +33 -31
  18. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +43 -20
  19. data/lib/onelogin/ruby-saml/utils.rb +101 -9
  20. data/lib/onelogin/ruby-saml/version.rb +1 -1
  21. data/lib/xml_security.rb +39 -13
  22. data/ruby-saml.gemspec +21 -8
  23. metadata +43 -284
  24. data/.travis.yml +0 -32
  25. data/test/certificates/certificate1 +0 -12
  26. data/test/certificates/certificate_without_head_foot +0 -1
  27. data/test/certificates/formatted_certificate +0 -14
  28. data/test/certificates/formatted_chained_certificate +0 -42
  29. data/test/certificates/formatted_private_key +0 -12
  30. data/test/certificates/formatted_rsa_private_key +0 -12
  31. data/test/certificates/invalid_certificate1 +0 -1
  32. data/test/certificates/invalid_certificate2 +0 -1
  33. data/test/certificates/invalid_certificate3 +0 -12
  34. data/test/certificates/invalid_chained_certificate1 +0 -1
  35. data/test/certificates/invalid_private_key1 +0 -1
  36. data/test/certificates/invalid_private_key2 +0 -1
  37. data/test/certificates/invalid_private_key3 +0 -10
  38. data/test/certificates/invalid_rsa_private_key1 +0 -1
  39. data/test/certificates/invalid_rsa_private_key2 +0 -1
  40. data/test/certificates/invalid_rsa_private_key3 +0 -10
  41. data/test/certificates/ruby-saml-2.crt +0 -15
  42. data/test/certificates/ruby-saml.crt +0 -14
  43. data/test/certificates/ruby-saml.key +0 -15
  44. data/test/idp_metadata_parser_test.rb +0 -579
  45. data/test/logging_test.rb +0 -62
  46. data/test/logout_requests/invalid_slo_request.xml +0 -6
  47. data/test/logout_requests/slo_request.xml +0 -4
  48. data/test/logout_requests/slo_request.xml.base64 +0 -1
  49. data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
  50. data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
  51. data/test/logout_requests/slo_request_with_session_index.xml +0 -5
  52. data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
  53. data/test/logoutrequest_test.rb +0 -226
  54. data/test/logoutresponse_test.rb +0 -402
  55. data/test/metadata/idp_descriptor.xml +0 -26
  56. data/test/metadata/idp_descriptor_2.xml +0 -56
  57. data/test/metadata/idp_descriptor_3.xml +0 -14
  58. data/test/metadata/idp_descriptor_4.xml +0 -72
  59. data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
  60. data/test/metadata/idp_metadata_multi_certs.xml +0 -75
  61. data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
  62. data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
  63. data/test/metadata/idp_multiple_descriptors.xml +0 -53
  64. data/test/metadata/no_idp_descriptor.xml +0 -21
  65. data/test/metadata_test.rb +0 -331
  66. data/test/request_test.rb +0 -323
  67. data/test/response_test.rb +0 -1619
  68. data/test/responses/adfs_response_sha1.xml +0 -46
  69. data/test/responses/adfs_response_sha256.xml +0 -46
  70. data/test/responses/adfs_response_sha384.xml +0 -46
  71. data/test/responses/adfs_response_sha512.xml +0 -46
  72. data/test/responses/adfs_response_xmlns.xml +0 -45
  73. data/test/responses/attackxee.xml +0 -13
  74. data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
  75. data/test/responses/invalids/empty_destination.xml.base64 +0 -1
  76. data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
  77. data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
  78. data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
  79. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  80. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  81. data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
  82. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
  83. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
  84. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
  85. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
  86. data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
  87. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  88. data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
  89. data/test/responses/invalids/no_conditions.xml.base64 +0 -1
  90. data/test/responses/invalids/no_id.xml.base64 +0 -1
  91. data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
  92. data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
  93. data/test/responses/invalids/no_nameid.xml.base64 +0 -1
  94. data/test/responses/invalids/no_saml2.xml.base64 +0 -1
  95. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  96. data/test/responses/invalids/no_status.xml.base64 +0 -1
  97. data/test/responses/invalids/no_status_code.xml.base64 +0 -1
  98. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
  99. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
  100. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
  101. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  102. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  103. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  104. data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
  105. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
  106. data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
  107. data/test/responses/no_signature_ns.xml +0 -48
  108. data/test/responses/open_saml_response.xml +0 -56
  109. data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
  110. data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
  111. data/test/responses/response_double_status_code.xml.base64 +0 -1
  112. data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
  113. data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
  114. data/test/responses/response_eval.xml +0 -7
  115. data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
  116. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  117. data/test/responses/response_node_text_attack2.xml.base64 +0 -1
  118. data/test/responses/response_node_text_attack3.xml.base64 +0 -1
  119. data/test/responses/response_unsigned_xml_base64 +0 -1
  120. data/test/responses/response_with_ampersands.xml +0 -139
  121. data/test/responses/response_with_ampersands.xml.base64 +0 -93
  122. data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
  123. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  124. data/test/responses/response_with_multiple_attribute_values.xml +0 -67
  125. data/test/responses/response_with_retrieval_method.xml +0 -26
  126. data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
  127. data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
  128. data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
  129. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  130. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  131. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  132. data/test/responses/response_without_attributes.xml.base64 +0 -79
  133. data/test/responses/response_without_reference_uri.xml.base64 +0 -1
  134. data/test/responses/response_wrapped.xml.base64 +0 -150
  135. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
  136. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  137. data/test/responses/signed_nameid_in_atts.xml +0 -47
  138. data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
  139. data/test/responses/simple_saml_php.xml +0 -71
  140. data/test/responses/starfield_response.xml.base64 +0 -1
  141. data/test/responses/test_sign.xml +0 -43
  142. data/test/responses/unsigned_encrypted_adfs.xml +0 -23
  143. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
  144. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
  145. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
  146. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
  147. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
  148. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
  149. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  150. data/test/responses/valid_response.xml.base64 +0 -1
  151. data/test/responses/valid_response_with_formatted_x509certificate.xml.base64 +0 -1
  152. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  153. data/test/saml_message_test.rb +0 -56
  154. data/test/settings_test.rb +0 -329
  155. data/test/slo_logoutrequest_test.rb +0 -448
  156. data/test/slo_logoutresponse_test.rb +0 -199
  157. data/test/test_helper.rb +0 -327
  158. data/test/utils_test.rb +0 -254
  159. 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
- METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
19
- DSIG = "http://www.w3.org/2000/09/xmldsig#"
20
- NAME_FORMAT = "urn:oasis:names:tc:SAML:2.0:attrname-format:*"
21
- SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
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 [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.
36
- # @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.
37
- # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
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 [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.
54
- # @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.
55
- # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
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
- parse_to_hash(idp_metadata, options)
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 [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.
72
- # @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.
73
- # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
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 [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.
96
- # @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.
97
- # @option options [String, nil] :entity_id when this is given, the entity descriptor for this ID is used. When ommitted, the first entity descriptor is used.
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
- if idpsso_descriptor.nil?
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
- response = http.request(get)
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
- def entity_descriptor
159
- @entity_descriptor ||= REXML::XPath.first(
160
- document,
161
- entity_descriptor_path,
162
- namespace
163
- )
164
- end
223
+ class IdpMetadata
224
+ attr_reader :idpsso_descriptor, :entity_id
165
225
 
166
- def entity_descriptor_path
167
- path = "//md:EntityDescriptor"
168
- entity_id = options[:entity_id]
169
- return path unless entity_id
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
- def idpsso_descriptor
174
- unless entity_descriptor.nil?
175
- return REXML::XPath.first(
176
- entity_descriptor,
177
- "md:IDPSSODescriptor",
178
- namespace
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
- # @return [String|nil] IdP Entity ID value if exists
184
- #
185
- def idp_entity_id
186
- entity_descriptor.attributes["entityID"]
187
- end
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
- # @return [String|nil] IdP Name ID Format value if exists
190
- #
191
- def idp_name_id_format
192
- node = REXML::XPath.first(
193
- entity_descriptor,
194
- "md:IDPSSODescriptor/md:NameIDFormat",
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
- # @param binding_priority [Array]
201
- # @return [String|nil] SingleSignOnService binding if exists
202
- #
203
- def single_signon_service_binding(binding_priority = nil)
204
- nodes = REXML::XPath.match(
205
- entity_descriptor,
206
- "md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
207
- namespace
208
- )
209
- if binding_priority
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
- # @param options [Hash]
218
- # @return [String|nil] SingleSignOnService endpoint if exists
219
- #
220
- def single_signon_service_url(options = {})
221
- binding = single_signon_service_binding(options[:sso_binding])
222
- unless binding.nil?
223
- node = REXML::XPath.first(
224
- entity_descriptor,
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
- return node.value if node
288
+ first_ranked_value(nodes, binding_priority)
229
289
  end
230
- end
231
290
 
232
- # @param binding_priority [Array]
233
- # @return [String|nil] SingleLogoutService binding if exists
234
- #
235
- def single_logout_service_binding(binding_priority = nil)
236
- nodes = REXML::XPath.match(
237
- entity_descriptor,
238
- "md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
239
- namespace
240
- )
241
- if binding_priority
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
- # @param options [Hash]
250
- # @return [String|nil] SingleLogoutService endpoint if exists
251
- #
252
- def single_logout_service_url(options = {})
253
- binding = single_logout_service_binding(options[:slo_binding])
254
- unless binding.nil?
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
- entity_descriptor,
257
- "md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
258
- namespace
311
+ @idpsso_descriptor,
312
+ "md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
313
+ SamlMetadata::NAMESPACE
259
314
  )
260
- return node.value if node
315
+ node.value if node
261
316
  end
262
- end
263
317
 
264
- # @return [String|nil] Unformatted Certificate if exists
265
- #
266
- def certificates
267
- @certificates ||= begin
268
- signing_nodes = REXML::XPath.match(
269
- entity_descriptor,
270
- "md:IDPSSODescriptor/md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
271
- namespace
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
- encryption_nodes = REXML::XPath.match(
275
- entity_descriptor,
276
- "md:IDPSSODescriptor/md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
277
- namespace
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
- # @return [String|nil] the fingerpint of the X509Certificate if it exists
302
- #
303
- def fingerprint(certificate, fingerprint_algorithm = XMLSecurity::Document::SHA1)
304
- @fingerprint ||= begin
305
- if certificate
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
- # @return [Array] the names of all SAML attributes if any exist
315
- #
316
- def attribute_names
317
- nodes = REXML::XPath.match(
318
- entity_descriptor,
319
- "md:IDPSSODescriptor/saml:Attribute/@Name",
320
- namespace
321
- )
322
- nodes.map(&:value)
323
- end
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
- def merge_certificates_into(parsed_metadata)
335
- if (certificates.size == 1 &&
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
- if certificates.key?("signing")
341
- parsed_metadata[:idp_cert] = certificates["signing"][0]
342
- parsed_metadata[:idp_cert_fingerprint] = fingerprint(
343
- parsed_metadata[:idp_cert],
344
- parsed_metadata[:idp_cert_fingerprint_algorithm]
345
- )
346
- else
347
- parsed_metadata[:idp_cert] = certificates["encryption"][0]
348
- parsed_metadata[:idp_cert_fingerprint] = fingerprint(
349
- parsed_metadata[:idp_cert],
350
- parsed_metadata[:idp_cert_fingerprint_algorithm]
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
- else
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
- def certificates_has_one(key)
360
- certificates.key?(key) && certificates[key].size == 1
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
- (defined?(::Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
12
- DEFAULT_LOGGER
13
- end
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)