openai / openai/codex

[macOS] Freshly issued refresh token rejected as refresh_token_invalidated ~10s later, reproduced with a single local process (trigger: opening a thread)

Open
#40,541 10 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server auth bug
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

Edited for concision. An earlier version blamed the app-server for not adopting new credentials; that was wrong and is retracted (comment).

TL;DR
  • Severity: P0. Sign in succeeds, then opening a thread signs you out 6–17 s later, indefinitely. It is impossible to do anything in Codex Desktop until this is resolved. Still present after updating to 26.820.60940 / core 0.150.0-alpha.8. 20+ open issues in the past week report the same thing on macOS and Windows (list at the bottom).
  • Cause: the Desktop calls getAuthStatus{refreshToken:true} on thread open, which forces a refresh-token rotation, and the auth server rejects the just-issued token with refresh_token_invalidated.
  • Reproduced with one local codex process and no intervening refresh POST, so the invalidation is server-side (or the closed-source Desktop shell) — not in this repo.
  • Client code doesn't cause it but amplifies it: it rotates on every thread open with no freshness check. Hardening suggestions below.
  • In-app feedback thread: 01a036de-1b6d-71d0-8de3-e1595279a0dd
What version of the Codex App are you using (From "About Codex" dialog)?

26.818.61809 (build 7019) — bundled core codex-cli 0.149.0-alpha.4.3

What subscription do you have?

ChatGPT Pro

What platform is your computer?

macOS 26.6.2 (build 25G83), Apple Silicon (arm64)

What issue are you seeing?

One process, 10 seconds, freshly issued token:

21:23:06  oauth token exchange succeeded status=200 OK
21:23:07  Reloading auth
21:23:07  Reloaded auth, changed: false        <-- logging artifact, see below
21:23:16  app-server request: thread/resume    <-- user opened a thread
21:23:16  Reloading auth for account <account-C>
21:23:16  Reloaded auth, changed: false
21:23:16  Refreshing token
21:23:16  ERROR Failed to refresh token: 401 Unauthorized
          {"error":{"code":"refresh_token_invalidated", ...}}
Why the token presented at :16 is provably the one issued at :06
  • Reaching Refreshing token means the guarded reload returned ReloadedNoChange, i.e. auths_equal_for_refresh was true — for ChatGPT that compares the full auth.json contents (manager.rs:2471-2473). Cache and disk were byte-identical.
  • The cache was loaded at :07 from the file written by the :06 exchange (persist is awaited before the success redirect, server.rs:437-457).
  • The only refresh-grant POST in the codebase (manager.rs:1583) is reachable only through a path that always logs Refreshing token first (manager.rs:2818). No such line exists from this pid between :07 and :16.

Caveat: the Desktop shell is closed-source and could present the same refresh token itself — "one codex process" ≠ "one client".

The changed: false log line is misleading

CodexAuth::eq (manager.rs:154-163) compares only the auth mode for ChatGPT. set_cached_auth logs changed from that coarse comparison while installing the new auth unconditionally (manager.rs:2575-2582); the content-sensitive auth_changed_for_refresh is computed but never logged. So a ChatGPT→ChatGPT re-login always logs changed: false on a fully successful adoption.

Client-side amplifiers (not causes)
  • Unconditional rotation. refresh_token_if_requested (account_processor.rs:1019-1032) POSTs whenever refreshToken:true is passed — no freshness check, even though the refreshToken:false path already gates on should_refresh_proactively. This log: 256 rotations vs 3 skips.
  • No cross-process safety. refresh_lock (manager.rs:2045) is per-process; FileAuthStorage::save (storage.rs:206-223) is truncate+write, and torn reads are silently cached as None (manager.rs:2559-2560).
  • Stranded after account switch. A process whose cached account no longer matches auth.json hits ReloadOutcome::Skipped forever (manager.rs:2446-2452) — 128 occurrences here — and the error is never recorded, so getAuthStatus keeps reporting the stale account.
  • Three account ids were active on this machine that day; every failing cycle postdates the third. Correlation only (cf. #39199).
What steps can reproduce the bug?
  1. Sign in to Codex Desktop on macOS.
  2. Open an existing thread.
  3. Signed out (refresh_token_invalidated). Sign in again → repeats.
Pull the same evidence from any affected machine
sqlite3 -readonly "file:$HOME/.codex/logs_2.sqlite?mode=ro" "
  SELECT datetime(ts,'unixepoch','localtime'), process_uuid, feedback_log_body
  FROM logs
  WHERE feedback_log_body LIKE '%token exchange succeeded%'
     OR feedback_log_body LIKE '%Reloading auth for account%'
     OR feedback_log_body LIKE '%Refreshing token%'
     OR feedback_log_body LIKE '%account id mismatch%'
     OR feedback_log_body LIKE '%refresh_token_invalidated%'
  ORDER BY ts;"
What is the expected behavior?

A refresh token issued seconds earlier should not be rejected, and opening a thread should not force a rotation at all.

Suggested hardening (none is the root cause):

  1. Log auth_changed_for_refresh next to changed (manager.rs:2581) — one line.
  2. Make refreshToken:true honor should_refresh_proactively (or a short min interval) instead of always POSTing.
  3. temp-file+rename in FileAuthStorage::save; file lock around refresh→persist.
  4. On Skipped, surface an "account switched — sign in again" state instead of stranding the process.
  5. If the POST succeeds but persist fails (manager.rs:3019-3025), keep the rotated token in memory rather than losing it.
Additional information

Latent, probably unrelated: AutoAuthStorage (storage.rs:431-450) — save() falls back to the file on keyring failure without clearing the stale keyring entry, and load() prefers the keyring, so later loads return old tokens indefinitely. Worth its own issue.

Other reports of the same bug

Same mechanism — a fresh refresh token rejected as refresh_token_invalidated / auth lost on thread open or right after login:

  • macOS: #39162 (58 comments), #39803 (18), #39886, #39684, #40267, #40395, #40373, #40143, #40664, #40242
  • Windows: #39189 (22), #39170 (15), #39696 (12), #40036 (12), #39491 (10), #39925, #39966, #39199, #40632, #40699, #40704 (on 26.820.60940), #40374, #40761, #40603, #40074
  • Onset: #40138 (401s begin on update 0.148.0 → 0.149.0-alpha.4.1)

Roughly 200 user comments across these in eight days, with no maintainer response on any of them as of this edit.

Temporary workaround for Desktop users (patched core, tested on both 0.149.0-alpha.4.3 and 0.150.0-alpha.8): see this comment.

Account ids replaced with placeholders. Full log extract available privately on request.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with refresh_token_if_requested in account_processor.rs, then trace the auth reload and persistence paths in manager.rs and storage.rs, especially the cited line ranges. Reproduce the thread-open flow and inspect the existing auth tests or logs; done means the reported refresh-token failure is addressed and the expected sign-in state remains usable.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
authentication, cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.