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.
Files changed (157) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +30 -14
  3. data/README.md +108 -22
  4. data/changelog.md +38 -0
  5. data/lib/onelogin/ruby-saml/attributes.rb +24 -1
  6. data/lib/onelogin/ruby-saml/authrequest.rb +23 -6
  7. data/lib/onelogin/ruby-saml/idp_metadata_parser.rb +239 -171
  8. data/lib/onelogin/ruby-saml/logging.rb +3 -3
  9. data/lib/onelogin/ruby-saml/logoutrequest.rb +20 -5
  10. data/lib/onelogin/ruby-saml/logoutresponse.rb +25 -9
  11. data/lib/onelogin/ruby-saml/metadata.rb +11 -3
  12. data/lib/onelogin/ruby-saml/response.rb +67 -21
  13. data/lib/onelogin/ruby-saml/saml_message.rb +12 -2
  14. data/lib/onelogin/ruby-saml/setting_error.rb +6 -0
  15. data/lib/onelogin/ruby-saml/settings.rb +73 -7
  16. data/lib/onelogin/ruby-saml/slo_logoutrequest.rb +20 -1
  17. data/lib/onelogin/ruby-saml/slo_logoutresponse.rb +38 -16
  18. data/lib/onelogin/ruby-saml/utils.rb +74 -1
  19. data/lib/onelogin/ruby-saml/version.rb +1 -1
  20. data/lib/xml_security.rb +34 -6
  21. data/ruby-saml.gemspec +15 -7
  22. metadata +36 -278
  23. data/test/certificates/certificate1 +0 -12
  24. data/test/certificates/certificate_without_head_foot +0 -1
  25. data/test/certificates/formatted_certificate +0 -14
  26. data/test/certificates/formatted_chained_certificate +0 -42
  27. data/test/certificates/formatted_private_key +0 -12
  28. data/test/certificates/formatted_rsa_private_key +0 -12
  29. data/test/certificates/invalid_certificate1 +0 -1
  30. data/test/certificates/invalid_certificate2 +0 -1
  31. data/test/certificates/invalid_certificate3 +0 -12
  32. data/test/certificates/invalid_chained_certificate1 +0 -1
  33. data/test/certificates/invalid_private_key1 +0 -1
  34. data/test/certificates/invalid_private_key2 +0 -1
  35. data/test/certificates/invalid_private_key3 +0 -10
  36. data/test/certificates/invalid_rsa_private_key1 +0 -1
  37. data/test/certificates/invalid_rsa_private_key2 +0 -1
  38. data/test/certificates/invalid_rsa_private_key3 +0 -10
  39. data/test/certificates/ruby-saml-2.crt +0 -15
  40. data/test/certificates/ruby-saml.crt +0 -14
  41. data/test/certificates/ruby-saml.key +0 -15
  42. data/test/idp_metadata_parser_test.rb +0 -579
  43. data/test/logging_test.rb +0 -62
  44. data/test/logout_requests/invalid_slo_request.xml +0 -6
  45. data/test/logout_requests/slo_request.xml +0 -4
  46. data/test/logout_requests/slo_request.xml.base64 +0 -1
  47. data/test/logout_requests/slo_request_deflated.xml.base64 +0 -1
  48. data/test/logout_requests/slo_request_with_name_id_format.xml +0 -4
  49. data/test/logout_requests/slo_request_with_session_index.xml +0 -5
  50. data/test/logout_responses/logoutresponse_fixtures.rb +0 -67
  51. data/test/logoutrequest_test.rb +0 -226
  52. data/test/logoutresponse_test.rb +0 -402
  53. data/test/metadata/idp_descriptor.xml +0 -26
  54. data/test/metadata/idp_descriptor_2.xml +0 -56
  55. data/test/metadata/idp_descriptor_3.xml +0 -14
  56. data/test/metadata/idp_descriptor_4.xml +0 -72
  57. data/test/metadata/idp_metadata_different_sign_and_encrypt_cert.xml +0 -72
  58. data/test/metadata/idp_metadata_multi_certs.xml +0 -75
  59. data/test/metadata/idp_metadata_multi_signing_certs.xml +0 -52
  60. data/test/metadata/idp_metadata_same_sign_and_encrypt_cert.xml +0 -71
  61. data/test/metadata/idp_multiple_descriptors.xml +0 -53
  62. data/test/metadata/no_idp_descriptor.xml +0 -21
  63. data/test/metadata_test.rb +0 -331
  64. data/test/request_test.rb +0 -323
  65. data/test/response_test.rb +0 -1619
  66. data/test/responses/adfs_response_sha1.xml +0 -46
  67. data/test/responses/adfs_response_sha256.xml +0 -46
  68. data/test/responses/adfs_response_sha384.xml +0 -46
  69. data/test/responses/adfs_response_sha512.xml +0 -46
  70. data/test/responses/adfs_response_xmlns.xml +0 -45
  71. data/test/responses/attackxee.xml +0 -13
  72. data/test/responses/invalids/duplicated_attributes.xml.base64 +0 -1
  73. data/test/responses/invalids/empty_destination.xml.base64 +0 -1
  74. data/test/responses/invalids/empty_nameid.xml.base64 +0 -1
  75. data/test/responses/invalids/encrypted_new_attack.xml.base64 +0 -1
  76. data/test/responses/invalids/invalid_audience.xml.base64 +0 -1
  77. data/test/responses/invalids/invalid_issuer_assertion.xml.base64 +0 -1
  78. data/test/responses/invalids/invalid_issuer_message.xml.base64 +0 -1
  79. data/test/responses/invalids/invalid_signature_position.xml.base64 +0 -1
  80. data/test/responses/invalids/invalid_subjectconfirmation_inresponse.xml.base64 +0 -1
  81. data/test/responses/invalids/invalid_subjectconfirmation_nb.xml.base64 +0 -1
  82. data/test/responses/invalids/invalid_subjectconfirmation_noa.xml.base64 +0 -1
  83. data/test/responses/invalids/invalid_subjectconfirmation_recipient.xml.base64 +0 -1
  84. data/test/responses/invalids/multiple_assertions.xml.base64 +0 -2
  85. data/test/responses/invalids/multiple_signed.xml.base64 +0 -1
  86. data/test/responses/invalids/no_authnstatement.xml.base64 +0 -1
  87. data/test/responses/invalids/no_conditions.xml.base64 +0 -1
  88. data/test/responses/invalids/no_id.xml.base64 +0 -1
  89. data/test/responses/invalids/no_issuer_assertion.xml.base64 +0 -1
  90. data/test/responses/invalids/no_issuer_response.xml.base64 +0 -1
  91. data/test/responses/invalids/no_nameid.xml.base64 +0 -1
  92. data/test/responses/invalids/no_saml2.xml.base64 +0 -1
  93. data/test/responses/invalids/no_signature.xml.base64 +0 -1
  94. data/test/responses/invalids/no_status.xml.base64 +0 -1
  95. data/test/responses/invalids/no_status_code.xml.base64 +0 -1
  96. data/test/responses/invalids/no_subjectconfirmation_data.xml.base64 +0 -1
  97. data/test/responses/invalids/no_subjectconfirmation_method.xml.base64 +0 -1
  98. data/test/responses/invalids/response_invalid_signed_element.xml.base64 +0 -1
  99. data/test/responses/invalids/response_with_concealed_signed_assertion.xml +0 -51
  100. data/test/responses/invalids/response_with_doubled_signed_assertion.xml +0 -49
  101. data/test/responses/invalids/signature_wrapping_attack.xml.base64 +0 -1
  102. data/test/responses/invalids/status_code_responder.xml.base64 +0 -1
  103. data/test/responses/invalids/status_code_responer_and_msg.xml.base64 +0 -1
  104. data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +0 -1
  105. data/test/responses/no_signature_ns.xml +0 -48
  106. data/test/responses/open_saml_response.xml +0 -56
  107. data/test/responses/response_assertion_wrapped.xml.base64 +0 -93
  108. data/test/responses/response_audience_self_closed_tag.xml.base64 +0 -1
  109. data/test/responses/response_double_status_code.xml.base64 +0 -1
  110. data/test/responses/response_encrypted_attrs.xml.base64 +0 -1
  111. data/test/responses/response_encrypted_nameid.xml.base64 +0 -1
  112. data/test/responses/response_eval.xml +0 -7
  113. data/test/responses/response_no_cert_and_encrypted_attrs.xml +0 -29
  114. data/test/responses/response_node_text_attack.xml.base64 +0 -1
  115. data/test/responses/response_node_text_attack2.xml.base64 +0 -1
  116. data/test/responses/response_node_text_attack3.xml.base64 +0 -1
  117. data/test/responses/response_unsigned_xml_base64 +0 -1
  118. data/test/responses/response_with_ampersands.xml +0 -139
  119. data/test/responses/response_with_ampersands.xml.base64 +0 -93
  120. data/test/responses/response_with_ds_namespace_at_the_root.xml.base64 +0 -1
  121. data/test/responses/response_with_multiple_attribute_statements.xml +0 -72
  122. data/test/responses/response_with_multiple_attribute_values.xml +0 -67
  123. data/test/responses/response_with_retrieval_method.xml +0 -26
  124. data/test/responses/response_with_saml2_namespace.xml.base64 +0 -102
  125. data/test/responses/response_with_signed_assertion.xml.base64 +0 -66
  126. data/test/responses/response_with_signed_assertion_2.xml.base64 +0 -1
  127. data/test/responses/response_with_signed_assertion_3.xml +0 -30
  128. data/test/responses/response_with_signed_message_and_assertion.xml +0 -34
  129. data/test/responses/response_with_undefined_recipient.xml.base64 +0 -1
  130. data/test/responses/response_without_attributes.xml.base64 +0 -79
  131. data/test/responses/response_without_reference_uri.xml.base64 +0 -1
  132. data/test/responses/response_wrapped.xml.base64 +0 -150
  133. data/test/responses/signed_message_encrypted_signed_assertion.xml.base64 +0 -1
  134. data/test/responses/signed_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  135. data/test/responses/signed_nameid_in_atts.xml +0 -47
  136. data/test/responses/signed_unqual_nameid_in_atts.xml +0 -47
  137. data/test/responses/simple_saml_php.xml +0 -71
  138. data/test/responses/starfield_response.xml.base64 +0 -1
  139. data/test/responses/test_sign.xml +0 -43
  140. data/test/responses/unsigned_encrypted_adfs.xml +0 -23
  141. data/test/responses/unsigned_message_aes128_encrypted_signed_assertion.xml.base64 +0 -1
  142. data/test/responses/unsigned_message_aes192_encrypted_signed_assertion.xml.base64 +0 -1
  143. data/test/responses/unsigned_message_aes256_encrypted_signed_assertion.xml.base64 +0 -1
  144. data/test/responses/unsigned_message_des192_encrypted_signed_assertion.xml.base64 +0 -1
  145. data/test/responses/unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64 +0 -1
  146. data/test/responses/unsigned_message_encrypted_signed_assertion.xml.base64 +0 -1
  147. data/test/responses/unsigned_message_encrypted_unsigned_assertion.xml.base64 +0 -1
  148. data/test/responses/valid_response.xml.base64 +0 -1
  149. data/test/responses/valid_response_with_formatted_x509certificate.xml.base64 +0 -1
  150. data/test/responses/valid_response_without_x509certificate.xml.base64 +0 -1
  151. data/test/saml_message_test.rb +0 -56
  152. data/test/settings_test.rb +0 -329
  153. data/test/slo_logoutrequest_test.rb +0 -448
  154. data/test/slo_logoutresponse_test.rb +0 -199
  155. data/test/test_helper.rb +0 -327
  156. data/test/utils_test.rb +0 -254
  157. 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.idp_sso_target_url =~ /\?/) ? '&' : '?'
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, idp_sso_target_url is not set!" if settings.idp_sso_target_url.nil?
40
- @login_url = settings.idp_sso_target_url + request_params
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.idp_sso_target_url unless settings.idp_sso_target_url.nil?
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.issuer != nil
125
+ if settings.sp_entity_id != nil
121
126
  issuer = root.add_element "saml:Issuer"
122
- issuer.text = settings.issuer
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
- 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"
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
- parse_to_hash(idp_metadata, options)
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
- if idpsso_descriptor.nil?
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
- response = http.request(get)
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
- def entity_descriptor
159
- @entity_descriptor ||= REXML::XPath.first(
160
- document,
161
- entity_descriptor_path,
162
- namespace
163
- )
164
- end
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
- 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
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
- def idpsso_descriptor
174
- unless entity_descriptor.nil?
175
- return REXML::XPath.first(
176
- entity_descriptor,
177
- "md:IDPSSODescriptor",
178
- namespace
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
- # @return [String|nil] IdP Entity ID value if exists
184
- #
185
- def idp_entity_id
186
- entity_descriptor.attributes["entityID"]
187
- end
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
- # @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
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
- # @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?
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
- # @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?
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
- entity_descriptor,
225
- "md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
226
- namespace
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
- # @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?
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
- # @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?
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
- entity_descriptor,
257
- "md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
258
- namespace
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
- # @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
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
- 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
+ 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
- # @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
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
- # @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
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
- def merge_certificates_into(parsed_metadata)
335
- if (certificates.size == 1 &&
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
- 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
- )
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
- 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
- )
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
- def certificates_has_one(key)
360
- certificates.key?(key) && certificates[key].size == 1
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)