Repository navigation
Do not restore a flushed refresh token during a refresh - #148
Merged
tuwukee merged 2 commits intoOct 13, 2026
Merged
Conversation
A refresh that ran at the same time as a flush of its session wrote csrf, access_uid and access_expiration to the removed refresh token again. With Redis, the key had no expiration field and no TTL, so no later flush found it. With the memory store, the flushed session stayed refreshable. The refresh also returned a new access token that stayed valid after the flush. update_refresh now writes only while the refresh token exists, and returns false otherwise. The session then removes the access token that it just created and raises Unauthorized.
doutatsu
force-pushed
the
fix-refresh-recreating-revoked-session
branch
from
October 8, 2026 08:16
6022cc5 to
a784bbd
Compare
Owner
|
Thank you! I need to look into CI, it needs some maintenance, will merge once it's green.... |
…g-revoked-session
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A flush does not always end a session. If a refresh of the same session runs at the same time as the flush, the refresh writes part of the refresh token again.
Why this is a problem
Apps flush a session to end it: on sign-out, on "sign out of all devices", or after a password change. They do this when they think that someone else has the token.
noeviction, these keys use memory until Redis is full. With an eviction policy, Redis can remove keys that are still in use to make space.How it happens
Session#refreshand#refresh_by_access_payloaddo these steps:RefreshToken#update.If a flush removes the refresh token between steps 1 and 3:
update_refreshrunsHMSETon the removed key.HMSETcreates the key again with onlycsrf,access_expirationandaccess_uid, and with no TTL.fetch_refreshignores a key withoutexpiration, soflush_namespacedandflush_alldo not see this key.update_refresh_fieldsmerges the fields into{}and stores the record again, without an expiration.Reproduction against 3.2.4 with the Redis store:
Fix
update_refreshwrites only while the refresh token exists. Otherwise, it returnsfalse. The Redis store does the check and the write in one Lua script, so no other command can run between them.persist_refreshnow writes all four fields itself. Before, it usedupdate_refresh.update_refreshreturnsfalse,Sessionremoves the access token that it just created and raisesUnauthorized("Refresh token not found").flush_namespaced_access_tokensfrom writing a token again after another flush removed it.Only an explicit
falsestops the refresh. A custom store adapter whoseupdate_refreshreturnsnilor"OK"continues to work.Tests
update_refreshafterdestroy_refreshfor both stores, and a refresh that races a flush inTestSession. All three fail without the fix.main.ymlin Docker (Ruby 3.3, Rails 7.0). The unit tests and the Sinatra specs pass on both stores.The CI on this PR fails for reasons that also apply to
main:TestRefreshTokendepends on the test order. ItssetupcallsSession.flush_allbefore it sets a signing key. It fails when earlier tests left refresh tokens and also removed the key. Onmain, 12 of 60 seeds fail with the same 4 errors.concurrent-ruby.config.fixture_path, because rspec-rails 7 removed it. Withfixture_paths, they pass on both stores, with and without this change.