From 9d809e68cd606f0d6753733f16143bd032661bd4 Mon Sep 17 00:00:00 2001
From: GraciousGazelles <55840159+GraciousGazelles@users.noreply.github.com>
Date: Sat, 20 Jun 2026 14:30:22 +1000
Subject: [PATCH 1/4] Serialize MCP OAuth refresh ownership
---
codex-rs/cli/src/mcp_cmd.rs | 4 +-
codex-rs/rmcp-client/src/lib.rs | 4 +-
codex-rs/rmcp-client/src/oauth.rs | 668 ++++++++++++++++--
.../src/perform_oauth_device_login.rs | 4 +-
.../rmcp-client/src/perform_oauth_login.rs | 4 +-
codex-rs/rmcp-client/src/rmcp_client.rs | 209 +++---
.../rmcp-client/src/streamable_http_retry.rs | 121 +++-
.../tests/streamable_http_oauth_startup.rs | 2 +-
8 files changed, 829 insertions(+), 187 deletions(-)
diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs
index 4def1e0649aa..97c1d62cf5ac 100644
--- a/codex-rs/cli/src/mcp_cmd.rs
+++ b/codex-rs/cli/src/mcp_cmd.rs
@@ -27,7 +27,7 @@ use codex_mcp::resolve_oauth_scopes;
use codex_mcp::should_retry_without_scopes;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::DeviceAuthorizationPrompt;
-use codex_rmcp_client::delete_oauth_tokens;
+use codex_rmcp_client::delete_oauth_tokens_locked;
use codex_rmcp_client::perform_oauth_device_login;
use codex_rmcp_client::perform_oauth_login;
use codex_utils_cli::CliConfigOverrides;
@@ -649,7 +649,7 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
_ => bail!("OAuth logout is only supported for streamable_http transports."),
};
- match delete_oauth_tokens(&name, &url, config.mcp_oauth_credentials_store_mode) {
+ match delete_oauth_tokens_locked(&name, &url, config.mcp_oauth_credentials_store_mode).await {
Ok(true) => println!("Removed OAuth credentials for '{name}'."),
Ok(false) => println!("No OAuth credentials stored for '{name}'."),
Err(err) => return Err(anyhow!("failed to delete OAuth credentials: {err}")),
diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs
index d18d0acce6bc..b564f01653ac 100644
--- a/codex-rs/rmcp-client/src/lib.rs
+++ b/codex-rs/rmcp-client/src/lib.rs
@@ -21,8 +21,10 @@ pub use in_process_transport::InProcessTransportFactory;
pub use oauth::StoredOAuthTokens;
pub use oauth::WrappedOAuthTokenResponse;
pub use oauth::delete_oauth_tokens;
-pub(crate) use oauth::load_oauth_tokens;
+pub use oauth::delete_oauth_tokens_locked;
+pub(crate) use oauth::load_oauth_tokens_with_source;
pub use oauth::save_oauth_tokens;
+pub use oauth::save_oauth_tokens_locked;
pub use perform_oauth_device_login::DeviceAuthorizationPrompt;
pub use perform_oauth_device_login::perform_oauth_device_login;
pub use perform_oauth_login::OAuthProviderError;
diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs
index 3ab905b83e2d..d007cec83fe3 100644
--- a/codex-rs/rmcp-client/src/oauth.rs
+++ b/codex-rs/rmcp-client/src/oauth.rs
@@ -35,11 +35,14 @@ use sha2::Digest;
use sha2::Sha256;
use std::collections::BTreeMap;
use std::fs;
+use std::fs::File;
+use std::fs::OpenOptions;
use std::io::ErrorKind;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
+use std::time::Instant;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tracing::warn;
@@ -47,12 +50,22 @@ use tracing::warn;
use codex_keyring_store::DefaultKeyringStore;
use codex_keyring_store::KeyringStore;
use rmcp::transport::auth::AuthorizationManager;
+use rmcp::transport::auth::CredentialStore as _;
+use rmcp::transport::auth::InMemoryCredentialStore;
+use rmcp::transport::auth::StoredCredentials;
use tokio::sync::Mutex;
+use tokio::time::sleep;
+use tokio::time::timeout;
use codex_utils_home_dir::find_codex_home;
const KEYRING_SERVICE: &str = "Codex MCP Credentials";
-const REFRESH_SKEW_MILLIS: u64 = 30_000;
+const REFRESH_SKEW_MILLIS: u64 = 60_000;
+const REFRESH_LOCK_DIR: &str = "mcp-oauth-refresh-locks";
+const LOCK_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(60);
+const CREDENTIAL_LOCK_RETRY_SLEEP: Duration = Duration::from_millis(500);
+const STORE_LOCK_RETRY_SLEEP: Duration = Duration::from_millis(50);
+const REFRESH_TRANSACTION_TIMEOUT: Duration = Duration::from_secs(45);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StoredOAuthTokens {
@@ -84,20 +97,67 @@ pub(crate) enum StoredOAuthTokenStatus {
AuthorizationRequired,
}
+/// Concrete credential store resolved for one MCP OAuth client lifecycle.
+///
+/// A client that loaded credentials from one backend must reread, refresh, and
+/// persist through that same backend. Falling back mid-refresh can replay stale
+/// rotating refresh tokens from another store.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum ResolvedOAuthCredentialStore {
+ File,
+ Keyring,
+}
+
+#[derive(Debug)]
+pub(crate) struct LoadedOAuthTokens {
+ pub(crate) tokens: StoredOAuthTokens,
+ pub(crate) store: ResolvedOAuthCredentialStore,
+}
+
pub(crate) fn load_oauth_tokens(
server_name: &str,
url: &str,
store_mode: OAuthCredentialsStoreMode,
) -> Result