You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Codex does not auto-refresh routed MCP OAuth tokens even when a refresh token is stored #17265
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.
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_
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.
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.
Add an MCP server authenticated via OAuth to Codex.
Complete the OAuth flow to obtain a valid access token and refresh token.
In the Codex desktop app, use the MCP server successfully in a chat.
Wait until the access token expires.
In the same chat, send another request that uses the same MCP server.
Actual result
The MCP server responds with error: Auth required.
The desktop app appears to continue using the expired access token.
Retrying in the same chat does not recover authentication.
Starting a new chat makes the MCP server work again.
This suggests the existing chat session does not trigger token refresh or pick up refreshed auth state.
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.
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
Product: Codex Desktop on Windows 26.429.61741
MCP auth: OAuth / Dynamic Client Registration
Access token lifetime: 10 minutes
Refresh token lifetime: 30 days
Server behavior: refresh tokens rotate on use
Server mitigation already tried: a 5-minute idempotent replay marker for the immediately previous refresh token
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:
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
The MCP server is healthy and accepts current refresh tokens.
A different Codex session in the same app instance can continue to use the MCP server successfully.
The failing session presents an older refresh token generation instead of the latest one.
Starting/switching sessions can temporarily restore functionality, but older sessions later fail after the access token's expiry.
When one Codex session refreshes an OAuth MCP token, other sessions using the same MCP server should either:
read the latest app-level credential state before attempting refresh, or
invalidate/reload their per-session MCP auth cache, or
recover cleanly after invalid_grant by 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-auth directory
Observed on 2026-05-12:
codex mcp login mixpanel had previously succeeded.
The stored MCP OAuth payload contained both access_token and refresh_token.
The stored token response had expires_in: 86400; local expires_at indicated the access token should still have been valid.
A direct MCP initialize request using the stored access token returned 401 invalid_token from Mixpanel. The error description said the bearer token was invalid, expired, or no longer recognized, and recommended clearing/reconnecting MCP client auth.
A direct OAuth refresh-token request to Mixpanel succeeded using the stored refresh_token and returned a fresh access token plus a fresh refresh token.
The freshly refreshed access token then succeeded against the same Mixpanel MCP initialize request with 200 OK.
Running codex mcp login mixpanel again 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.
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:
22:28:00 (×9 simultaneous calls) tok_fp=25a4fabf... (token already expired)
23:15:31 (×10 simultaneous calls) tok_fp=25a4fabf... (47 min later, SAME token)
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:
HTTP/2 401
WWW-Authenticate: Bearer realm="mcp", error="invalid_token",
error_description="The access token expired",
resource_metadata="https://my.jetoctopus.com/.well-known/oauth-protected-resource"
{"jsonrpc":"2.0","error":{"code":-32001,
"message":"Access token expired. Refresh via /oauth/token using your refresh_token and retry.",
"data":{"reason":"token_expired",
"remediation":"refresh_via_oauth_token",
"refresh_endpoint":"https://my.jetoctopus.com/oauth/token"}},
"id":null}
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.
After codex mcp login notion, Notion tools are available.
~/.codex/.credentials.json contains a Notion entry with both access_token and refresh_token.
The Notion access token expired at 2026-06-05 01:03:15 PDT.
The credentials file was last modified at 2026-06-05 00:03:15 PDT, so Codex did not refresh it after expiry.
After expiry, Notion tools are no longer exposed/discoverable.
codex mcp list still shows Notion configured and enabled with OAuth.
Logs show Codex waiting for Notion tools after expiry, but not listing them; nearby MCP auth failure includes: Auth required, when send initialize request
Expected:
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 notion generated an OAuth authorize URL containing both:
resource=https://mcp.notion.com/mcp
resource=https://mcp.notion.com
That may be related to resource/audience handling for Notion MCP.
I reproduced and fixed the remaining RMCP-initiated/background refresh persistence bypass on Linux x86_64, rebased on current origin/main (3418498f).
Fresh validation:
Regression test on unmodified main: refreshed access token was used while the durable rotating refresh token was still stale (stale vs expected persisted).
Same test with the patch: passes and verifies the rotated refresh token is durable before the background SSE reconnect sends its request.
cargo check -p codex-rmcp-client and cargo build -p codex-cli: passed.
Isolated real Atlassian OAuth login with the patched binary: initialize and tools/list succeeded; 40 tools were returned. No production tool mutation was invoked.
The patch routes post_message, delete_session, and background get_stream token lookup through Codex's serialized OAuthPersistor transaction and always supplies RMCP an explicit current token, so RMCP cannot rotate credentials only in memory.
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.
The specific background Streamable HTTP/SSE persistence bypass documented in my previous comment is not yet fixed upstream.
Current main still constructs the OAuth transport around RMCP's AuthClient. Codex calls OAuthPersistor::refresh_if_needed() before explicit MCP operations and polls/persists afterward, but a background get_stream reconnect 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:
obtains the access token through OAuthPersistor for post_message, delete_session, and get_stream;
durably persists rotation before exposing the refreshed access token to the transport;
forces one serialized refresh and rebuild/retry after an unexpected 401 while local expires_at remains valid.
So the current status is:
Codex-owned expiry refresh and cross-process serialization: fixed and shipped in stable 0.146.0.
Definitive-versus-transient refresh failure classification: fixed in alpha.15+.
Background RMCP/SSE refresh persistence: still open.
One-shot unexpected-401 recovery: 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 add saved the Streamable HTTP server but its automatic OAuth flow
failed with:
AADSTS900144: The request body must contain the following parameter: 'scope'.
Afterward, codex mcp list showed:
{name} https://{domain}mcp enabled Not logged in
codex mcp add --help exposes 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 required
in 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_sessions
Caused 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:
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.
2026-08-15T23:46:55Z: Desktop completed authorization and received an access token plus refresh token. The credential file was written at 23:46:58Z, remained mode 0600, and contained both tokens.
2026-08-16T00:46:55Z: the access token expired.
2026-08-16T00:53:59Z: provider logs show codex-mcp-client/0.148.0-alpha.9 successfully sent POST /token using the refresh grant.
The provider atomically marked the presented refresh token consumed and issued an active rotated successor. The successor remains active server-side.
The credential file's modification time did not change after 23:46:58Z and still contains the now-consumed predecessor.
A later Desktop session surfaced Authentication expired and 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.
Three Streamable HTTP MCP servers packaged in one private Codex marketplace plugin
Separate public PKCE client registration for each server
Access-token lifetime: 1 hour
Refresh-token lifetime: 30 days, rotating on use
The current stable 0.150.1 changelog contains no MCP OAuth change, so I did not treat that patch as a candidate fix.
2026-08-27 reproduction
All three connectors had previously completed authorization, registered as enabled/OAuth, and passed live read-only tool calls.
Their stored access tokens expired at approximately 2026-08-26T22:24:16Z.
More than four hours later, a fresh ephemeral Codex process initialized the plugin and attempted one harmless read from each server.
All three refresh transactions failed independently with the same result:
OAuth refresh token was rejected: Server returned error response:
invalid_grant: Refresh token is invalid or has already been used
The durable credential file was last modified at 2026-08-26T21:24:18Z, before those access tokens expired. Its mode remained 0600; the stored expiry metadata was unchanged after the refresh attempts.
codex mcp list --json still 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 client 0.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.
One additional concurrency datapoint on Desktop 26.825.51511 / bundled Codex 0.151.0-alpha.7.2 (macOS 15.7.9):
With a synthetic near-expiry OAuth-authenticated Streamable HTTP MCP credential, a controlled VPN disconnect/reconnect produced two successful refreshes in the same logged second. Both used predecessor A but returned distinct successors B and C. The provider permits reuse of a consumed refresh token for approximately 10 minutes, allowing both requests to succeed.
With a strict rotating-token provider that immediately invalidates A, these concurrent requests would race: one would receive the successor while the other would fail with invalid_grant. If the winning manager were discarded before persistence, the successor would be lost immediately.
Durable storage nevertheless retained A. After the provider’s grace period, restarting Desktop reused A and received invalid_grant.
Each loaded chat owns an independent MCP runtime and OAuth manager. Current logs do not retain the originating thread, so we cannot determine whether these requests came from two loaded chats or overlapping transports within one chat. Either way, multiple managers independently refreshed the same durable credential without persisting their successors.
After invalid_grant, Desktop displayed no Authenticate/Reauthenticate button or prompt. Recovery required deleting the credential outside the app and restarting it.
I posted a current Windows repro with correlated Codex and server logs: #27165 (comment).
In this trace, Codex sent the expired bearer at the first one-hour access-token expiry. It entered refresh handling but made no request to the provider’s token endpoint.
Recent reports here cover a path where the provider processes a refresh, but Codex does not save the rotated credential. The two cases may share a problem in how the active MCP runtime loads or retains refresh credentials. Hoping the additional trace helps narrow that down.
can't believe OAuth MCPs in codex are in the state they are... cmon now all other CLI harnesses support it... it's dumbfounding how backwards and late the codex CLI is... oh well
MCP worked initially, but required interactive reauthentication twice that day:
MCP startup failed: failed to refresh OAuth tokens:
invalid_grant: refresh token does not exist
Server logs show successful Codex refreshes at 11:22:42 and 12:23:28 UTC, followed by repeated requests presenting a missing refresh-token hash from 13:03 onward. Both failure waves started before our server deployment. The same server accepted refreshes for the Claude Code client ID and Claude's hosted MCP connector client ID. The hosted connector ID does not distinguish Desktop from the web app; these server logs do not establish subsequent client tool-call success.
Native CLI login restored tools in the existing task. We have not demonstrated which Codex process or persistence path retained the rejected token. The evidence appears consistent with the background-refresh persistence reports here.
What targeted diagnostics would help establish whether this is the same remaining defect?
Activity
github-actions commentedon Apr 9, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
infoseekAI commentedon Apr 10, 2026
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.
ankitvg commentedon Apr 20, 2026
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:
client_id/audmaps to ChatGPT client.client_id/audmaps correctly to Codex client.Expected:
Actual:
Cyanoth commentedon Apr 24, 2026
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.
technotablet commentedon May 4, 2026
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.
amiranvarov commentedon May 4, 2026
i'm on v0.128.0, and it still has this issue
dsghi commentedon May 5, 2026
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.
w00ing commentedon May 12, 2026
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.
toxaus commentedon May 21, 2026
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.
nimz commentedon Jun 5, 2026
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.
16 remaining items
2sang-min commentedon Jul 28, 2026
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: 2sang-min@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.
publi0 commentedon Jul 29, 2026
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.
kosmikza commentedon Jul 30, 2026
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.
Repo-qualify or retire every dangling agent-orchestrator issue ref in…
Point every predecessor-repo issue reference at the repo it actually …
Point every predecessor-repo issue reference in app/ at the repo it a…
sahilsaiyed-oss commentedon Aug 8, 2026
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.
blakehedlin-sibel commentedon Aug 17, 2026
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.
infoseekAI commentedon Aug 21, 2026
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.
blakehedlin-sibel commentedon Aug 27, 2026
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
All three connectors had previously completed authorization, registered as enabled/OAuth, and passed live read-only tool calls.
Their stored access tokens expired at approximately
2026-08-26T22:24:16Z.More than four hours later, a fresh ephemeral Codex process initialized the plugin and attempted one harmless read from each server.
All three refresh transactions failed independently with the same result:
The durable credential file was last modified at
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.
blakehedlin-sibel commentedon Aug 27, 2026
@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.
daniel-meismer-zocdoc commentedon Sep 1, 2026
One additional concurrency datapoint on Desktop
26.825.51511/ bundled Codex0.151.0-alpha.7.2(macOS 15.7.9):With a synthetic near-expiry OAuth-authenticated Streamable HTTP MCP credential, a controlled VPN disconnect/reconnect produced two successful refreshes in the same logged second. Both used predecessor
Abut returned distinct successorsBandC. The provider permits reuse of a consumed refresh token for approximately 10 minutes, allowing both requests to succeed.With a strict rotating-token provider that immediately invalidates
A, these concurrent requests would race: one would receive the successor while the other would fail withinvalid_grant. If the winning manager were discarded before persistence, the successor would be lost immediately.Durable storage nevertheless retained
A. After the provider’s grace period, restarting Desktop reusedAand receivedinvalid_grant.Each loaded chat owns an independent MCP runtime and OAuth manager. Current logs do not retain the originating thread, so we cannot determine whether these requests came from two loaded chats or overlapping transports within one chat. Either way, multiple managers independently refreshed the same durable credential without persisting their successors.
After
invalid_grant, Desktop displayed no Authenticate/Reauthenticate button or prompt. Recovery required deleting the credential outside the app and restarting it.- Codex 🧭🛠️
cassxw commentedon Sep 2, 2026
I posted a current Windows repro with correlated Codex and server logs: #27165 (comment).
In this trace, Codex sent the expired bearer at the first one-hour access-token expiry. It entered refresh handling but made no request to the provider’s token endpoint.
Recent reports here cover a path where the provider processes a refresh, but Codex does not save the rotated credential. The two cases may share a problem in how the active MCP runtime loads or retains refresh credentials. Hoping the additional trace helps narrow that down.
FxmousKid commentedon Sep 3, 2026
can't believe OAuth MCPs in codex are in the state they are... cmon now all other CLI harnesses support it... it's dumbfounding how backwards and late the codex CLI is... oh well
kim0 commentedon Sep 12, 2026
Additional occurrence on September 12, 2026:
MCP worked initially, but required interactive reauthentication twice that day:
Server logs show successful Codex refreshes at 11:22:42 and 12:23:28 UTC, followed by repeated requests presenting a missing refresh-token hash from 13:03 onward. Both failure waves started before our server deployment. The same server accepted refreshes for the Claude Code client ID and Claude's hosted MCP connector client ID. The hosted connector ID does not distinguish Desktop from the web app; these server logs do not establish subsequent client tool-call success.
Native CLI login restored tools in the existing task. We have not demonstrated which Codex process or persistence path retained the rejected token. The evidence appears consistent with the background-refresh persistence reports here.
What targeted diagnostics would help establish whether this is the same remaining defect?