ruby-saml 1.12.4 → 1.18.1

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.
@@ -43,7 +43,7 @@ module OneLogin
43
43
  end
44
44
  end
45
45
 
46
- @request = decode_raw_saml(request)
46
+ @request = decode_raw_saml(request, settings)
47
47
  @document = REXML::Document.new(@request)
48
48
  end
49
49
 
@@ -62,10 +62,7 @@ module OneLogin
62
62
  # @return [String] Gets the NameID of the Logout Request.
63
63
  #
64
64
  def name_id
65
- @name_id ||= begin
66
- node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
67
- Utils.element_text(node)
68
- end
65
+ @name_id ||= Utils.element_text(name_id_node)
69
66
  end
70
67
 
71
68
  alias_method :nameid, :name_id
@@ -73,15 +70,49 @@ module OneLogin
73
70
  # @return [String] Gets the NameID Format of the Logout Request.
74
71
  #
75
72
  def name_id_format
76
- @name_id_node ||= REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
77
73
  @name_id_format ||=
78
- if @name_id_node && @name_id_node.attribute("Format")
79
- @name_id_node.attribute("Format").value
74
+ if name_id_node && name_id_node.attribute("Format")
75
+ name_id_node.attribute("Format").value
80
76
  end
81
77
  end
82
78
 
83
79
  alias_method :nameid_format, :name_id_format
84
80
 
81
+ def name_id_node
82
+ @name_id_node ||=
83
+ begin
84
+ encrypted_node = REXML::XPath.first(document, "/p:LogoutRequest/a:EncryptedID", { "p" => PROTOCOL, "a" => ASSERTION })
85
+ if encrypted_node
86
+ node = decrypt_nameid(encrypted_node)
87
+ else
88
+ node = REXML::XPath.first(document, "/p:LogoutRequest/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
89
+ end
90
+ end
91
+ end
92
+
93
+ # Decrypts an EncryptedID element
94
+ # @param encrypted_id_node [REXML::Element] The EncryptedID element
95
+ # @return [REXML::Document] The decrypted EncrypedtID element
96
+ #
97
+ def decrypt_nameid(encrypted_id_node)
98
+
99
+ if settings.nil? || settings.get_sp_decryption_keys.empty?
100
+ raise ValidationError.new('An ' + encrypted_id_node.name + ' found and no SP private key found on the settings to decrypt it')
101
+ end
102
+
103
+ elem_plaintext = OneLogin::RubySaml::Utils.decrypt_multi(encrypted_id_node, settings.get_sp_decryption_keys)
104
+ # If we get some problematic noise in the plaintext after decrypting.
105
+ # This quick regexp parse will grab only the Element and discard the noise.
106
+ elem_plaintext = elem_plaintext.match(/(.*<\/(\w+:)?NameID>)/m)[0]
107
+
108
+ # To avoid namespace errors if saml namespace is not defined
109
+ # create a parent node first with the namespace defined
110
+ node_header = '<node xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">'
111
+ elem_plaintext = node_header + elem_plaintext + '</node>'
112
+ doc = REXML::Document.new(elem_plaintext)
113
+ doc.root[0]
114
+ end
115
+
85
116
  # @return [String|nil] Gets the ID attribute from the Logout Request. if exists.
86
117
  #
87
118
  def id
@@ -130,6 +161,12 @@ module OneLogin
130
161
 
131
162
  private
132
163
 
164
+ # returns the allowed clock drift on timing validation
165
+ # @return [Float]
166
+ def allowed_clock_drift
167
+ options[:allowed_clock_drift].to_f.abs + Float::EPSILON
168
+ end
169
+
133
170
  # Hard aux function to validate the Logout Request
134
171
  # @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true)
135
172
  # @return [Boolean] TRUE if the Logout Request is valid
@@ -180,15 +217,17 @@ module OneLogin
180
217
  true
181
218
  end
182
219
 
183
- # Validates the time. (If the logout request was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
220
+ # Validates the time. (If the logout request was initialized with the :allowed_clock_drift
221
+ # option, the timing validations are relaxed by the allowed_clock_drift value)
184
222
  # If fails, the error is added to the errors array
185
223
  # @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
186
224
  # @raise [ValidationError] if soft == false and validation fails
187
225
  #
188
226
  def validate_not_on_or_after
189
227
  now = Time.now.utc
190
- if not_on_or_after && now >= (not_on_or_after + (options[:allowed_clock_drift] || 0))
191
- return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after})")
228
+
229
+ if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
230
+ return append_error("Current time is on or after NotOnOrAfter (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})")
192
231
  end
193
232
 
194
233
  true
@@ -241,32 +280,8 @@ module OneLogin
241
280
  return true unless options.has_key? :get_params
242
281
  return true unless options[:get_params].has_key? 'Signature'
243
282
 
244
- # SAML specifies that the signature should be derived from a concatenation
245
- # of URI-encoded values _as sent by the IDP_:
246
- #
247
- # > Further, note that URL-encoding is not canonical; that is, there are multiple legal encodings for a given
248
- # > value. The relying party MUST therefore perform the verification step using the original URL-encoded
249
- # > values it received on the query string. It is not sufficient to re-encode the parameters after they have been
250
- # > processed by software because the resulting encoding may not match the signer's encoding.
251
- #
252
- # <http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf>
253
- #
254
- # If we don't have the original parts (for backward compatibility) required to correctly verify the signature,
255
- # then fabricate them by re-encoding the parsed URI parameters, and hope that we're lucky enough to use
256
- # the exact same URI-encoding as the IDP. (This is not the case if the IDP is ADFS!)
257
- options[:raw_get_params] ||= {}
258
- if options[:raw_get_params]['SAMLRequest'].nil? && !options[:get_params]['SAMLRequest'].nil?
259
- options[:raw_get_params]['SAMLRequest'] = CGI.escape(options[:get_params]['SAMLRequest'])
260
- end
261
- if options[:raw_get_params]['RelayState'].nil? && !options[:get_params]['RelayState'].nil?
262
- options[:raw_get_params]['RelayState'] = CGI.escape(options[:get_params]['RelayState'])
263
- end
264
- if options[:raw_get_params]['SigAlg'].nil? && !options[:get_params]['SigAlg'].nil?
265
- options[:raw_get_params]['SigAlg'] = CGI.escape(options[:get_params]['SigAlg'])
266
- end
283
+ options[:raw_get_params] = OneLogin::RubySaml::Utils.prepare_raw_get_params(options[:raw_get_params], options[:get_params], settings.security[:lowercase_url_encoding])
267
284
 
268
- # If we only received the raw version of SigAlg,
269
- # then parse it back into the decoded params hash for convenience.
270
285
  if options[:get_params]['SigAlg'].nil? && !options[:raw_get_params]['SigAlg'].nil?
271
286
  options[:get_params]['SigAlg'] = CGI.unescape(options[:raw_get_params]['SigAlg'])
272
287
  end
@@ -328,7 +343,6 @@ module OneLogin
328
343
 
329
344
  true
330
345
  end
331
-
332
346
  end
333
347
  end
334
348
  end
@@ -13,7 +13,7 @@ module OneLogin
13
13
  class SloLogoutresponse < SamlMessage
14
14
 
15
15
  # Logout Response ID
16
- attr_reader :uuid
16
+ attr_accessor :uuid
17
17
 
18
18
  # Initializes the Logout Response. A SloLogoutresponse Object that is an extension of the SamlMessage class.
19
19
  # Asigns an ID, a random uuid.
@@ -41,7 +41,7 @@ module OneLogin
41
41
  saml_response = CGI.escape(params.delete("SAMLResponse"))
42
42
  response_params = "#{params_prefix}SAMLResponse=#{saml_response}"
43
43
  params.each_pair do |key, value|
44
- response_params << "&#{key.to_s}=#{CGI.escape(value.to_s)}"
44
+ response_params << "&#{key}=#{CGI.escape(value.to_s)}"
45
45
  end
46
46
 
47
47
  raise SettingError.new "Invalid settings, idp_slo_service_url is not set!" if url.nil? or url.empty?
@@ -70,7 +70,7 @@ module OneLogin
70
70
  response_doc = create_logout_response_xml_doc(settings, request_id, logout_message, logout_status_code)
71
71
  response_doc.context[:attribute_quote] = :quote if settings.double_quote_xml_attribute_values
72
72
 
73
- response = ""
73
+ response = "".dup
74
74
  response_doc.write(response)
75
75
 
76
76
  Logging.debug "Created SLO Logout Response: #{response}"
@@ -78,9 +78,10 @@ module OneLogin
78
78
  response = deflate(response) if settings.compress_response
79
79
  base64_response = encode(response)
80
80
  response_params = {"SAMLResponse" => base64_response}
81
+ sp_signing_key = settings.get_sp_signing_key
81
82
 
82
- if settings.security[:logout_responses_signed] && !settings.security[:embed_sign] && settings.private_key
83
- params['SigAlg'] = settings.security[:signature_method]
83
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:redirect] && settings.security[:logout_responses_signed] && sp_signing_key
84
+ params['SigAlg'] = settings.security[:signature_method]
84
85
  url_string = OneLogin::RubySaml::Utils.build_query(
85
86
  :type => 'SAMLResponse',
86
87
  :data => base64_response,
@@ -88,7 +89,7 @@ module OneLogin
88
89
  :sig_alg => params['SigAlg']
89
90
  )
90
91
  sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(settings.security[:signature_method])
91
- signature = settings.get_sp_key.sign(sign_algorithm.new, url_string)
92
+ signature = sp_signing_key.sign(sign_algorithm.new, url_string)
92
93
  params['Signature'] = encode(signature)
93
94
  end
94
95
 
@@ -150,9 +151,8 @@ module OneLogin
150
151
 
151
152
  def sign_document(document, settings)
152
153
  # embed signature
153
- if settings.security[:logout_responses_signed] && settings.private_key && settings.certificate && settings.security[:embed_sign]
154
- private_key = settings.get_sp_key
155
- cert = settings.get_sp_cert
154
+ cert, private_key = settings.get_sp_signing_pair
155
+ if settings.idp_slo_service_binding == Utils::BINDINGS[:post] && private_key && cert
156
156
  document.sign_document(private_key, cert, settings.security[:signature_method], settings.security[:digest_method])
157
157
  end
158
158
 
@@ -13,20 +13,47 @@ module OneLogin
13
13
  class Utils
14
14
  @@uuid_generator = UUID.new if RUBY_VERSION < '1.9'
15
15
 
16
- DSIG = "http://www.w3.org/2000/09/xmldsig#"
17
- XENC = "http://www.w3.org/2001/04/xmlenc#"
18
- DURATION_FORMAT = %r(^(-?)P(?:(?:(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?)|(?:(\d+)W))$)
16
+ BINDINGS = { :post => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze,
17
+ :redirect => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze }.freeze
18
+ DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze
19
+ XENC = "http://www.w3.org/2001/04/xmlenc#".freeze
20
+ DURATION_FORMAT = %r(^
21
+ (-?)P # 1: Duration sign
22
+ (?:
23
+ (?:(\d+)Y)? # 2: Years
24
+ (?:(\d+)M)? # 3: Months
25
+ (?:(\d+)D)? # 4: Days
26
+ (?:T
27
+ (?:(\d+)H)? # 5: Hours
28
+ (?:(\d+)M)? # 6: Minutes
29
+ (?:(\d+(?:[.,]\d+)?)S)? # 7: Seconds
30
+ )?
31
+ |
32
+ (\d+)W # 8: Weeks
33
+ )
34
+ $)x.freeze
19
35
 
20
- # Checks if the x509 cert provided is expired
21
- #
22
- # @param cert [Certificate] The x509 certificate
36
+ UUID_PREFIX = '_'
37
+ @@prefix = '_'
38
+
39
+ # Checks if the x509 cert provided is expired.
23
40
  #
41
+ # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
42
+ # @return [true|false] Whether the certificate is expired.
24
43
  def self.is_cert_expired(cert)
25
- if cert.is_a?(String)
26
- cert = OpenSSL::X509::Certificate.new(cert)
27
- end
44
+ cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)
28
45
 
29
- return cert.not_after < Time.now
46
+ cert.not_after < Time.now
47
+ end
48
+
49
+ # Checks if the x509 cert provided has both started and has not expired.
50
+ #
51
+ # @param cert [OpenSSL::X509::Certificate|String] The x509 certificate.
52
+ # @return [true|false] Whether the certificate is currently active.
53
+ def self.is_cert_active(cert)
54
+ cert = OpenSSL::X509::Certificate.new(cert) if cert.is_a?(String)
55
+ now = Time.now
56
+ cert.not_before <= now && cert.not_after >= now
30
57
  end
31
58
 
32
59
  # Interprets a ISO8601 duration value relative to a given timestamp.
@@ -37,38 +64,33 @@ module OneLogin
37
64
  # current time.
38
65
  #
39
66
  # @return [Integer] The new timestamp, after the duration is applied.
40
- #
67
+ #
41
68
  def self.parse_duration(duration, timestamp=Time.now.utc)
69
+ return nil if RUBY_VERSION < '1.9' # 1.8.7 not supported
70
+
42
71
  matches = duration.match(DURATION_FORMAT)
43
-
72
+
44
73
  if matches.nil?
45
- raise Exception.new("Invalid ISO 8601 duration")
74
+ raise StandardError.new("Invalid ISO 8601 duration")
46
75
  end
47
76
 
