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_

View original on GitHub ↗

34 Comments

github-actions[bot] contributor · 4 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #15754
  • #15502

Powered by Codex Action

infoseekAI · 4 months ago

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 · 4 months ago

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:

  • I connected the same MCP server (stewreads.com) from both ChatGPT app and Codex app.
  • Codex app creates its own OAuth client during setup.
  • But server logs show Codex app requests carrying token claims (client_id / aud) for the ChatGPT client.

Control test:

  • I ran Codex CLI inside the Codex app terminal against the same MCP server.
  • Codex CLI used the correct Codex client-issued token.
  • So this seems specific to Codex App’s built-in MCP runtime path, not Codex CLI.

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/aud maps to ChatGPT client.
5) Run Codex CLI against same server and compare: client_id/aud maps correctly to Codex client.

Expected:

  • Codex App should use only Codex client-issued tokens (app/client-scoped credential isolation).

Actual:

  • Codex App appears to reuse ChatGPT client token for the same MCP server identity.
Cyanoth · 4 months ago

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

  1. Add an MCP server authenticated via OAuth to Codex.
  2. Complete the OAuth flow to obtain a valid access token and refresh token.
  3. In the Codex desktop app, use the MCP server successfully in a chat.
  4. Wait until the access token expires.
  5. In the same chat, send another request that uses the same MCP server.

Actual result

  1. The MCP server responds with error: Auth required.
  2. The desktop app appears to continue using the expired access token.
  3. Retrying in the same chat does not recover authentication.
  4. Starting a new chat makes the MCP server work again.
  5. 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.

technotablet · 3 months ago

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 · 3 months ago

i'm on v0.128.0, and it still has this issue

dsghi · 3 months ago

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

  • 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:

19:18  a577bd5762f9 -> 2c038f3caf9e
19:27  2c038f3caf9e -> cf1e044b93dc
19:39  cf1e044b93dc -> 432dc8317a97
19:48  432dc8317a97 -> 70a8eba10384

Then an older/stale Codex session attempted MCP calls:

19:55  rejected reason=missing refreshTokenFingerprint=2c038f3caf9e

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.
  • The behavior matches the report in #14144: active sessions continue using stale refresh credentials.

Expected Behavior

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.

w00ing · 3 months ago

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.

toxaus · 3 months ago

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:

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.

nimz · 2 months ago

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:

  • Codex CLI: 0.137.0
  • Auth mode: ChatGPT
  • MCP server: notion -> https://mcp.notion.com/mcp
  • Config:
  • mcp_oauth_credentials_store = "file"
  • [mcp_servers.notion]

url = "https://mcp.notion.com/mcp"
oauth_resource = "https://mcp.notion.com"

Observed:

  • 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.

drkvj · 2 months ago

Adding another sanitized confirmation with a controlled proof on current Codex.

Environment:

  • Codex CLI: 0.137.0-alpha.4
  • Platform: macOS
  • Credential store: macOS Keychain, service Codex MCP Credentials
  • MCP server shape: private Streamable HTTP MCP server using OAuth 2.1 + PKCE, short-lived JWT access tokens, long-lived refresh tokens
  • Token values, server URL, user identifiers, and Keychain account IDs intentionally omitted

Observed on two separate Macs:

  1. codex mcp login <server> succeeds and a Codex MCP Credentials Keychain item exists.
  2. The stored credential contains both access_token and refresh_token plus an expires_at timestamp.
  3. Once the stored access token expires, fresh codex exec fails during MCP startup/tool discovery with:
AuthRequired(AuthRequiredError { www_authenticate_header: "Bearer realm=..., resource_metadata=..., scope=..., error=\"invalid_token\"" })
  1. In that failed state, the MCP tools are not exposed to the session.
  2. Directly calling the MCP server token endpoint with the stored refresh token and stored client ID returns HTTP 200 and a fresh access token. The server is not rejecting the refresh token.
  3. Manually writing that fresh token response back into the same Codex MCP Credentials Keychain item makes a fresh codex exec immediately succeed and expose/call the MCP tool.

Controlled proof sequence:

T0: Keychain contains expired access token and valid refresh token.
T1: Direct POST /token with grant_type=refresh_token returns 200 and fresh access_token.
T2: Fresh codex exec immediately after direct refresh, without updating Keychain, still fails with invalid_token.
T3: Manually update the existing Keychain item with the fresh token response.
T4: Fresh codex exec succeeds and calls the MCP tool.

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_token challenge.

Expected behavior:

  • On MCP startup, read stored credential and use expires_at to refresh before initialize when expired or near expiry.
  • On 401 invalid_token, attempt refresh with the stored refresh token, persist the new token response, and retry MCP initialization/tool discovery.
  • Keep tools exposed after successful refresh.
  • codex mcp list should not show the server as Auth = Unsupported when OAuth metadata and stored OAuth credentials are present; one affected Mac showed Unsupported while another showed OAuth on 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.

radugheorman · 2 months ago

adding another data point here since this is basically what i just reproduced on current stable codex cli (0.137.0).

sanitised setup:

  • remote streamable-http MCP server using OAuth
  • credentials stored in macOS keychain under Codex MCP Credentials
  • stored credential has an expired access_token, a present refresh_token, expires_in: 3600, and expires_at in the past

what i verified:

  1. fresh codex startup fails during MCP initialize with the JsonRpcMessage deserialise error
  2. calling the MCP endpoint directly with the expired stored access token returns an auth failure, not JSON-RPC
  3. calling the OAuth token endpoint directly with the stored refresh token succeeds (HTTP 200) and returns a fresh access token
  4. calling the same MCP initialize endpoint 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:

  • check expires_at before MCP startup/tool discovery and refresh if expired / near expiry
  • on auth failure during initialize, try the stored refresh token, persist the new token response, and retry
  • if refresh fails, surface the actual OAuth/HTTP auth error instead of a JSON-RPC deserialise error
xinyu-dev · 2 months ago

I have similar issue

oppegard · 2 months ago

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:

  1. Try to remember: has it been an hour since I authenticated? If "yes":
  2. Quit Codex.app
  3. Run codex mcp login honeycomb from the terminal
  4. Launch Codex.app
  5. Query "can you see honeycomb?" in a session.

I 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.

bhudgens · 2 months ago

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.

dissonantP · 2 months ago

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.

ankitvg · 2 months ago

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.

ericyangliu · 2 months ago

Same issue I'm seeing on my end, too.

roshanjrajan · 2 months ago

https://github.com/openai/codex/pull/30089

I think this stack is trying to work on it?

nima20002000 · 1 month ago

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:

  • Ubuntu Linux, Codex CLI 0.144.3
  • Remote Streamable HTTP MCP: Linear (https://mcp.linear.app/mcp)
  • OAuth credential written to ~/.codex/.credentials.json

Observed:

  1. codex mcp login linear completed successfully multiple times.
  2. The file credential contained an access token, refresh token, read/write scopes, and a future expires_at value.
  3. A direct MCP initialize request using that exact stored access token returned HTTP 200, proving the token and provider session were valid.
  4. A fresh codex exec still failed during MCP startup with:

``text
Auth error: OAuth authorization required, when send initialize request
``

  1. I also removed a duplicate curated Linear plugin registration and kept only the direct [mcp_servers.linear] entry. The failure persisted, so duplicate registration was not the root cause.
  2. Starting the same fresh read-only probe with this override immediately succeeded and exposed the Linear tools:

``text
-c 'mcp_oauth_credentials_store="file"'
``

  1. Persisting the following setting in ~/.codex/config.toml fixed 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.

stevenlee-oai contributor · 1 month ago

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:

  • Codex Desktop production: 26.715.21316 or newer, first released July 16, 2026 at approximately 23:27 UTC. Versions starting with 26.707 or older do not contain the complete fix.
  • Codex CLI/harness alpha: 0.145.0-alpha.3 or newer, first released July 10, 2026 at approximately 23:11 UTC. Alpha builds are publicly available.
  • Codex CLI/harness stable: the current 0.144.6 release does not contain the complete fix. The fixes are expected in the upcoming 0.145 stable 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.

infoseekAI · 1 month ago

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.

stevenlee-oai contributor · 1 month ago
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?

infoseekAI · 1 month ago

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

kosmikza · 1 month ago
> 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?

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

  • Codex CLI: 0.145.0
  • Client OS: Windows
  • MCP transport: Streamable HTTP
  • Authorization server: Microsoft Entra ID
  • MCP URL: xx
  • Public OAuth client; no client secret

The MCP server publishes RFC 9728 protected-resource metadata containing:

  • resource: xxx
  • authorization_servers: the tenant-specific Entra v2 endpoint
  • scopes_supported: xxxaccess_as_user

### 1. The automatic add-time OAuth flow omits the advertised API scope

The server is registered with:

``powershell
codex 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:

  • Callback failure: Entra cannot return the authorization code to Codex.
  • Scope/resource failure: Codex receives the code, but Entra refuses to exchange it for the

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
tool call error: tool call failed for
{name}/create_session`

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 public MCP endpoint was healthy.
  • RFC 9728 protected-resource metadata was available.
  • The unauthenticated MCP response returned the expected 401 challenge.
  • The challenge contained the correct resource metadata URL and required scope.
  • Re-running the interactive scoped login restored MCP access.
  • No MCP server reconfiguration was required.

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:

  1. no refresh token was persisted;
  2. the refresh token was persisted but not loaded;
  3. Codex attempted refresh with incorrect scope/resource parameters;
  4. a rotated refresh token was not persisted;
  5. an active Codex session retained stale OAuth state.

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:

  1. load the stored OAuth credential for the MCP server;
  2. exchange its refresh token at the discovered authorization server;
  3. preserve the MCP resource and required scope;
  4. persist any replacement refresh token returned by the provider;
  5. retry MCP initialization or the failed tool call;
  6. require browser interaction only when Entra explicitly rejects the refresh token or

requires user interaction.

### Actual behavior

After the access token expires:

  1. MCP tool calls fail before reaching the server;
  2. Codex reports OAuth authorization required;
  3. it does not recover silently;
  4. the user must run codex mcp login {name} and complete browser authorization again.

### Reproduction

  1. Register the server and complete the explicit scoped login.
  2. Confirm a tool call succeeds.
  3. Leave Codex configured until the access token expires.
  4. Attempt another tool call
  5. Observe OAuth tokens for server {name} cannot be refreshed.
  6. Run the scoped codex mcp login {name} command again.
  7. Confirm that the same MCP registration works without removing or re-adding it.

### Impact

This prevents reliable use of OAuth-protected MCP servers in:

  • long-running Codex sessions;
  • unattended workflows;
  • scheduled or recurring work;
  • clients expected to remain configured across days;
  • onboarding where users reasonably expect a one-time OAuth login.

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

  • openai/codex#17265 — Codex does not automatically refresh MCP OAuth tokens even when a

refresh token is stored.

  • openai/codex#14144 — successful reauthentication can leave an active session using stale

OAuth state.

  • openai/codex#12755 — refresh-token reuse/race failures across Codex processes.

The strongest evidence is that manual mcp login restores access immediately without
changing {name}, its Entra registration, discovery metadata, or server configuration. That
places the failure in the client-side OAuth credential/refresh lifecycle.

publi0 · 1 month ago

Adding a concrete Codex CLI datapoint for the remaining RMCP-initiated/background refresh edge case mentioned by @stevenlee-oai above.

Environment

  • macOS arm64
  • Codex CLI: local diagnostic build based on origin/main at 95637f7056835fea66bdd0044414af480fc0fd74
  • MCP transport: Streamable HTTP/SSE
  • Provider: enterprise Atlassian MCP behind an OAuth gateway
  • Access-token lifetime: approximately 1 hour
  • Refresh-token behavior: rotation with one-time use
  • Credential store: file

No token values, OAuth client secrets, or private endpoints are included below.

Observed timeline

All times are from the same machine on 2026-07-27:

  1. At approximately 08:27 local time, interactive OAuth login completed successfully. Codex received an access token expiring at approximately 09:27 and a refresh token, and persisted both.
  2. Process 57028, started at approximately 08:53, loaded those credentials and established the MCP connection.
  3. At 09:33:51, while the process was still alive, the background SSE stream/reconnect path logged:

``text
Access token expired or nearly expired, refreshing.
refresh token present, attempting refresh
Refreshed access token.
``

  1. The provider accepted the refresh token and rotated it, invalidating the previous refresh token. However, the durable credential record still contained the original expiry and original refresh-token generation. The replacement credentials existed only in the RMCP client's in-memory credential store.
  2. A later Codex process, 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.
``

  1. Codex therefore classified the credential as requiring reauthorization and fell back to a new browser/consent flow.

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 SSE get_stream reconnect, RMCP can call its own get_access_token and 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:

durable store: access A0 / refresh R0
RMCP memory:   access A1 / refresh R1
provider:      R0 invalidated after issuing R1

After a restart, or when another process reads the durable store, Codex presents R0 again 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 SSE get_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_stream path, 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:

codex-rmcp-client tests: 121 passed, 7 skipped
cargo check: passed
cargo build -p codex-cli: passed

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.

2sang-min · 1 month ago

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.
  • codex-rmcp-client: 120/120 tests passed (6 helper tests skipped).
  • 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.

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.

publi0 · 29 days ago

Status update — partially fixed, background SSE bypass still open

Rechecked against stable 0.146.0, 0.146.0-alpha.16, and current main@9a6668f67 on July 29, 2026.

What is now fixed

Stable 0.146.0 contains the serialized Codex-owned refresh transaction:

  • acquire the per-credential/store lock;
  • reread the authoritative credential after acquiring the lock;
  • adopt a newer generation committed by another process;
  • refresh an expired credential;
  • persist any rotated refresh token before installing/using the refreshed credential;
  • restore the previous in-memory credential and fail closed if persistence fails.

This addresses the primary concurrent-refresh/token-clobbering path described earlier by the maintainers.

Newer alphas add two related improvements:

  • #35720 (alpha.15+) distinguishes definitive refresh-token rejection from transient token endpoint/network failures.
  • #35814 (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 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:

  1. obtains the access token through OAuthPersistor for post_message, delete_session, and get_stream;
  2. durably persists rotation before exposing the refreshed access token to the transport;
  3. 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.

kosmikza · 29 days ago

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:

resource:
https://{name}/mcp

scopes_supported:
https://{name}/mcp/access_as_user

The failure occurred before list_sessions reached 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.

sahilsaiyed-oss · 19 days ago

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 · 10 days ago

Adding a current macOS Desktop datapoint that appears to isolate the remaining failure to background Streamable HTTP refresh persistence.

Environment

  • Codex Desktop: 26.810.52044
  • Bundled CLI / MCP client: 0.148.0-alpha.9
  • Credential store: mcp_oauth_credentials_store = "file"
  • OAuth-backed Streamable HTTP MCP
  • Access-token lifetime: 1 hour
  • Refresh-token lifetime: 30 days, rotating on use

Observed sequence

All times UTC:

  1. 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.
  2. 2026-08-16T00:46:55Z: the access token expired.
  3. 2026-08-16T00:53:59Z: provider logs show codex-mcp-client/0.148.0-alpha.9 successfully sent POST /token using the refresh grant.
  4. The provider atomically marked the presented refresh token consumed and issued an active rotated successor. The successor remains active server-side.
  5. The credential file's modification time did not change after 23:46:58Z and still contains the now-consumed predecessor.
  6. 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.

infoseekAI · 7 days ago

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 · 1 day ago

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

  • Codex Desktop: 26.820.60940
  • Bundled CLI / MCP client: 0.150.0-alpha.8
  • Credential store: mcp_oauth_credentials_store = "file"
  • 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

  1. All three connectors had previously completed authorization, registered as enabled/OAuth, and passed live read-only tool calls.
  2. Their stored access tokens expired at approximately 2026-08-26T22:24:16Z.
  3. More than four hours later, a fresh ephemeral Codex process initialized the plugin and attempted one harmless read from each server.
  4. All three refresh transactions failed independently with the same result:

``text
OAuth refresh token was rejected: Server returned error response:
invalid_grant: Refresh token is invalid or has already been used
``

  1. 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.
  2. 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.

blakehedlin-sibel · 1 day ago

@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.