fix(auth): serialize token refresh to prevent self-inflicted logout#991
Merged
Conversation
The credential refresh is a read-modify-write of ~/.railway/config.json with no locking, while the server rotates and reuse-detects refresh tokens on every refresh. Two concurrent CLI invocations past access-token expiry (e.g. a running `railway mcp` plus an interactive command) both present the same refresh token; the second presents an already-consumed token, so the server revokes the entire grant -- a hard logout requiring `railway login`. Serialize the refresh behind an exclusive advisory file lock on a dedicated lockfile (~/.railway/.config.lock). After acquiring the lock we re-read the config and re-check expiry, so only the lock winner performs the refresh and the others pick up its freshly-rotated token. A 10s timeout with a graceful unlocked fallback ensures a stale lock can never wedge the CLI. Also preserve the stored refresh token when a refresh response omits one, rather than nulling it (which would force a re-login on the next refresh). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Problem
The Railway CLI logs users out frequently, forcing repeated
railway login.Root cause is a concurrency race in credential refresh:
~/.railway/config.jsonwith a whole-file, last-writer-wins write and no lock.railway mcpprocess alongside an interactive command, or a shell-prompt integration) both read the same refresh token and both POSTgrant_type=refresh_token. The second presents a now-consumed token → grant revoked → hard logout. Last-writer-wins can also overwrite a freshly-rotated token with a stale one.Confirmed in production: ~128k/7d
refresh token not foundfailures on the server's token endpoint, 99.6% from the CLI's OAuth client.Change
~/.railway/.config.lock, a dedicated lockfile — neverconfig.jsonitself). After acquiring the lock, re-read the config and re-check expiry: only the lock winner refreshes; the others pick up its freshly-rotated token.Companion
This is the client-side half. The complementary server-side change (stop rotating public-client refresh tokens on every refresh) is in
railwayapp/mono: railwayapp/mono#32205.Testing
cargo checkclean;cargo fmt --checkclean.cargo test --bin railway config::(24 passed) andclient::(4 passed).Review notes
fs2advisory locks coordinate correctly between cooperating CLI processes (the race in scope). On network filesystems advisory locking can be unreliable, but~/.railwayis normally local; the unlocked fallback degrades to today's behavior rather than wedging.CONFIG_LOCK_TIMEOUT) — comfortably covers a refresh round-trip; bump if refreshes can ever exceed that.🤖 Generated with Claude Code