Codex Desktop does not refresh OAuth-backed MCP access token before startup; keeps sending expired bearer from Keychain
Summary
Codex Desktop / bundled Codex CLI fails to refresh an OAuth-backed Streamable HTTP MCP server before startup/tool discovery, even though the stored refresh token is present and valid.
Instead, Codex reads the expired access token from macOS Keychain and sends it to /mcp as Authorization: Bearer .... The MCP server returns 401 Invalid or expired access token. Codex does not call the advertised /oauth/token endpoint, so the MCP tools are not exposed.
This looks closely related to #17265, #26237, #26522, and #26523, but I am adding a concrete end-to-end trace with both Codex logs and server-side logs.
Environment
- Product: Codex Desktop App on macOS
- Bundled Codex CLI:
codex-cli 0.137.0-alpha.4 - PATH Codex CLI:
codex-cli 0.135.0(not the failing runtime) - Credential store: macOS Keychain, service
Codex MCP Credentials - MCP server transport:
streamable_http - MCP server auth: OAuth, no static
bearer_token_env_var - Access token TTL: 3600 seconds
- Refresh token TTL: 30 days
- MCP config shape:
[mcp_servers.dam]
url = "https://direct-alert.ru/mcp"
codex mcp list --json reports this server as OAuth-backed:
{
"name": "dam",
"transport": {
"type": "streamable_http",
"url": "https://direct-alert.ru/mcp",
"bearer_token_env_var": null,
"http_headers": null,
"env_http_headers": null
},
"auth_status": "o_auth"
}
Stored credential state
The local Keychain item exists under service Codex MCP Credentials for this MCP server. It contains both an access token and a refresh token.
Sanitized details:
server_name: dam
url: https://direct-alert.ru/mcp
client_id: mcp_SfV2_SWHRrmg8skr6xFJ51BuYbwdloxe
access_token_sha256_prefix16: 666fece093144e0c
refresh_token_sha256_prefix16: b5a3ba9ca5cfeeb5
expires_at: 2026-06-08T14:38:29.780Z
scope: mcp:tools
token_type: bearer
Server-side DB confirms the same token state:
access fp=666fece093144e0c user_id=1 expires_at=2026-06-08 14:38:29.777816 revoked_at=null
refresh fp=b5a3ba9ca5cfeeb5 user_id=1 expires_at=2026-07-08 13:38:29.777680 revoked_at=null
There are no newer access tokens for this user/client after 2026-06-08 13:38:29Z.
OAuth metadata is valid
The protected resource metadata advertises the authorization server:
{
"resource": "https://direct-alert.ru/mcp",
"authorization_servers": ["https://direct-alert.ru"],
"bearer_methods_supported": ["header"],
"resource_name": "direct-alert-mcp",
"scopes_supported": ["mcp:tools"]
}
The authorization server metadata advertises the token endpoint:
{
"issuer": "https://direct-alert.ru",
"authorization_endpoint": "https://direct-alert.ru/oauth/authorize",
"token_endpoint": "https://direct-alert.ru/oauth/token",
"registration_endpoint": "https://direct-alert.ru/oauth/register",
"revocation_endpoint": "https://direct-alert.ru/oauth/revoke"
}
Reproduction
- Configure an OAuth-backed Streamable HTTP MCP server.
- Run
codex mcp login <server>and complete OAuth. - Confirm the Keychain credential contains both
access_tokenandrefresh_token. - Wait until the stored access token expires while the refresh token remains valid.
- Start a fresh Codex Desktop / bundled CLI run that needs MCP tool discovery.
I reproduced the fresh startup path with:
/Applications/Codex.app/Contents/Resources/codex exec \
--cd /Users/antonperepecaev/Desktop/projects/direct-alert \
--sandbox read-only \
--json \
'Check whether the DAM MCP is available; do not create or modify files.'
Observed Codex behavior
Bundled Codex stderr shows an auth failure during MCP startup:
ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when AuthRequired(AuthRequiredError { www_authenticate_header: "Bearer resource_metadata=\"https://direct-alert.ru/.well-known/oauth-protected-resource/mcp\"" })
WARN codex_mcp::rmcp_client: failed to initialize MCP client during shutdown: MCP startup failed: handshaking with MCP server failed: Send message error Transport [...] error: Auth required, when send initialize request
The MCP tools are not exposed to the session.
Server-side trace for the same startup window
Fresh startup window: 2026-06-09T10:19:41Z..10:21:00Z.
Server logs:
10:19:50.243 GET /mcp -> 401, no bearer token
10:19:50.277 GET /.well-known/oauth-protected-resource/mcp -> 200
10:19:50.310 GET /.well-known/oauth-authorization-server -> 200
10:19:50.464 POST /mcp -> 401 Invalid or expired access token, access_token_fingerprint=666fece093144e0c
Critically, there is no /oauth/token request in that window.
So Codex successfully discovers the OAuth metadata, learns the correct token endpoint, but then sends the expired stored access token to /mcp instead of refreshing via /oauth/token.
Why this appears Codex-side
- The server exposes a valid
token_endpointin OAuth metadata. - The refresh token is present, not revoked, and not expired.
- The access token fingerprint sent by Codex exactly matches the expired access token stored in Keychain.
- No
/oauth/tokenrequest reaches the server during startup/tool discovery. - Other MCP transport types work because they do not use this path:
- stdio MCPs do not use OAuth refresh;
- local Streamable HTTP with
bearer_token_env_varuses a static bearer; - only OAuth-backed Streamable HTTP MCP fails after access token expiry.
Expected behavior
Before MCP initialize/tool discovery, Codex should detect that the persisted OAuth access token is expired or near expiry, call the advertised token endpoint with grant_type=refresh_token, persist the new token response, and then initialize the MCP server with the fresh access token.
If the server returns 401 invalid_token during initialize, Codex should refresh once using the stored refresh token and retry initialize, or surface a clear reauth-required error if refresh fails.
Actual behavior
Codex does not call /oauth/token; it sends the expired Keychain access token to /mcp, receives 401, and the MCP tools are unavailable.
Manual re-login fixes the problem temporarily until the next access-token expiry.
7 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I can corroborate this from a Windows Codex Desktop setup with an OAuth-backed Streamable HTTP MCP server that supports Dynamic Client Registration and refresh-token rotation.
Sanitized reproduction/evidence:
token_endpoint,grant_types_supportedincludesrefresh_token,token_endpoint_auth_methods_supportedincludesnone, and scopes include offline refresh capability.access_token,refresh_token,client_id,expires_at, scopes, server name, and server URL.grant_type=refresh_tokenrequest to the advertised token endpoint succeeds and returns a rotatedaccess_token, rotatedrefresh_token, andexpires_in.initializerequest using the refreshed access token succeeds with HTTP 200 and returns server info.Auth requiredafter external refresh/login, which suggests the worker either does not always refresh before initialize, does not recover reactively from 401/AuthRequired, or does not update its in-memory Authorization state after the credential cache changes.The code appears to have part of the intended mechanism already:
codex-rs/rmcp-client/src/oauth.rshasOAuthPersistor::refresh_if_needed()and persists rotated tokens viapersist_if_needed().codex-rs/app-server/src/mcp_refresh.rscan queueRefreshMcpServersfor active threads.The remaining product gap seems to be one or both of these:
Desired behavior:
access_token,refresh_token, andexpires_atatomically in the configured credential store.invalid_grantor equivalent, only then prompt browser OAuth/reconnect.Useful tests would cover:
This is especially important for unattended/overnight agents using OAuth-backed MCP servers where daily browser reauth defeats the purpose of storing refresh tokens.
Additional sanitized reproduction, in case it helps narrow this down. This looks like the same failure mode, but on Linux with
mcp_oauth_credentials_store = "file"and Atlassian Rovo MCP rather than macOS Keychain.Environment
codex-cli 0.141.0mcp_oauth_credentials_store = "file"https://mcp.atlassian.com/v1/mcp/authv2codex mcp list --jsonreports this server as OAuth-backed:Timeline
codex mcp login atlassian. The credential file was updated and Codex could treat the server as configured/authenticated again.OAuth authorization required, when send initialize requesterror.Sanitized credential state after manual login
No token values included. The stored credential contained exactly one Atlassian entry.
Atlassian OAuth metadata checks
Unauthenticated request to the MCP endpoint returns a proper Bearer challenge with resource metadata:
Protected resource metadata also matches the configured resource and advertises
offline_access:The advertised authorization server metadata includes a token endpoint and
refresh_tokengrant support:Interpretation
This does not appear to be specific to macOS Keychain. In this reproduction:
auth_status: "o_auth").offline_access, a token endpoint, andrefresh_tokengrant support.codex mcp login atlassiantemporarily fixes the issue until the access token expires again.I did not manually exchange the stored refresh token because Atlassian uses a rotating refresh token and I did not want to mutate the user's credential state while collecting diagnostics.
Based on the above, this looks consistent with Codex not refreshing the stored OAuth-backed Streamable HTTP MCP access token before startup/tool discovery, or not retrying initialize with a refreshed token after
invalid_token.This is not a duplicate. I'm seeing it on ubuntu with the latest codex cli.
Have this on OpenAI Codex (v0.143.0)
This happens a lot
My
claudedoes not log out this much, butcodexdoes all the time. It's very annoying.Additional current reproduction for Atlassian Rovo MCP.
Environment
codex-cli 0.147.0https://mcp.atlassian.com/v1/mcp/authv2Observed behavior
codex mcp login atlassianrestores access temporarily. After the access token expires, Codex requests reauthorization. The Codex logs contain:The same failure occurs with Codex CLI
0.145.0and0.147.0. Immediately before the provider error, the logs report successful loading of the Atlassian credential fromCodex MCP Credentials. The current CLI also uses the shared MCP OAuth transaction lock and rereads the authoritative credential store before refreshing.Expected behavior
Concurrent Codex sessions should refresh the shared Atlassian OAuth credentials and continue operating without daily browser reauthorization. Rotated credentials should update active MCP clients or trigger a clean MCP reconnect.
No OAuth token values or private credentials are included here.