Skip to content

Codex Desktop does not refresh OAuth-backed MCP access token before startup; keeps sending expired bearer from Keychain #27165

@tonyloks

Description

@tonyloks

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

  1. Configure an OAuth-backed Streamable HTTP MCP server.
  2. Run codex mcp login <server> and complete OAuth.
  3. Confirm the Keychain credential contains both access_token and refresh_token.
  4. Wait until the stored access token expires while the refresh token remains valid.
  5. 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_endpoint in 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/token request 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_var uses 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appauthIssues related to authentication and accountsbugSomething isn't workingmcpIssues related to the use of model context protocol (MCP) servers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions