MCP OAuth refresh omits the RFC 8707 resource parameter and breaks authenticated servers after access-token expiry

Open 💬 7 comments Opened Jul 15, 2026 by GARMTECH
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of the Codex App are you using (From “About Codex” dialog)?

Codex App 26.707.72221 (build 5307), bundled codex-cli 0.144.2

What subscription do you have?

ChatGPT Pro

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

A remote Streamable HTTP MCP server authenticates and works immediately after OAuth login, but stops working when the 15-minute access token expires.

The authorization server requires the RFC 8707 resource parameter in authorization and token requests, as required by the MCP 2025-11-25 authorization specification.

In Codex App 26.707.72221, after token expiry the MCP server becomes unavailable and a required MCP server can prevent creation of a new task:

Failed to initialize session: required MCP servers failed to initialize: supportpal: handshaking with MCP server failed: Send message error Transport [rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<rmcp::transport::auth::AuthClient<codex_rmcp_client::http_client_adapter::StreamableHttpClientAdapter>>>] error: Auth error: OAuth authorization required, when send initialize request

Server-side audit showed that the access token had expired, while the rotating refresh token remained valid. With the bundled CLI 0.144.2, no successful refresh reached the token endpoint.
I repeated the test using codex-cli 0.144.3. That version attempted refresh, but omitted resource and failed with:

Token refresh not possible, re-authorization required: OAuth token refresh failed: Server returned error response: invalid_target: resource is required and must match this MCP server.

The project configuration already supplies the canonical resource:

[mcp_servers.supportpal]
url = "https://support.yourdomain.com/mcp"
oauth_resource = "https://support.yourdomain.com/mcp"
required = true

[mcp_servers.supportpal.oauth]
client_id = "codex-desktop-support-yourdomain"

The configured oauth_resource is present during login but is not included in the refresh-token request.

What steps can reproduce the bug?

  1. Configure a Streamable HTTP MCP server whose OAuth token endpoint requires resource on every token request:
[mcp_servers.supportpal]
url = "https://support.yourdomain.com/mcp"
oauth_resource = "https://support.yourdomain.com/mcp"
required = true

[mcp_servers.supportpal.oauth]
client_id = "codex-desktop-support-yourdomain"
  1. Authenticate the MCP server through Codex App.
  2. Start a task and confirm that MCP initialization and a metadata-only tool call succeed.
  3. Wait until the access token expires. The test server uses a 15-minute access-token TTL and a 30-day rotating refresh-token TTL.
  4. Start another task or invoke the MCP server again.
  5. Observe that Codex App cannot restore the MCP connection. With codex-cli 0.144.3, the refresh request is sent without resource and the authorization server returns:
invalid_target: resource is required and must match this MCP server.

Investigation session ID:

019f5c1e-7426-7a42-bcbe-c1017fef84ef

The 0.144.3 command-line reproduction used codex exec --ephemeral. thread/start failed during required MCP initialization, before a persistent session ID was created.

Token-limit and context-window usage are not applicable; the failure happens during MCP initialization before the task starts.

What is the expected behavior?

Codex should include the configured canonical resource in every OAuth token request, including refresh-token requests:

resource=https://support.yourdomain.com/mcp

When the access token expires, Codex should use the still-valid refresh token, accept and persist the rotated refresh token, and continue using the MCP server without requiring another browser authorization.

A required MCP server should not prevent task creation when a valid refresh token can be refreshed successfully.

Additional information

The MCP specification requires clients to include resource in both authorization and token requests:

https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#resource-parameter-implementation

This exact omission was fixed upstream in the MCP Rust SDK on July 3, 2026:

https://github.com/modelcontextprotocol/rust-sdk/pull/951

The PR states:

Refresh-token requests omitted the RFC 8707 resource parameter, which the spec requires on all token requests.

Codex 0.144.3 still pins rmcp 1.8.0, whose refresh path does not contain that fix. The Codex 0.144.3 release is documented as a version-only release with no merged changes since 0.144.2:

https://github.com/openai/codex/releases/tag/rust-v0.144.3

Codex previously added oauth_resource handling for MCP login flows in:

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

That change covered the authorization/login path, but not refresh-token requests.

Suggested fix: update the rmcp dependency to a revision containing modelcontextprotocol/rust-sdk#951, or cherry-pick the equivalent change so the refresh request includes the configured oauth_resource.

The MCP server should not be changed to accept refresh requests without resource, because doing so would weaken token audience binding. Setting required = false prevents the entire Codex task from failing, but it does not restore MCP access and is only an availability workaround.

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 1 month ago

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

  • #32590

Powered by Codex Action

evannotfound · 1 month ago

i have the same issue

nkurpas-indeed · 1 month ago

Confirmed this on Codex 0.145.0 with the same MCP server on both macOS and Linux.

At access-token expiry:

  • Codex successfully sends the refresh request.
  • Both access and refresh tokens rotate and are persisted.
  • The new expiry advances normally.
  • The refreshed JWT has the authorization server’s default audience instead of the MCP protected resource.
  • MCP initialization immediately fails with 401 AuthRequired.

The initial authorization-code exchange produces the correct audience, so the failure is specific to refresh. This matches RMCP 1.8.0 omitting resource from exchange_refresh_token().

Updating RMCP to a version containing modelcontextprotocol/rust-sdk#951—or cherry-picking that refresh-path change—should fix this.

DainisGorbunovs · 24 days ago

rmcp was upgraded from 1.8.0 to 3.0.0 in https://github.com/openai/codex/pull/35720 and https://github.com/openai/codex/pull/36001. This means this issue should be fixed in the latest Codex's alpha releases rust-v0.146.0-alpha.15, rust-v0.147.0-alpha.1 or newer. Likely it is resolved, but I haven't run the alpha release to confirm.

EDIT: on v0.147.0-alpha.4 this issue seems to be resolved and it auto-refreshed OAuth sessions for me. However, due to it being more strict, one of the MCPs I'm getting Error: Authorization server issuer mismatch: expected https://auth.example.com/..., received https://auth.example.com when doing codex mcp login example.

  • It appears to be an issue in the third party's MCP, where their returned inconsistent metadata advertises https://auth.example.com/... as its issuer but returns https://auth.example.com in the corresponding authorization-server metadata.
  • Codex's rmcp 3.0 dependency strictly rejects that mismatch, while codex with the older rmcp 1.8 tolerated it.
m13v · 24 days ago

adding resource to the refresh request is the right call, but there is a second quiet failure sitting behind it. with a rotating refresh token, a successful refresh hands you a NEW refresh token, and if you persist the fresh access token but the rotated refresh token does not land (write races the task exit, partial flush, whatever), the next expiry replays reauthorization required against an already-consumed token. that surfaces as plain session-expired, so you cant tell a real 30-day expiry apart from a dropped rotation. worth asserting the rotated token actually persisted before you treat the refresh as succeeded, otherwise the fix just moves the silent break one cycle downstream.

cbratschi · 22 days ago

Still reproducible in Codex desktop 0.146.0-alpha.9.2 (embedding rmcp 1.8.0). Initial OAuth authorization succeeds against a resource-bound MCP server. After the one-hour access token expires, Codex attempts refresh, but the refresh request omits the RFC 8707 resource parameter. The server rejects it with invalid_grant; manual reauthentication works until the next access-token expiry. Please update rmcp to the version containing the refresh-resource fix.

royhenengel · 13 days ago

Still reproducible with a newer Codex build that already embeds rmcp 3.0.0.

Environment

  • Codex App: 26.810.41047 (build 6570)
  • Bundled CLI: codex-cli 0.148.0-alpha.9
  • macOS: 26.6.1 (25G76), Apple Silicon
  • MCP server: Apify, Streamable HTTP at https://mcp.apify.com
  • Date reproduced: August 14, 2026

Clean reproduction

  1. Confirmed the Apify MCP registration was enabled and used the exact URL https://mcp.apify.com.
  2. Fully quit Codex/ChatGPT and restarted with exactly one ChatGPT process and one bundled Codex process.
  3. Ran codex mcp login apify and completed OAuth.
  4. Immediately made a read-only Apify MCP call. It succeeded.
  5. Avoided all other ChatGPT/Codex sessions during the isolation interval.
  6. After the one-hour access-token lifetime had passed, made the same read-only call.
  7. The first refresh attempt failed with:
failed to refresh OAuth tokens for server apify:
OAuth refresh token was rejected:
Server returned error response:
invalid_grant: Invalid refresh token

The issued refresh token advertised an approximately 60-day lifetime. Its stored credential was written only at login and was not modified before the failed refresh, ruling out an earlier successful rotation followed by stale-token replay. No competing Codex process existed during the reproduction.

Likely root cause

rmcp 3.0.0 contains a second refresh-resource bug after the earlier missing-resource fix.

Initial authorization and authorization-code exchange use the resource discovered from RFC 9728 protected-resource metadata:

.add_extra_param("resource", self.oauth_resource().await)

But the rmcp 3.0.0 refresh path uses:

.add_extra_param("resource", self.base_url.to_string())

Apify’s protected-resource metadata identifies the canonical resource as:

https://mcp.apify.com

The refresh path can therefore send a different serialized audience, such as:

https://mcp.apify.com/

This exactly matches the upstream bug fixed by modelcontextprotocol/rust-sdk#1084:

https://github.com/modelcontextprotocol/rust-sdk/pull/1084

That fix shipped in rmcp 3.0.1:

https://github.com/modelcontextprotocol/rust-sdk/releases/tag/rmcp-v3.0.1

Codex currently pins rmcp exactly to 3.0.0, including rust-v0.148.0-alpha.15, so upgrading between current 0.148 alpha builds does not include the upstream fix:

https://github.com/openai/codex/blob/rust-v0.148.0-alpha.15/codex-rs/Cargo.toml#L399

Suggested fix

Update Codex from rmcp = "=3.0.0" to rmcp >= 3.0.1, or cherry-pick the one-line upstream fix so refresh uses self.oauth_resource().await.

A regression test should cover a root MCP endpoint whose protected-resource metadata omits the trailing slash and confirm that authorization, code exchange, and refresh all send the identical discovered resource.

No token or credential values are included in this report.