Skip to content

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

Open

Description

@GARMTECH

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:

modelcontextprotocol/rust-sdk#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:

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

Activity

  1. added
    appIssues related to the Codex desktop app
    on Jul 15, 2026
  2. added
    bugSomething isn't working
    mcpIssues related to the use of model context protocol (MCP) servers
    authIssues related to authentication and accounts
    on Jul 15, 2026
  3. github-actions commented on Jul 15, 2026

    @github-actions
    Contributor

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

    Powered by Codex Action

  4. evannotfound commented on Jul 16, 2026

    @evannotfound

    i have the same issue

  5. nkurpas-indeed commented on Jul 23, 2026

    @nkurpas-indeed

    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.

  6. DainisGorbunovs commented on Aug 3, 2026

    @DainisGorbunovs

    rmcp was upgraded from 1.8.0 to 3.0.0 in #35720 and #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.
  7. m13v commented on Aug 4, 2026

    @m13v

    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.

  8. cbratschi commented on Aug 6, 2026

    @cbratschi

    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.

  9. royhenengel commented on Aug 14, 2026

    @royhenengel

    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:

    modelcontextprotocol/rust-sdk#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.

  10. m13v commented on Sep 2, 2026

    @m13v

    the trailing-slash fix kills the invalid_grant, but not the quiet path: nkurpas already saw a refresh rotate, persist, and advance expiry while the JWT still carried the wrong aud, so init 401s right after. a green 200 refresh isnt proof. assert the returned token's aud matches the discovered resource before you treat the refresh as done, or you just trade a loud reauth for a silent one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions