From e1d4efc0b0ae3b3f8cc60dd7ef61a0ac8beb3c7c Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Thu, 15 Oct 2026 10:42:03 +0300 Subject: [PATCH] Accept Integer and Float remember-me timestamps from JSON Rememberable only coerced String timestamps, while Timeoutable already handles Integer last_request_at values. Cookie/session JSON can revive the remember timestamp as Integer, Float, or an integer-only string, so remember_me? failed even though the token was still valid. Coerce those forms with Time.at, matching Timeoutable. --- lib/devise/models/rememberable.rb | 12 ++++++++---- test/models/rememberable_test.rb | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/devise/models/rememberable.rb b/lib/devise/models/rememberable.rb index a66979ad59..e20d8dd03e 100644 --- a/lib/devise/models/rememberable.rb +++ b/lib/devise/models/rememberable.rb @@ -101,9 +101,13 @@ def after_remembered end def remember_me?(token, generated_at) - # TODO: Normalize the JSON type coercion along with the Timeoutable hook - # in a single place https://github.com/heartcombo/devise/blob/ffe9d6d406e79108cf32a2c6a1d0b3828849c40b/lib/devise/hooks/timeoutable.rb#L14-L18 - if generated_at.is_a?(String) + # Normalize cookie/session JSON type coercion the same way Timeoutable + # does for last_request_at (Integer from to_i, Float/numeric strings + # from to_f / to_json). + case generated_at + when Integer, Float + generated_at = Time.at(generated_at).utc + when String generated_at = time_from_json(generated_at) end @@ -122,7 +126,7 @@ def remember_me?(token, generated_at) private def time_from_json(value) - if value =~ /\A\d+\.\d+\Z/ + if value =~ /\A\d+(?:\.\d+)?\Z/ Time.at(value.to_f) else Time.parse(value) rescue nil diff --git a/test/models/rememberable_test.rb b/test/models/rememberable_test.rb index 8b83172120..a1cc9fd45a 100644 --- a/test/models/rememberable_test.rb +++ b/test/models/rememberable_test.rb @@ -66,6 +66,23 @@ def create_resource assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc.to_f.to_json) end + test 'serialize from cookie should accept Integer and Float timestamps from JSON coercion' do + user = create_user + user.remember_me! + assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc.to_f) + + # Integer seconds truncate subseconds, so keep remember_created_at clearly earlier. + user.update_attribute(:remember_created_at, 1.minute.ago) + assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc.to_i) + end + + test 'serialize from cookie should accept an Integer-only numeric String timestamp' do + user = create_user + user.remember_me! + user.update_attribute(:remember_created_at, 1.minute.ago) + assert_equal user, User.serialize_from_cookie(user.to_key, user.authenticatable_salt, Time.now.utc.to_i.to_s) + end + test 'serialize from cookie should return nil with invalid datetime' do user = create_user user.remember_me!