Skip to content

OAuth2 refresh races between MCP processes sharing one token store #102

Open

Description

@pddkhanh

Problem

Independent xurl mcp processes on the same machine, using the same app/user and shared ~/.xurl/auth.yml, can attempt to redeem the same stale OAuth2 refresh token. Each process loads its own token-store snapshot. Refresh is serialized within one bridge, but not across processes, and the store is not reloaded before refreshing.

Once one process rotates the grant, another process can use the old refresh token. At startup, mcpBridge.bootstrap() treats any token-resolution error as a reason to open interactive browser login and does not preserve the original error in its diagnostic.

This is distinct from #22: it requires neither separate machines nor a second interactive authorization. The clients intentionally share one persisted login.

Environment

  • Official xurl v1.3.4, commit 18dcb447f090667171ff23666a05d6d387e2aa73
  • macOS arm64
  • Multiple stdio MCP clients, including Codex and Claude Code, launching the same xurl mcp https://api.x.com/mcp bridge

Deterministic reproduction

I reproduced the refresh defect using four independent test processes and a local HTTP token endpoint. No real X credentials or calls are needed:

  1. Persist one expired access token and a refresh token in a temporary shared store.
  2. Start four independent Auth instances in separate processes. Make all four load the store before proceeding.
  3. Release a barrier so they request a valid token together.
  4. Have the fake provider accept the old refresh token once, return a replacement access/refresh pair, and reject reuse with invalid_grant.

Actual on stock v1.3.4: at least one child process fails because it submits the stale grant.

Expected: one process refreshes and persists the replacement; other processes reload it and reuse the new access token. The endpoint receives exactly one refresh request.

A local prototype with a cross-process file lock, reload inside the lock, and atomic store replacement passes that test. It also handles a forced refresh when another process has already replaced the token.

Scope of the evidence

This establishes the refresh-coordination defect. Repeated browser login pages prompted the investigation, but I cannot prove that this race accounts for every observed popup. Access tokens expiring after two hours is expected; a stored refresh token should permit silent renewal.

The proposed fix and regression test are in #103. No credentials, account-specific configuration, or private logs are included.

Activity

  1. maartengoet commented on Oct 17, 2026

    @maartengoet

    Same failure mode here on macOS arm64 with xurl 1.3.4 via npx: Codex (ChatGPT desktop) keeps one xurl mcp bridge per thread alive for days, plus one bridge per Claude Code session, all sharing one ~/.xurl/auth.yml. New bridges hit the bootstrap fallback (no valid OAuth2 token; opening the browser to sign in) while the already-running ones silently fail their refresh.

    Reviewed and tested #103: go vet, go test -race (incl. the 4-process regression test), Windows/Linux cross-builds and gofmt all pass, and a build of that branch now runs as the bridge here without the browser prompt. Details in #103 (review)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions