Skip to content

Concurrent config.json read-modify-write can clobber rotated OAuth tokens → spurious logouts #8

Description

@NGHINAI

config.Save is a full-file overwrite — os.WriteFile with no temp-file+rename (internal/config/config.go:120-133) — and several code paths do independent load→mutate→save cycles on the whole struct:

  • token refresh persisting rotated tokens (internal/auth/refresh.go:131, SaveTokens)
  • the auto update checker (cmd/root.go:103-131, saves twice per check)
  • anonymous token minting (internal/api/client.go:344-367)
  • analytics distinct-id creation on first run (internal/analytics/analytics.go:52-55)

The refresh flow rotates refresh tokens (the oauth tests expect a new refresh_token back), which makes the race destructive:

  1. A long-lived codag mcp serve session refreshes: config now holds AT-new/RT-2; RT-1 is consumed server-side.
  2. A parallel codag <anything> invocation loaded config before that refresh and saves after it (update checker, anon-token mint, or distinct-id write) — writing AT-old/RT-1 back over the rotated pair.
  3. Next 401 → refresh fires with the revoked RT-1 → ErrUnauthenticated → user is told to codag auth login again.

From the user's side this is "codag randomly logs me out", which is painful to attribute precisely because the failing process isn't the one that caused it. The MCP path makes the overlap window realistic: mcp serve runs for hours while the user also uses the CLI directly.

Related smaller issues with the same root cause:

  • Non-atomic writes mean a crash mid-Save leaves a corrupt config.json; Load then errors and auth paths fail until the file is deleted by hand.
  • N concurrent MCP tool calls on a fresh install each mint an anonymous token (internal/api/client.go:344 has no single-flight) — the auth refresher solves exactly this stampede for OAuth (internal/auth/refresh.go:15-20), but the anon-token path doesn't reuse the pattern.

Suggested fix: atomic write (write temp file in the same dir, then rename) plus read-modify-write narrowing — re-Load immediately before Save and apply only the caller's delta, or guard cross-process with a lock file. Happy to discuss/PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions