Skip to content

Fix MCP OAuth refresh-token races across Codex processes#12815

Closed
etraut-openai wants to merge 4 commits intomainfrom
etraut/add-guarded-reload-for-mcp
Closed

Fix MCP OAuth refresh-token races across Codex processes#12815
etraut-openai wants to merge 4 commits intomainfrom
etraut/add-guarded-reload-for-mcp

Conversation

@etraut-openai
Copy link
Copy Markdown
Collaborator

@etraut-openai etraut-openai commented Feb 25, 2026

Addresses #12755

Token refresh for MCP OAuth suffered from the same problem that our main Codex ChatGPT auth had until recently. This PR updates the MCP logic to mirror the solution in place for ChatGPT auth token refresh.

MCP OAuth credentials are shared across processes via keyring or
CODEX_HOME/.credentials.json, but each process kept its own in-memory
snapshot and refreshed from that local copy. That meant one process could
rotate the refresh token while another still tried to use the stale token,
causing refresh_token_reused failures.

This changes the MCP refresh path to use a guarded reload flow modeled on
the main ChatGPT auth fix:

  • reload shared MCP OAuth credentials before any refresh attempt
  • compare persisted credentials to the in-memory cached snapshot with
    refresh-specific equality
  • if persisted credentials changed, adopt them into the live RMCP auth runtime
    and skip the local refresh
  • if persisted credentials are unchanged, perform the refresh and persist the
    rotated credentials
  • if persisted credentials are missing, treat that as logged out and clear the
    live runtime auth instead of using a stale refresh token

To keep the running RMCP auth stack in sync without reconnecting, the client
now owns the AuthorizationManager credential store and can update the live
runtime in place when reloaded credentials win.

Also adds:

  • unit tests for guarded reload decision logic in codex-rs/rmcp-client/src/oauth.rs
  • an end-to-end two-client integration test in
    codex-rs/core/tests/suite/rmcp_client.rs
  • refresh-token support in the streamable HTTP MCP test server fixture

@etraut-openai
Copy link
Copy Markdown
Collaborator Author

@codex review

@etraut-openai etraut-openai changed the title Add guarded reload for MCP OAuth refresh Fix token refresh issue with MCP OAuth Feb 25, 2026
@etraut-openai etraut-openai changed the title Fix token refresh issue with MCP OAuth Fix MCP OAuth refresh-token races across Codex processes Feb 25, 2026
Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c47fd545dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@etraut-openai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78249f7fce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@etraut-openai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7479ee1cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

self.persist_if_needed().await
}
GuardedRefreshOutcome::MissingOrInvalid => self.apply_runtime_credentials(None).await,
GuardedRefreshOutcome::ReloadFailed => Ok(()),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh tokens when guarded reload fails

refresh_if_needed returns Ok(()) on ReloadFailed, even though this branch is reached only after token_needs_refresh(...) is true. If keyring/file reload fails transiently, the runtime keeps near-expired credentials and skips both refresh and logout, so subsequent MCP calls can keep sending stale bearer tokens and fail with 401s.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe at least log?

@etraut-openai
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7479ee1cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -595,22 +604,20 @@ async fn create_oauth_transport_and_runtime(
)> {
let http_client =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would've been very nice to reuse default_client we use elsewhere, it sets all the right headers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated, sorry

}
};

if let Some(runtime) = &oauth_persistor
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: oauth_persistor is a bad name for this type at this point. McpAuthManager maybe

token_response: Some(credentials.token_response.0.clone()),
})
.await?;
guard
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does client id ever change on refresh?

}
}

fn load_oauth_tokens_for_guarded_refresh_with_keyring_fallback<K: KeyringStore>(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we avoid a separate set of reading method for guarded refresh and reuse existing read methods?

}
}

fn oauth_tokens_equal_for_refresh(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this much logic? can we just compare the access token?

}

#[derive(Debug, PartialEq)]
enum GuardedRefreshPersistedCredentials {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be Option<StoredOAuthTokens> ?

server_name: String,
url: String,
authorization_manager: Arc<Mutex<AuthorizationManager>>,
runtime_credentials: InMemoryCredentialStore,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunate that we have multiple creadential storages to keep in sync. Any chance we can reuse runtime_credentials as last_credentials?

Copy link
Copy Markdown
Collaborator

@pakrym-oai pakrym-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core logic looks good but I wonder if we can collapse reader methods.

@github-actions
Copy link
Copy Markdown
Contributor

Closing this pull request because it has had no updates for more than 14 days. If you plan to continue working on it, feel free to reopen or open a new PR.

@github-actions github-actions bot closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants