Skip to content

Commit 04b2c8f

Browse files
authored
fix(auth): renew refresh tokens before expiry (#101)
1 parent 55a2956 commit 04b2c8f

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

‎app/helpers/authentication_helpers.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def set_refresh_cookie_in_response(remember)
237237
token = current_user.auth_tokens.where(token_type: :refresh_token).last
238238

239239
# Generate a new token when the old one is absent or getting close to expiring
240-
if token.nil? || token.auth_token_expiry <= Time.zone.now - 12.hours
240+
if token.nil? || token.auth_token_expiry <= Time.zone.now + 12.hours
241241
token = current_user.generate_authentication_token!(token_type: :refresh_token)
242242
end
243243

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require 'test_helper'
2+
3+
class AuthenticationRefreshCookieTest < ActiveSupport::TestCase
4+
include Rack::Test::Methods
5+
include TestHelpers::JsonHelper
6+
7+
def app
8+
Rails.application
9+
end
10+
11+
setup do
12+
Rack::Attack.reset!
13+
end
14+
15+
def test_remembered_login_rotates_refresh_token_at_renewal_boundary
16+
travel_to Time.zone.parse('2026-08-28 12:00:00 UTC') do
17+
user = FactoryBot.create(:user)
18+
renewal_boundary = Time.zone.now + 12.hours
19+
old_token = user.generate_authentication_token!(
20+
expiry: renewal_boundary,
21+
token_type: :refresh_token
22+
)
23+
24+
post_remembered_login(user)
25+
26+
refresh_tokens = user.auth_tokens.where(token_type: :refresh_token).order(:id)
27+
new_token = refresh_tokens.last
28+
29+
assert_equal 2, refresh_tokens.count
30+
assert_not_equal old_token.id, new_token.id
31+
assert_operator new_token.auth_token_expiry, :>, renewal_boundary
32+
assert_match(/refresh_token=#{new_token.authentication_token};/, last_response.cookies['refresh_token'].to_s)
33+
end
34+
end
35+
36+
def test_remembered_login_reuses_refresh_token_outside_renewal_window
37+
travel_to Time.zone.parse('2026-08-28 12:00:00 UTC') do
38+
user = FactoryBot.create(:user)
39+
old_token = user.generate_authentication_token!(
40+
expiry: Time.zone.now + 12.hours + 1.second,
41+
token_type: :refresh_token
42+
)
43+
44+
post_remembered_login(user)
45+
46+
refresh_tokens = user.auth_tokens.where(token_type: :refresh_token).order(:id)
47+
48+
assert_equal [old_token.id], refresh_tokens.pluck(:id)
49+
assert_match(/refresh_token=#{old_token.authentication_token};/, last_response.cookies['refresh_token'].to_s)
50+
end
51+
end
52+
53+
def test_remembered_login_rotates_expired_refresh_token
54+
travel_to Time.zone.parse('2026-08-28 12:00:00 UTC') do
55+
user = FactoryBot.create(:user)
56+
old_token = user.generate_authentication_token!(
57+
expiry: Time.zone.now - 1.second,
58+
token_type: :refresh_token
59+
)
60+
61+
post_remembered_login(user)
62+
63+
refresh_tokens = user.auth_tokens.where(token_type: :refresh_token).order(:id)
64+
new_token = refresh_tokens.last
65+
66+
assert_equal 2, refresh_tokens.count
67+
assert_not_equal old_token.id, new_token.id
68+
assert_operator new_token.auth_token_expiry, :>, Time.zone.now
69+
assert_match(/refresh_token=#{new_token.authentication_token};/, last_response.cookies['refresh_token'].to_s)
70+
end
71+
end
72+
73+
private
74+
75+
def post_remembered_login(user)
76+
post_json '/api/auth.json', {
77+
username: user.username,
78+
password: 'password',
79+
remember: true
80+
}
81+
82+
assert_equal 201, last_response.status
83+
end
84+
end

0 commit comments

Comments
 (0)