48
- durYears = matches[2].to_i
49
- durMonths = matches[3].to_i
50
- durDays = matches[4].to_i
51
- durHours = matches[5].to_i
52
- durMinutes = matches[6].to_i
53
- durSeconds = matches[7].to_f
54
- durWeeks = matches[8].to_i
55
-
56
- if matches[1] == "-"
57
- durYears = -durYears
58
- durMonths = -durMonths
59
- durDays = -durDays
60
- durHours = -durHours
61
- durMinutes = -durMinutes
62
- durSeconds = -durSeconds
63
- durWeeks = -durWeeks
64
- end
77
+ sign = matches[1] == '-' ? -1 : 1
65
78
 
66
- initial_datetime = Time.at(timestamp).utc.to_datetime
67
- final_datetime = initial_datetime.next_year(durYears)
68
- final_datetime = final_datetime.next_month(durMonths)
69
- final_datetime = final_datetime.next_day((7*durWeeks) + durDays)
70
- final_timestamp = final_datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
71
- return final_timestamp
79
+ durYears, durMonths, durDays, durHours, durMinutes, durSeconds, durWeeks =
80
+ matches[2..8].map do |match|
81
+ if match
82
+ match = match.tr(',', '.').gsub(/\.0*\z/, '')
83
+ sign * (match.include?('.') ? match.to_f : match.to_i)
84
+ else
85
+ 0
86
+ end
87
+ end
88
+
89
+ datetime = Time.at(timestamp).utc.to_datetime
90
+ datetime = datetime.next_year(durYears)
91
+ datetime = datetime.next_month(durMonths)
92
+ datetime = datetime.next_day((7*durWeeks) + durDays)
93
+ datetime.to_time.utc.to_i + (durHours * 3600) + (durMinutes * 60) + durSeconds
72
94
  end
73
95
 
74
96
  # Return a properly formatted x509 certificate
@@ -122,6 +144,28 @@ module OneLogin
122
144
  "-----BEGIN #{key_label}-----\n#{key}\n-----END #{key_label}-----"
123
145
  end
124
146
 
147
+ # Given a certificate string, return an OpenSSL::X509::Certificate object.
148
+ #
149
+ # @param cert [String] The original certificate
150
+ # @return [OpenSSL::X509::Certificate] The certificate object
151
+ #
152
+ def self.build_cert_object(cert)
153
+ return nil if cert.nil? || cert.empty?
154
+
155
+ OpenSSL::X509::Certificate.new(format_cert(cert))
156
+ end
157
+
158
+ # Given a private key string, return an OpenSSL::PKey::RSA object.
159
+ #
160
+ # @param cert [String] The original private key
161
+ # @return [OpenSSL::PKey::RSA] The private key object
162
+ #
163
+ def self.build_private_key_object(private_key)
164
+ return nil if private_key.nil? || private_key.empty?
165
+
166
+ OpenSSL::PKey::RSA.new(format_private_key(private_key))
167
+ end
168
+
125
169
  # Build the Query String signature that will be used in the HTTP-Redirect binding
126
170
  # to generate the Signature
127
171
  # @param params [Hash] Parameters to build the Query String
@@ -161,30 +205,39 @@ module OneLogin
161
205
  #
162
206
  # @param rawparams [Hash] Raw GET Parameters
163
207
  # @param params [Hash] GET Parameters
208
+ # @param lowercase_url_encoding [bool] Lowercase URL Encoding (For ADFS urlencode compatiblity)
164
209
  # @return [Hash] New raw parameters
165
210
  #
166
- def self.prepare_raw_get_params(rawparams, params)
211
+ def self.prepare_raw_get_params(rawparams, params, lowercase_url_encoding=false)
167
212
  rawparams ||= {}
168
213
 
169
214
  if rawparams['SAMLRequest'].nil? && !params['SAMLRequest'].nil?
170
- rawparams['SAMLRequest'] = CGI.escape(params['SAMLRequest'])
215
+ rawparams['SAMLRequest'] = escape_request_param(params['SAMLRequest'], lowercase_url_encoding)
171
216
  end
172
217
  if rawparams['SAMLResponse'].nil? && !params['SAMLResponse'].nil?
173
- rawparams['SAMLResponse'] = CGI.escape(params['SAMLResponse'])
218
+ rawparams['SAMLResponse'] = escape_request_param(params['SAMLResponse'], lowercase_url_encoding)
174
219
  end
175
220
  if rawparams['RelayState'].nil? && !params['RelayState'].nil?
176
- rawparams['RelayState'] = CGI.escape(params['RelayState'])
221
+ rawparams['RelayState'] = escape_request_param(params['RelayState'], lowercase_url_encoding)
177
222
  end
178
223
  if rawparams['SigAlg'].nil? && !params['SigAlg'].nil?
179
- rawparams['SigAlg'] = CGI.escape(params['SigAlg'])
224
+ rawparams['SigAlg'] = escape_request_param(params['SigAlg'], lowercase_url_encoding)
180
225
  end
181
226
 
182
227
  rawparams
183
228
  end
184
229
 
230
+ def self.escape_request_param(param, lowercase_url_encoding)
231
+ CGI.escape(param).tap do |escaped|
232
+ next unless lowercase_url_encoding
233
+
234
+ escaped.gsub!(/%[A-Fa-f0-9]{2}/) { |match| match.downcase }
235
+ end
236
+ end
237
+
185
238
  # Validate the Signature parameter sent on the HTTP-Redirect binding
186
239
  # @param params [Hash] Parameters to be used in the validation process
187
- # @option params [OpenSSL::X509::Certificate] cert The Identity provider public certtificate
240
+ # @option params [OpenSSL::X509::Certificate] cert The IDP public certificate
188
241
  # @option params [String] sig_alg The SigAlg parameter
189
242
  # @option params [String] signature The Signature parameter (base64 encoded)
190
243
  # @option params [String] query_string The full GET Query String to be compared
@@ -201,6 +254,8 @@ module OneLogin
201
254
  # @param status_message [Strig] StatusMessage value
202
255
  # @return [String] The status error message
203
256
  def self.status_error_msg(error_msg, raw_status_code = nil, status_message = nil)
257
+ error_msg = error_msg.dup
258
+
204
259
  unless raw_status_code.nil?
205
260
  if raw_status_code.include? "|"
206
261
  status_codes = raw_status_code.split(' | ')
@@ -221,9 +276,29 @@ module OneLogin
221
276
  error_msg
222
277
  end
223
278
 
279
+ # Obtains the decrypted string from an Encrypted node element in XML,
280
+ # given multiple private keys to try.
281
+ # @param encrypted_node [REXML::Element] The Encrypted element
282
+ # @param private_keys [Array<OpenSSL::PKey::RSA>] The Service provider private key
283
+ # @return [String] The decrypted data
284
+ def self.decrypt_multi(encrypted_node, private_keys)
285
+ raise ArgumentError.new('private_keys must be specified') if !private_keys || private_keys.empty?
286
+
287
+ error = nil
288
+ private_keys.each do |key|
289
+ begin
290
+ return decrypt_data(encrypted_node, key)
291
+ rescue OpenSSL::PKey::PKeyError => e
292
+ error ||= e
293
+ end
294
+ end
295
+
296
+ raise(error) if error
297
+ end
298
+
224
299
  # Obtains the decrypted string from an Encrypted node element in XML
225
- # @param encrypted_node [REXML::Element] The Encrypted element
226
- # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
300
+ # @param encrypted_node [REXML::Element] The Encrypted element
301
+ # @param private_key [OpenSSL::PKey::RSA] The Service provider private key
227
302
  # @return [String] The decrypted data
228
303
  def self.decrypt_data(encrypted_node, private_key)
229
304
  encrypt_data = REXML::XPath.first(
@@ -287,7 +362,7 @@ module OneLogin
287
362
 
288
363
  # Obtains the deciphered text
289
364
  # @param cipher_text [String] The ciphered text
290
- # @param symmetric_key [String] The symetric key used to encrypt the text
365
+ # @param symmetric_key [String] The symmetric key used to encrypt the text
291
366
  # @param algorithm [String] The encrypted algorithm
292
367
  # @return [String] The deciphered text
293
368
  def self.retrieve_plaintext(cipher_text, symmetric_key, algorithm)
@@ -328,8 +403,16 @@ module OneLogin
328
403
  end
329
404
  end
330
405
 
406
+ def self.set_prefix(value)
407
+ @@prefix = value
408
+ end
409
+
410
+ def self.prefix
411
+ @@prefix
412
+ end
413
+
331
414
  def self.uuid
332
- RUBY_VERSION < '1.9' ? "_#{@@uuid_generator.generate}" : "_#{SecureRandom.uuid}"
415
+ "#{prefix}" + (RUBY_VERSION < '1.9' ? "#{@@uuid_generator.generate}" : "#{SecureRandom.uuid}")
333
416
  end
334
417
 
335
418
  # Given two strings, attempt to match them as URIs using Rails' parse method. If they can be parsed,
@@ -1,5 +1,5 @@
1
1
  module OneLogin
2
2
  module RubySaml
3
- VERSION = '1.12.4'
3
+ VERSION = '1.18.1'
4
4
  end
5
5
  end