Skip to content

Commit dfab94b

Browse files
authored
Merge pull request #605 from johnnyshields/fix-clock-drift
:allowed_clock_drift should be bidrectional
2 parents 83d559b + b79859f commit dfab94b

File tree

5 files changed

+102
-37
lines changed

5 files changed

+102
-37
lines changed

lib/onelogin/ruby-saml/response.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,9 @@ def audiences
337337
end
338338

339339
# returns the allowed clock drift on timing validation
340-
# @return [Integer]
340+
# @return [Float]
341341
def allowed_clock_drift
342-
return options[:allowed_clock_drift].to_f
342+
options[:allowed_clock_drift].to_f.abs + Float::EPSILON
343343
end
344344

345345
# Checks if the SAML Response contains or not an EncryptedAssertion element
@@ -692,13 +692,13 @@ def validate_conditions
692692

693693
now = Time.now.utc
694694

695-
if not_before && (now_with_drift = now + allowed_clock_drift) < not_before
696-
error_msg = "Current time is earlier than NotBefore condition (#{now_with_drift} < #{not_before})"
695+
if not_before && now < (not_before - allowed_clock_drift)
696+
error_msg = "Current time is earlier than NotBefore condition (#{now} < #{not_before}#{" - #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
697697
return append_error(error_msg)
698698
end
699699

700-
if not_on_or_after && now >= (not_on_or_after_with_drift = not_on_or_after + allowed_clock_drift)
701-
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after_with_drift})"
700+
if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
701+
error_msg = "Current time is on or after NotOnOrAfter condition (#{now} >= #{not_on_or_after}#{" + #{allowed_clock_drift.ceil}s" if allowed_clock_drift > 0})"
702702
return append_error(error_msg)
703703
end
704704

@@ -740,7 +740,7 @@ def validate_session_expiration(soft = true)
740740
return true if session_expires_at.nil?
741741

742742
now = Time.now.utc
743-
unless (session_expires_at + allowed_clock_drift) > now
743+
unless now < (session_expires_at + allowed_clock_drift)
744744
error_msg = "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
745745
return append_error(error_msg)
746746
end
@@ -778,8 +778,8 @@ def validate_subject_confirmation
778778

779779
attrs = confirmation_data_node.attributes
780780
next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
781-
(attrs.include? "NotOnOrAfter" and (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift) <= now) ||
782-
(attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift)) ||
781+
(attrs.include? "NotBefore" and now < (parse_time(confirmation_data_node, "NotBefore") - allowed_clock_drift)) ||
782+
(attrs.include? "NotOnOrAfter" and now >= (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift)) ||
783783
(attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
784784

785785
valid_subject_confirmation = true

lib/onelogin/ruby-saml/slo_logoutrequest.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ def session_indexes
130130

131131
private
132132

133+
# returns the allowed clock drift on timing validation
134+
# @return [Float]
135+
def allowed_clock_drift
136+
options[:allowed_clock_drift].to_f.abs + Float::EPSILON
137+
end
138+
133139
# Hard aux function to validate the Logout Request
134140
# @param collect_errors [Boolean] Stop validation when first error appears or keep validating. (if soft=true)
135141
# @return [Boolean] TRUE if the Logout Request is valid
@@ -180,15 +186,17 @@ def validate_version
180186
true
181187
end
182188

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)
189+
# Validates the time. (If the logout request was initialized with the :allowed_clock_drift
190+
# option, the timing validations are relaxed by the allowed_clock_drift value)
184191
# If fails, the error is added to the errors array
185192
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
186193
# @raise [ValidationError] if soft == false and validation fails
187194
#
188195
def validate_not_on_or_after
189196
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})")
197+
198+
if not_on_or_after && now >= (not_on_or_after + allowed_clock_drift)
199+
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})")
192200
end
193201

194202
true

test/response_test.rb

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,44 +1107,74 @@ def generate_audience_error(expected, actual)
11071107
end
11081108
end
11091109

1110-
it "optionally allows for clock drift" do
1110+
it "optionally allows for clock drift on NotBefore" do
1111+
settings.soft = true
1112+
11111113
# The NotBefore condition in the document is 2011-06-14T18:21:01.516Z
11121114
Timecop.freeze(Time.parse("2011-06-14T18:21:01Z")) do
1113-
settings.soft = true
11141115
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
11151116
response_document_with_saml2_namespace,
11161117
:allowed_clock_drift => 0.515,
11171118
:settings => settings
11181119
)
11191120
assert !special_response_with_saml2_namespace.send(:validate_conditions)
1120-
end
11211121

1122-
Timecop.freeze(Time.parse("2011-06-14T18:21:01Z")) do
11231122
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
11241123
response_document_with_saml2_namespace,
11251124
:allowed_clock_drift => 0.516
11261125
)
11271126
assert special_response_with_saml2_namespace.send(:validate_conditions)
1128-
end
11291127

1130-
Timecop.freeze(Time.parse("2011-06-14T18:21:01Z")) do
1131-
settings.soft = true
11321128
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
11331129
response_document_with_saml2_namespace,
11341130
:allowed_clock_drift => '0.515',
11351131
:settings => settings
11361132
)
11371133
assert !special_response_with_saml2_namespace.send(:validate_conditions)
1138-
end
11391134

1140-
Timecop.freeze(Time.parse("2011-06-14T18:21:01Z")) do
11411135
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
11421136
response_document_with_saml2_namespace,
11431137
:allowed_clock_drift => '0.516'
11441138
)
11451139
assert special_response_with_saml2_namespace.send(:validate_conditions)
11461140
end
11471141
end
1142+
1143+
it "optionally allows for clock drift on NotOnOrAfter" do
1144+
# Java Floats behave differently than MRI
1145+
java = defined?(RUBY_ENGINE) && %w[jruby truffleruby].include?(RUBY_ENGINE)
1146+
1147+
settings.soft = true
1148+
1149+
# The NotBefore condition in the document is 2011-06-1418:31:01.516Z
1150+
Timecop.freeze(Time.parse("2011-06-14T18:31:02Z")) do
1151+
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
1152+
response_document_with_saml2_namespace,
1153+
:allowed_clock_drift => 0.483,
1154+
:settings => settings
1155+
)
1156+
assert !special_response_with_saml2_namespace.send(:validate_conditions)
1157+
1158+
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
1159+
response_document_with_saml2_namespace,
1160+
:allowed_clock_drift => java ? 0.485 : 0.484
1161+
)
1162+
assert special_response_with_saml2_namespace.send(:validate_conditions)
1163+
1164+
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
1165+
response_document_with_saml2_namespace,
1166+
:allowed_clock_drift => '0.483',
1167+
:settings => settings
1168+
)
1169+
assert !special_response_with_saml2_namespace.send(:validate_conditions)
1170+
1171+
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
1172+
response_document_with_saml2_namespace,
1173+
:allowed_clock_drift => java ? '0.485' : '0.484'
1174+
)
1175+
assert special_response_with_saml2_namespace.send(:validate_conditions)
1176+
end
1177+
end
11481178
end
11491179

11501180
describe "#attributes" do

test/slo_logoutrequest_test.rb

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class RubySamlTest < Minitest::Test
109109
end
110110
end
111111

112-
describe "#not_on_or_after" do
112+
describe "#not_on_or_after" do
113113
it "extract the value of the NotOnOrAfter attribute" do
114114
time_value = '2014-07-17T01:01:48Z'
115115
assert_nil logout_request.not_on_or_after
@@ -158,25 +158,52 @@ class RubySamlTest < Minitest::Test
158158
it "return true when the logout request has a valid NotOnOrAfter or does not contain any" do
159159
assert logout_request.send(:validate_not_on_or_after)
160160
assert_empty logout_request.errors
161-
Timecop.freeze Time.parse('2011-06-14T18:25:01.516Z') do
162-
time_value = '2014-07-17T01:01:48Z'
163-
logout_request.document.root.attributes['NotOnOrAfter'] = time_value
161+
162+
Timecop.freeze Time.parse('2014-07-17T01:01:47Z') do
163+
logout_request.document.root.attributes['NotOnOrAfter'] = '2014-07-17T01:01:48Z'
164164
assert logout_request.send(:validate_not_on_or_after)
165165
assert_empty logout_request.errors
166166
end
167167
end
168168

169169
it "return false when the logout request has an invalid NotOnOrAfter" do
170-
logout_request.document.root.attributes['NotOnOrAfter'] = '2014-07-17T01:01:48Z'
171-
assert !logout_request.send(:validate_not_on_or_after)
172-
assert /Current time is on or after NotOnOrAfter/.match(logout_request.errors[0])
170+
Timecop.freeze Time.parse('2014-07-17T01:01:49Z') do
171+
logout_request.document.root.attributes['NotOnOrAfter'] = '2014-07-17T01:01:48Z'
172+
assert !logout_request.send(:validate_not_on_or_after)
173+
assert /Current time is on or after NotOnOrAfter/.match(logout_request.errors[0])
174+
end
173175
end
174176

175177
it "raise when the logout request has an invalid NotOnOrAfter" do
176-
logout_request.document.root.attributes['NotOnOrAfter'] = '2014-07-17T01:01:48Z'
177-
logout_request.soft = false
178-
assert_raises(OneLogin::RubySaml::ValidationError, "Current time is on or after NotOnOrAfter") do
179-
logout_request.send(:validate_not_on_or_after)
178+
Timecop.freeze Time.parse('2014-07-17T01:01:49Z') do
179+
logout_request.document.root.attributes['NotOnOrAfter'] = '2014-07-17T01:01:48Z'
180+
logout_request.soft = false
181+
assert_raises(OneLogin::RubySaml::ValidationError, "Current time is on or after NotOnOrAfter") do
182+
logout_request.send(:validate_not_on_or_after)
183+
end
184+
end
185+
end
186+
187+
it "optionally allows for clock drift" do
188+
# Java Floats behave differently than MRI
189+
java = defined?(RUBY_ENGINE) && %w[jruby truffleruby].include?(RUBY_ENGINE)
190+
191+
logout_request.soft = true
192+
logout_request.document.root.attributes['NotOnOrAfter'] = '2011-06-14T18:31:01.516Z'
193+
194+
# The NotBefore condition in the document is 2011-06-1418:31:01.516Z
195+
Timecop.freeze(Time.parse("2011-06-14T18:31:02Z")) do
196+
logout_request.options[:allowed_clock_drift] = 0.483
197+
assert !logout_request.send(:validate_not_on_or_after)
198+
199+
logout_request.options[:allowed_clock_drift] = java ? 0.485 : 0.484
200+
assert logout_request.send(:validate_not_on_or_after)
201+
202+
logout_request.options[:allowed_clock_drift] = '0.483'
203+
assert !logout_request.send(:validate_not_on_or_after)
204+
205+
logout_request.options[:allowed_clock_drift] = java ? '0.485' : '0.484'
206+
assert logout_request.send(:validate_not_on_or_after)
180207
end
181208
end
182209
end

test/xml_security_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ class XmlSecurityTest < Minitest::Test
302302
let (:response) { OneLogin::RubySaml::Response.new(fixture(:starfield_response)) }
303303

304304
before do
305-
response.settings = OneLogin::RubySaml::Settings.new( :idp_cert_fingerprint => "8D:BA:53:8E:A3:B6:F9:F1:69:6C:BB:D9:D8:BD:41:B3:AC:4F:9D:4D")
305+
response.settings = OneLogin::RubySaml::Settings.new(:idp_cert_fingerprint => "8D:BA:53:8E:A3:B6:F9:F1:69:6C:BB:D9:D8:BD:41:B3:AC:4F:9D:4D")
306306
end
307307

308308
it "be able to validate a good response" do
@@ -320,19 +320,19 @@ class XmlSecurityTest < Minitest::Test
320320
time_2 = 'Tue Nov 20 17:55:00 UTC 2012 < Wed Nov 28 17:53:45 UTC 2012'
321321

322322
errors = [time_1, time_2].map do |time|
323-
"Current time is earlier than NotBefore condition (#{time})"
323+
"Current time is earlier than NotBefore condition (#{time} - 1s)"
324324
end
325325

326-
assert_predicate response.errors & errors, :any?
326+
assert_predicate(response.errors & errors, :any?)
327327
end
328328
end
329329

330330
it "fail after response expires" do
331331
Timecop.freeze Time.parse('2012-11-30 17:55:00 UTC') do
332332
assert !response.is_valid?
333333

334-
contains_expected_error = response.errors.include? "Current time is on or after NotOnOrAfter condition (2012-11-30 17:55:00 UTC >= 2012-11-28 18:33:45 UTC)"
335-
contains_expected_error ||= response.errors.include? "Current time is on or after NotOnOrAfter condition (Fri Nov 30 17:55:00 UTC 2012 >= Wed Nov 28 18:33:45 UTC 2012)"
334+
contains_expected_error = response.errors.include?("Current time is on or after NotOnOrAfter condition (2012-11-30 17:55:00 UTC >= 2012-11-28 18:33:45 UTC + 1s)")
335+
contains_expected_error ||= response.errors.include?("Current time is on or after NotOnOrAfter condition (Fri Nov 30 17:55:00 UTC 2012 >= Wed Nov 28 18:33:45 UTC 2012 + 1s)")
336336
assert contains_expected_error
337337
end
338338
end

0 commit comments

Comments
 (0)