Codex does not auto-refresh routed MCP OAuth tokens even when a refresh token is stored
Open 💬 34 comments Opened Apr 9, 2026 by infoseekAI
💡 Likely answer: A maintainer (github-actions[bot], contributor)
responded on this thread — see the highlighted reply below.
What issue are you seeing?
Summary
Codex persists a refresh_token for a routed MCP server in ~/.codex/.credentials.json, but it does not automatically refresh the access token when the token expires.
As a result, once the access token expires:
- MCP tool calls start failing with auth errors
- automations and later runs break
- the only recovery is manual CLI login and browser reauth
This defeats the purpose of storing a refresh token.
Environment
- Product: Codex desktop / Codex MCP client. MacOS 26.3.1 (a)
- Date observed: April 9, 2026
Expected behavior
If Codex has a valid stored refresh_token for the MCP server, it should automatically refresh the expired access token and continue making authenticated MCP calls without requiring manual CLI login.
This is especially important for:
- automations
- recurring background runs
- later thread/tool invocations after some idle time
Actual behavior
When the access token expires, Codex starts returning auth failures instead of using the stored refresh token to recover automatically.
Observed failure:
{"success": false, "error": "invalid_grant", "msg": "Authorization required"}
### What steps can reproduce the bug?
- Auth to MCP, observe the credentials and refresh token stored in credentials.json
- Wait for the token expiry, access the MCP again (prior to refresh token expiry), it will fail on auth and not automatically refresh as it should
- Use the CLI to authenticate manually, it will then work again
### What is the expected behavior?
Within the refresh token expiry window re-auth should be automatic
### Additional information
_No response_
34 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Yes. In practical terms, OAuth-backed MCPs are dead in Codex right now.
Not because provider auth is broken, but because Codex’s built-in MCP execution path is failing to use valid stored OAuth credentials. The proof is already clear:
codex mcp login succeeds
/Users/xyz/.codex/.credentials.json contains valid tokens
direct refresh works
direct authenticated MCP calls work
built-in MCP tool calls still return 401 oauth_required
So from the user’s perspective, the result is the same: any MCP that depends on OAuth is unusable in Codex until that runtime layer is fixed.
Adding a concrete datapoint that seems related but more specific than token refresh:
In my environment, Codex App (macOS) appears to send MCP requests using a token issued to the ChatGPT OAuth client, not the Codex OAuth client.
What I observed:
stewreads.com) from both ChatGPT app and Codex app.client_id/aud) for the ChatGPT client.Control test:
Repro:
1) Connect MCP server in ChatGPT app and complete OAuth.
2) Connect same MCP server in Codex app and complete OAuth.
3) Invoke MCP tools from Codex app.
4) Inspect token claims in MCP server logs:
client_id/audmaps to ChatGPT client.5) Run Codex CLI against same server and compare:
client_id/audmaps correctly to Codex client.Expected:
Actual:
We run a few MCP Servers and our users are reporting the same with the codex desktop app (both windows & mac)
In the Codex desktop app, MCP servers authenticated via OAuth stop working after the access token expires. The app appears to keep using the expired access token for the current chat instead of refreshing it with the refresh token.
Starting a new chat restores functionality, and manually restarting the MCP server also restores it temporarily. The Codex CLI does not have this issue & handles token refresh correctly.
Codex Desktop App (Version 26.415.30602 (1773))
MCP Auth type: OAuth Token lifetime: access token = 1 hour, refresh token = 30 days
Steps to reproduce
Actual result
Expected result
When the access token expires, the desktop app should automatically refresh it using the OAuth refresh token and continue making MCP requests seamlessly.
This is the behavior currently observed in codex CLI.
Workaround
In the Codex desktop app:
Go to Settings -> MCP Servers -> Toggle the affected MCP server off and on. Click the Restart button
After restarting, MCP calls work again in the chat, but only until the access token expires again.
Impact
This breaks long-lived chats that rely on OAuth-authenticated MCP servers. Users must either start a new chat or manually restart the server after token expiry.
I presume Codex v0.128.0 no longer has this issue.
I was able to login to the MCP server (todoist), continued the same codex session but with a 3-4 hour inactivity (the access token typically expires in 1 hour), and also started a fresh session the next day. All cases that never worked earlier worked this time.
i'm on v0.128.0, and it still has this issue
I’m seeing what appears to be the same issue in Codex Desktop on Windows, with server-side evidence from an OAuth-enabled MCP server.
Related: #14144
Summary
Codex Desktop sessions appear to retain per-session MCP OAuth refresh-token state. When multiple Codex sessions use the same OAuth MCP server, one session may rotate the refresh token chain forward while an older session later resumes and presents a refresh token from several generations back.
The older session then fails all MCP operations with generic errors until the MCP server is disabled/re-enabled or the session/app is restarted.
Environment
Server-side Evidence
The MCP server logs short SHA-256 fingerprints of refresh tokens, not token material.
Successful refresh chain for the same Codex MCP client ID:
Then an older/stale Codex session attempted MCP calls:
That token had been valid at 19:27 but was already multiple generations behind by 19:55. Because it was not the immediately previous token, the server’s replay window could not recover it.
The stale session then repeatedly failed reads/searches/writes with
invalid_grant, while another Codex session in the same desktop app instance could still read/write successfully.Why This Looks Codex-Side
Expected Behavior
When one Codex session refreshes an OAuth MCP token, other sessions using the same MCP server should either:
invalid_grantby reloading the current credential state / prompting reauth.Actual Behavior
Older sessions continue to present stale refresh tokens and surface generic MCP failures.
Suggested Fix Area
Codex Desktop’s MCP OAuth runtime likely needs shared credential synchronization or cache invalidation across sessions/chats for the same MCP server. Refresh-token rotation makes stale per-session credential copies invalid by design.
Adding another datapoint that matches this issue, with Mixpanel's hosted MCP server.
Environment:
Codex CLI: codex-cli 0.130.0
Platform: macOS 26.3.1 (a), Darwin 25.3.0 arm64
MCP server: mixpanel, streamable_http, https://mcp.mixpanel.com/mcp
Credential store: macOS Keychain item Codex MCP Credentials, not a local
.mcp-authdirectoryObserved on 2026-05-12:
codex mcp login mixpanelhad previously succeeded.access_tokenandrefresh_token.expires_in: 86400; localexpires_atindicated the access token should still have been valid.invalid_tokenfrom Mixpanel. The error description said the bearer token was invalid, expired, or no longer recognized, and recommended clearing/reconnecting MCP client auth.refresh_tokenand returned a fresh access token plus a fresh refresh token.codex mcp login mixpanelagain restored the stored Keychain credential and MCP access temporarily.So in this case the provider-side refresh path appears healthy: the refresh token was present and usable, and the refreshed access token worked. The failure mode is that Codex continued with an invalid/stale MCP access token instead of using the stored refresh token and updating the persisted rotated credential. This also looks sensitive to refresh-token rotation, since the refresh response includes a new refresh token.
Hitting this exactly with our self-hosted MCP server (JetOctopus). Adding a concrete data point with log evidence.
Codex Desktop: 26.519.22136 (Windows)
MCP server: OAuth 2.1 + PKCE, JWT access tokens with 1h TTL, refresh tokens stored client-side
Symptom: After the 1h JWT expires, every subsequent tool call within the same Codex chat returns 401. Codex never uses the stored refresh token to re-authenticate transparently. Confirmed via server logs that Codex retries with the same cached expired JWT for hours:
Both batches were parallel fan-outs from a single user prompt. Identical token fingerprint across the two batches, ~47 minutes apart, proves the cached credential is never refreshed.
Our server returns RFC 6750-compliant signals on expiry:
Per RFC 6750 §3.1, the error="invalid_token" parameter combined with the WWW-Authenticate header is the standard signal for OAuth 2.0 clients to refresh and retry. Codex ignores it.
Workaround that works: Starting a new chat within the same Codex Desktop window triggers a fresh MCP session initialize, which picks up a freshly-refreshed JWT. The chat session is the cache boundary.
Suggested fix: Either (a) check exp claim of cached JWT before each tool call and refresh proactively when <5 min remaining, or (b) honor RFC 6750 WWW-Authenticate: Bearer error="invalid_token" by refreshing via the stored refresh_token and transparently retrying the failed request.
Cross-referencing past work: PR openai/codex#6209 attempted this fix in Nov 2025 and was redirected to upstream modelcontextprotocol/rust-sdk#509, which merged. Codex PR #6574 also landed the rmcp-transport variant in Nov 2025. The symptom in #17265 (filed Apr 9 2026, ~5 months later) and what I'm reporting here on 26.519.22136 strongly suggests a regression — possibly introduced by the OAuth routing refactors in PRs #19064 / #19797 / #20265.
I see the same issue. This effectively makes OAuth-based MCPs unusable. More details below:
Notion MCP OAuth refresh fails in Codex even though a refresh token is stored
Environment:
url = "https://mcp.notion.com/mcp"
oauth_resource = "https://mcp.notion.com"
Observed:
codex mcp login notion, Notion tools are available.~/.codex/.credentials.jsoncontains a Notion entry with both access_token and refresh_token.codex mcp liststill shows Notion configured and enabled with OAuth.Auth required, when send initialize requestExpected:
Codex should use the stored refresh token to refresh the Notion MCP access token automatically before/during MCP initialization, without requiring daily
browser reauth.
Actual:
Codex behaves as logged out after the one-hour access token expires, despite a stored refresh token.
Impact:
This blocks normal work because Notion MCP must be manually reauthenticated every day.
Additional possible related issue:
When setting
oauth_resource = "https://mcp.notion.com",codex mcp login notiongenerated an OAuth authorize URL containing both:resource=https://mcp.notion.com/mcpresource=https://mcp.notion.comThat may be related to resource/audience handling for Notion MCP.
Adding another sanitized confirmation with a controlled proof on current Codex.
Environment:
0.137.0-alpha.4Codex MCP CredentialsObserved on two separate Macs:
codex mcp login <server>succeeds and aCodex MCP CredentialsKeychain item exists.access_tokenandrefresh_tokenplus anexpires_attimestamp.codex execfails during MCP startup/tool discovery with:Codex MCP CredentialsKeychain item makes a freshcodex execimmediately succeed and expose/call the MCP tool.Controlled proof sequence:
Conclusion:
Codex can read and use the Keychain credential when the stored access token is fresh. The refresh token is valid and accepted by the MCP server. The failure appears to be that Codex does not refresh and persist the OAuth token response before MCP startup/tool discovery, nor recover by refreshing after the
401 invalid_tokenchallenge.Expected behavior:
expires_atto refresh beforeinitializewhen expired or near expiry.401 invalid_token, attempt refresh with the stored refresh token, persist the new token response, and retry MCP initialization/tool discovery.codex mcp listshould not show the server asAuth = Unsupportedwhen OAuth metadata and stored OAuth credentials are present; one affected Mac showedUnsupportedwhile another showedOAuthon the same CLI version.This is distinct from refresh-token rotation/server rejection: direct refresh returns HTTP 200, and manual Keychain replacement is sufficient to make Codex work.
adding another data point here since this is basically what i just reproduced on current stable codex cli (
0.137.0).sanitised setup:
Codex MCP Credentialsaccess_token, a presentrefresh_token,expires_in: 3600, andexpires_atin the pastwhat i verified:
initializewith theJsonRpcMessagedeserialise errorHTTP 200) and returns a fresh access tokeninitializeendpoint with that fresh access token succeeds (HTTP 200, valid JSON-RPC initialise result)so this is not a provider-side refresh-token expiry/revocation. the refresh token is valid, refresh works, and a fresh access token immediately fixes MCP initialisation.
the issue seems to be that codex is trying MCP startup/tool discovery with the expired access token instead of refreshing first. the deserialise error is secondary: codex is trying to parse the provider's auth failure response as JSON-RPC.
expected behaviour:
expires_atbefore MCP startup/tool discovery and refresh if expired / near expiryinitialize, try the stored refresh token, persist the new token response, and retryI have similar issue
I have this same issue with the Honeycomb MCP server and the token expiring after one hour. I'm a Codex.app user on Mac, and my workflow is:
codex mcp login honeycombfrom the terminalI do this multiple times per day.
I suspect I had the same issue with the Cloudflare MCP server months ago and didn't know what the problem was (Codex.app doesn't clearly surface an OAuth error and how to re-authenticate). I ended up creating an API key to use as a bearer token env var; I've done the same for Honeycomb as a workaround.
FWIW, I've checked with coworkers that use Claude Code, and they don't have to do this manual OAuth re-authentication dance.
We are seeing the same behavior.
Codex has a stored refresh token for an MCP server, but after the access token expires, subsequent MCP tool calls fail with an auth error instead of automatically refreshing within the refresh-token validity window. Running the CLI login flow manually restores access.
Expected behavior matches the report: if a valid refresh token is present, Codex should refresh the access token automatically and continue the MCP call without requiring manual browser reauthentication.
This issue is extra annoying when using Codex CLI on headless machines because you need to do the OAuth login in a browser. You can make it work (e.g. SSH port forwarding) but it's a hassle.
Its not making sense why this is not getting more attention from OpenAI. Its a massive issue when using tools with Codex. I routinely have to restart my app to force token refresh. Can someone please look into this? My workflow will thank you.
Same issue I'm seeing on my end, too.
https://github.com/openai/codex/pull/30089
I think this stack is trying to work on it?
Adding a sanitized Linux datapoint from Codex CLI
0.144.3. This looks related to the credential-store selection side of the OAuth problem, rather than provider rejection or access-token expiry.Environment:
0.144.3https://mcp.linear.app/mcp)~/.codex/.credentials.jsonObserved:
codex mcp login linearcompleted successfully multiple times.expires_atvalue.initializerequest using that exact stored access token returned HTTP 200, proving the token and provider session were valid.codex execstill failed during MCP startup with:``
text
``Auth error: OAuth authorization required, when send initialize request
[mcp_servers.linear]entry. The failure persisted, so duplicate registration was not the root cause.``
text
``-c 'mcp_oauth_credentials_store="file"'
~/.codex/config.tomlfixed subsequent fresh sessions without another OAuth login:``
toml
``mcp_oauth_credentials_store = "file"
This indicates that login wrote a usable file credential, while the default/Auto runtime path selected or read a different credential backend. It may be relevant to the replacement OAuth stack, especially #30293 and the still-open Auto-store drift diagnostics in #30296. No token values, workspace identifiers, or private data are included here.
We’ve landed the first set of fixes for concurrent MCP OAuth refreshes. Codex now serializes refreshes per MCP credential, rereads the latest stored credential after acquiring the lock, and prevents concurrent read-modify-write operations from overwriting newly refreshed tokens.
The relevant changes are:
Minimum versions containing the fixes:
26.715.21316or newer, first released July 16, 2026 at approximately 23:27 UTC. Versions starting with26.707or older do not contain the complete fix.0.145.0-alpha.3or newer, first released July 10, 2026 at approximately 23:11 UTC. Alpha builds are publicly available.0.144.6release does not contain the complete fix. The fixes are expected in the upcoming0.145stable release.If an earlier refresh race already invalidated the provider’s refresh-token family, updating cannot recover that credential, so logging into the affected MCP once more may still be necessary.
Please let us know how MCP authentication behaves after updating, including whether the issue is resolved or repeated reauthentication still occurs. If problems continue, please share whether you’re using Desktop or CLI, the version, and the affected MCP provider.
These changes should address the primary token-clobbering failure reported here. We’re continuing to work through remaining edge cases involving RMCP-initiated/background refresh and 401 recovery.
OAuth related issues are getting worse, not better. Latest Codex can't even reliably detect and initiate OAuth for MCP with known good configs. It's incredibly frustrating.
What are you seeing that's not working? Could you provide more details?
MCP Apps/Plugins with OAuth that work flawlessly in the web client only work sporadically and sometimes not at all in Codex. The Plugins > MCP config screen in Codex will sporadically or not at all show the 'authenticate' button to start the OAuth flow. Codex itself, if prompted to investigate the issue, will itself admit that it has problems.
And this is on top of the issue i originally logged here where refresh tokens are not reliable.
Basically, you need parity in Codex with what you have built in the web client OAuth 1) Reliable OAuth trigger 2) Dynamic OAuth trigger when tokens expire 3) Reliable token refresh
We are seeing three issues ( Codex cli 0.145.0 ) and Entra OAuth, the below is from a debugging codex session around the issue with details. Following our workaround we can login but the auto refresh doesn't succeed later and the first we know about it is when tool calls to the oauthed mcp server start failing and then we have to repeat the whole login flow again.
## Initial registration problem and subsequent refresh
There are two related problems during the first registration of an OAuth-protected MCP
server and the same subsequent refresh which may be related
### Environment
xxThe MCP server publishes RFC 9728 protected-resource metadata containing:
resource:xxxauthorization_servers: the tenant-specific Entra v2 endpointscopes_supported:xxxaccess_as_user### 1. The automatic add-time OAuth flow omits the advertised API scope
The server is registered with:
``
powershellcodex mcp add {name}
--url https://{domain}/mcp
--oauth-client-id CLIENT_ID
-c 'mcp_oauth_callback_port=53682' `
-c 'mcp_oauth_callback_url="http://127.0.0.1:53682/callback"'
Codex successfully detects OAuth support and immediately starts an authorization flow.
However, its authorization request contains only:
scope=openid profile email offline_access
resource=https://{domain}/mcp
It does not include the scope advertised by the MCP server:
https://{domain}/mcp/access_as_user
Microsoft Entra consequently rejects the token exchange with:
invalid_target: AADSTS9010010: The resource parameter provided in the request
doesn't match with the requested scopes.
The MCP entry has already been saved, but the automatic login fails. A second, explicit
login is required:
codex mcp login {name}
--scopes https://{domain}/mcp/access_as_user
-c 'mcp_oauth_callback_port=53682' `
-c 'mcp_oauth_callback_url="http://127.0.0.1:53682/callback"'
That flow succeeds and produces a token with the correct API audience and delegated scope.
Expected behavior: the automatic OAuth flow started by codex mcp add should use the
server’s advertised scopes_supported value, just as the explicit mcp login --scopes flow
does.
### 2. The default dynamic callback URI is unsuitable for Entra registration
Without callback overrides, Codex chooses a dynamic loopback redirect similar to:
http://127.0.0.1:55718/callback/KcdHHZBqATU8
Entra requires the redirect URI to match one registered on the public-client application.
The changing port, combined with Codex’s server-specific callback path, results in:
AADSTS50011: The redirect URI specified in the request does not match the
redirect URIs configured for the application.
We worked around this by fixing the callback port and base URL:
mcp_oauth_callback_port=53682
mcp_oauth_callback_url=http://127.0.0.1:53682/callback
and registering Codex’s full derived callback URI in Entra:
http://127.0.0.1:53682/callback/KcdHHZBqATU8
The callback issue is solvable through configuration, but it makes the initial registration
more complicated. The missing advertised scope is the reason a separate second login step
remains necessary.
The key distinction is:
required API token.
They are independent problems encountered during the same onboarding flow.
OAuth refresh failure after successful registration
After completing the explicit scoped login, the MCP server works correctly for a limited
period. Sessions can be created, messages sent, files uploaded, and tools called
successfully.
After the Entra access token expires, later MCP calls fail instead of being refreshed
automatically.
### Observed error
A later tool call failed with:
``
text{name}/create_session`tool call error: tool call failed for
Caused by:
0: OAuth tokens for server {name} cannot be refreshed; authorization required
1: OAuth authorization required
The error occurs before the requested MCP tool reaches the server.
At the time of failure:
The recovery command was:
codex mcp login {name}
--scopes https://{domain}/mcp/access_as_user
-c 'mcp_oauth_callback_port=53682' `
-c 'mcp_oauth_callback_url="http://127.0.0.1:53682/callback"'
After browser authorization completed, MCP tool calls worked again.
### Relevant authorization request
The successful interactive login requests:
openid
profile
email
offline_access
https:/{domain}/mcp/access_as_user
Because offline_access is requested, the resulting OAuth session is expected to support
silent renewal. Microsoft Entra access tokens normally have relatively short lifetimes, but
the user should not need another interactive browser login whenever an access token
expires.
We did not inspect or expose the locally stored OAuth credentials, so we cannot confirm
whether:
The client-facing symptom is only that Codex reports that the OAuth tokens cannot be
refreshed and requires interactive authorization again.
### Expected behavior
Before an access token expires—or after receiving a bearer-token authentication failure—
Codex should:
requires user interaction.
### Actual behavior
After the access token expires:
### Reproduction
### Impact
This prevents reliable use of OAuth-protected MCP servers in:
The MCP server itself cannot safely compensate for this. Extending Entra access-token
lifetime only delays the failure and increases the security impact of a stolen bearer
token; it does not repair refresh-token handling.
### Possibly related Codex issues
refresh token is stored.
OAuth state.
The strongest evidence is that manual
mcp loginrestores access immediately withoutchanging {name}, its Entra registration, discovery metadata, or server configuration. That
places the failure in the client-side OAuth credential/refresh lifecycle.
Adding a concrete Codex CLI datapoint for the remaining RMCP-initiated/background refresh edge case mentioned by @stevenlee-oai above.
Environment
origin/mainat95637f7056835fea66bdd0044414af480fc0fd74No token values, OAuth client secrets, or private endpoints are included below.
Observed timeline
All times are from the same machine on 2026-07-27:
57028, started at approximately 08:53, loaded those credentials and established the MCP connection.``
text
``Access token expired or nearly expired, refreshing.
refresh token present, attempting refresh
Refreshed access token.
86818, started at approximately 10:16 and loaded the stale durable credential. At 10:16:15 its refresh attempt failed with:``
text
``invalid_grant: The refresh token was already used.
This was not a general provider, network, VPN, or credential-file failure: other OAuth MCP credentials refreshed and persisted successfully during the same startup. The stale token replay can be explained by the successful background refresh above.
Source-level cause
The recent serialized-refresh work protects refreshes that pass through Codex's
OAuthPersistor: it acquires the credential lock, rereads the authoritative stored value, refreshes it, and persists the rotated credentials.The Streamable HTTP transport, however, is still backed by RMCP's
AuthClient. During an SSEget_streamreconnect, RMCP can call its ownget_access_tokenand refresh internally. That autonomous refresh does not pass through the Codex refresh transaction. Codex's persistence polling runs after explicit MCP operations, but the background SSE reconnect is not one of those operations.The resulting state is:
After a restart, or when another process reads the durable store, Codex presents
R0again and the provider correctly rejects it as already used.This is related to the cross-process race, but is a narrower bypass: the serialized refresh transaction cannot protect or persist a refresh that happens entirely inside RMCP's background transport path.
Expected invariant
Every access-token lookup/refresh used by Streamable HTTP — including
post_message, session deletion, and SSEget_stream/reconnect — should use the same serialized Codex credential transaction. A rotated refresh token should be durably persisted before the refreshed access token is exposed to the HTTP transport.Local fix and regression test
I validated a small wrapper around the RMCP HTTP client that routes all three transport operations through
OAuthPersistor::refreshed_access_token, preventing RMCP from performing an unobserved internal refresh.The regression test starts with an expired access token, calls the SSE
get_streampath, has the mock provider return a new access token plus a rotated refresh token, and asserts that the rotated refresh token is persisted before the HTTP request proceeds.Validation results:
I can provide the local patch/test if that would help. The key distinction from the already-landed concurrency fixes is that this refresh is initiated by RMCP's background SSE transport, not by Codex's guarded refresh path.
I reproduced and fixed the remaining RMCP-initiated/background refresh persistence bypass on Linux x86_64, rebased on current
origin/main(3418498f).Fresh validation:
stalevs expectedpersisted).codex-rmcp-client: 120/120 tests passed (6 helper tests skipped).cargo check -p codex-rmcp-clientandcargo build -p codex-cli: passed.initializeandtools/listsucceeded; 40 tools were returned. No production tool mutation was invoked.The patch routes
post_message,delete_session, and backgroundget_streamtoken lookup through Codex's serializedOAuthPersistortransaction and always supplies RMCP an explicit current token, so RMCP cannot rotate credentials only in memory.Branch/commit: https://github.com/2sang-min/codex/commit/c060d7dd5f
I have not opened a PR because the contribution guide says external PRs are by invitation. If a maintainer would like this submitted, I am happy to open the PR or adjust the patch to the preferred shape.
Status update — partially fixed, background SSE bypass still open
Rechecked against stable
0.146.0,0.146.0-alpha.16, and currentmain@9a6668f67on July 29, 2026.What is now fixed
Stable
0.146.0contains the serialized Codex-owned refresh transaction:This addresses the primary concurrent-refresh/token-clobbering path described earlier by the maintainers.
Newer alphas add two related improvements:
alpha.15+) distinguishes definitive refresh-token rejection from transient token endpoint/network failures.alpha.15+) routes discovery, login, and refresh through the configured HTTP client, including enterprise headers/proxy routing.What remains open
The specific background Streamable HTTP/SSE persistence bypass documented in my previous comment is not yet fixed upstream.
Current
mainstill constructs the OAuth transport around RMCP'sAuthClient. Codex callsOAuthPersistor::refresh_if_needed()before explicit MCP operations and polls/persists afterward, but a backgroundget_streamreconnect is not an explicit operation boundary. RMCP can therefore refresh autonomously in its internal credential state without first committing a rotated refresh token through Codex's serialized transaction.There is still no upstream equivalent of the locally validated wrapper that:
OAuthPersistorforpost_message,delete_session, andget_stream;401while localexpires_atremains valid.So the current status is:
0.146.0.alpha.15+.401recovery: still open.If an older build already caused a provider to invalidate the stored refresh-token generation, updating cannot recover that credential; one new login may still be required. After that, the shipped transaction protects Codex-owned refreshes, while the background/SSE edge case above still needs upstream coverage.
In addendum to my earlier comment around the initial retrieval of a token and the mcp still not leveraging advertised scopes in the oauth call in codex-cli 0.146.0 and the refresh still fails to action after a token expires.
Retested with codex-cli 0.146.0 on Windows.
codex mcp addsaved the Streamable HTTP server but its automatic OAuth flowfailed with:
AADSTS900144: The request body must contain the following parameter: 'scope'.
Afterward,
codex mcp listshowed:{name} https://{domain}mcp enabled Not logged in
codex mcp add --helpexposes no scope option.The MCP protected-resource metadata correctly advertises:
https://{domain}/mcp/access_as_user
Therefore, a separate
codex mcp login {name} --scopes ...step remains requiredin codex-cli 0.146.0.
Regarding the refresh with codex-cli 0.146.0 also failed.
A clean explicit scoped login initially allowed authenticated MCP calls. After the Entra access token expired, repeating the same read-only tool call produced:
tool call error: tool call failed for
{name}/list_sessionsCaused by:
0: OAuth tokens for server {name} cannot be refreshed; authorization required
1: OAuth authorization required
At the time of failure, the MCP service was healthy and its RFC 9728 metadata still advertised:
resource:
https://{name}/mcp
scopes_supported:
https://{name}/mcp/access_as_user
The failure occurred before
list_sessionsreached the MCP server. No session was created or modified.Therefore codex-cli 0.146.0 still does not transparently recover after this OAuth access token expires.
Hi, I’d like to work on the remaining OAuth refresh issue here, particularly the background Streamable HTTP/SSE refresh path and persistence of rotated refresh tokens.
I’ve read through the recent investigation and understand that the Codex-owned serialized refresh path is already fixed, while the RMCP-initiated background refresh persistence bypass remains open.
I’d like to reproduce the remaining issue locally, investigate the relevant OAuth/RMCP transport code, add a regression test, and propose a fix if the issue is still reproducible on current
main.Please let me know if this is still an appropriate area for an external contributor to work on.
Adding a current macOS Desktop datapoint that appears to isolate the remaining failure to background Streamable HTTP refresh persistence.
Environment
26.810.520440.148.0-alpha.9mcp_oauth_credentials_store = "file"Observed sequence
All times UTC:
2026-08-15T23:46:55Z: Desktop completed authorization and received an access token plus refresh token. The credential file was written at23:46:58Z, remained mode0600, and contained both tokens.2026-08-16T00:46:55Z: the access token expired.2026-08-16T00:53:59Z: provider logs showcodex-mcp-client/0.148.0-alpha.9successfully sentPOST /tokenusing the refresh grant.23:46:58Zand still contains the now-consumed predecessor.Authentication expiredand requested reconnection.No endpoint, client identifier, IP address, token value, or token fingerprint is included here.
Control
Earlier on the same bundled CLI version, a fresh CLI process with the stored access-token expiry deliberately forced into the past completed a refresh and persisted the rotated successor to the same file. The file's refresh-token hash and expiry changed, mode remained
0600, and the matching provider-side successor was active.That contrast suggests the Codex-owned explicit/proactive CLI path can persist the rotation, while a refresh initiated by the Desktop/background Streamable HTTP transport can successfully rotate provider state in memory without committing the successor to the configured durable store.
This matches the remaining background transport persistence gap described here and in #35006. The provider, OAuth discovery, token endpoint, and MCP runtime were healthy throughout; relaxing refresh-token replay protection would only hide the client-side loss and introduce a token-branching race.
Expected behavior: any transport-initiated refresh should use the same serialized read-refresh-write transaction, and the rotated successor should be durably committed before it is exposed to the transport.
Related information for those using AWS CloudFront
While the MCP application correctly returned a 401 oauth_required response with the proper protected-resource URL and authorization server, CloudFront exposed the challenge as x-amzn-Remapped-www-authenticate instead of the standard WWW-Authenticate header. ChatGPT appeared tolerant because it could infer the OAuth flow from the structured response and discovery metadata, while Codex seems to require an exact standard header before launching native OAuth.
The fix was a versioned CloudFront edge response handler that restores the header as WWW-Authenticate, and preserves the original status, body, and OAuth metadata.
Follow-up to my earlier Desktop/background persistence datapoint: this is now blocking an organization-wide Codex plugin rollout, and it reproduces across three independently registered OAuth-backed MCP servers rather than one provider integration.
Current environment
26.820.609400.150.0-alpha.8mcp_oauth_credentials_store = "file"The current stable
0.150.1changelog contains no MCP OAuth change, so I did not treat that patch as a candidate fix.2026-08-27 reproduction
2026-08-26T22:24:16Z.``
text
``OAuth refresh token was rejected: Server returned error response:
invalid_grant: Refresh token is invalid or has already been used
2026-08-26T21:24:18Z, before those access tokens expired. Its mode remained0600; the stored expiry metadata was unchanged after the refresh attempts.codex mcp list --jsonstill reported all three plugin MCPs as enabled with OAuth, while startup omitted their tools because no ready authenticated client could be created.This is the same failure class as the earlier single-server datapoint, but the three-client reproduction rules out a provider-specific MCP runtime problem and shows the impact on marketplace/plugin distribution.
Impact
We cannot promote the plugin from an opt-in pilot to default organization installation. Requiring every employee to reauthorize three connectors after each access-token expiry is not a viable enterprise workflow. Disabling refresh-token rotation, accepting replayed predecessors, or substantially extending access-token lifetime would only conceal the client persistence defect by weakening the authorization server.
Another supported client continues to refresh against the same authorization server semantics successfully, so changing the shared server would also create cross-client security and compatibility risk.
Could an owner please confirm whether the routed/plugin/Desktop refresh path is expected to use the serialized read-refresh-write transaction from #32229, and whether this issue can receive an assignee or milestone? A useful regression case would install multiple OAuth MCPs from one plugin, expire their access tokens, and verify that a fresh process adopts the durably persisted rotated successors without manual login.
No endpoint, organization name, client identifier, cloud account, IP address, token value, or token fingerprint is included here.
@stevenlee-oai — tagging you specifically because you authored the shared MCP credential-store fixes in #30292/#30293 and asked for post-fix reports with the affected surface and version.
Today's three-server reproduction above is on Desktop
26.820.60940/ bundled client0.150.0-alpha.8, well beyond the minimum fixed versions you identified. It appears to be the remaining RMCP-initiated/background refresh persistence edge case you called out, now reproduced across multiple MCPs projected from one plugin rather than one provider.Could you help route/assign this issue or indicate the next diagnostic needed from us? It is blocking a secure organization-wide rollout, and we do not want to hide the client failure by weakening refresh-token rotation or replay protection.