Files
codex/codex-rs/codex-mcp/src/auth_tests.rs
T
cooper-oai 219ab4043e Use runtime authentication for OpenAI MCP and app requests (#59749)
## What changed

- Select fresh authentication for each HTTP operation on eligible OpenAI MCP connections while preserving the connection's credential source and account network policy. Stop redirects when requests carry runtime assertions.
- Pass the owning thread's authentication and runtime context through app discovery, connector metadata, and trusted-access requests. Include the runtime authentication mode in MCP connection and tool-cache identities.
- Give resumed actors analytics clients bound to their own authentication context, and serialize stored-credential reloads with token refresh while preserving host-provided credentials.

## Testing

Add coverage for fresh request assertions, revoked account policies, redirect handling, resumed-actor isolation, and MCP uploads in dual-header and assertion-only modes.

GitOrigin-RevId: f06900a6db61cd6cf53c3aa8075afeec4d5706d9
2026-11-05 22:03:29 +00:00

204 lines
8.1 KiB
Rust

use super::*;
use base64::Engine;
use codex_config::types::AuthKeyringBackendKind;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::HttpRedirectPolicy;
use codex_exec_server::HttpRequestParams;
use codex_features::RuntimeAuthMode;
use codex_http_client::DestinationPolicy;
use codex_http_client::HttpClientFactory;
use codex_http_client::NetworkPolicyController;
use codex_http_client::OutboundProxyPolicy;
use codex_login::AuthCredentialsStoreMode;
use codex_login::AuthManager;
use codex_login::AuthRouteConfig;
use codex_runtime_agent_instance::AgentHostAuthorization;
use codex_runtime_agent_instance::RuntimeAuthManager;
use codex_runtime_agent_instance::RuntimeRegistrationMetadata;
use codex_runtime_agent_instance::RuntimeRole;
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::path;
struct RotatingAuth(std::sync::RwLock<CodexAuth>);
impl codex_login::ExternalAuth for RotatingAuth {
fn resolve(&self) -> codex_login::ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Ok(self.0.read().unwrap().clone()) })
}
fn refresh(
&self,
_: codex_login::ExternalAuthRefreshContext,
) -> codex_login::ExternalAuthFuture<'_, CodexAuth> {
self.resolve()
}
}
#[tokio::test]
async fn deferred_connection_keeps_runtime_source_and_captured_account_policy() -> anyhow::Result<()>
{
for mode in [RuntimeAuthMode::DualHeader, RuntimeAuthMode::AssertionOnly] {
let server = MockServer::start().await;
Mock::given(path("/agent/register"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"runtime_agent_instance_id": "runtime-mcp", "agent_host_id": "host-a",
"actor": {"kind": "chatgpt", "user_id": "user-a", "account_id": "workspace-a"},
})))
.mount(&server)
.await;
Mock::given(path("/mcp"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let home = tempfile::tempdir()?;
let oauth = |session, token| {
let claims = json!({
"iss": "https://auth.openai.com/", "session_id": session, "jti": token,
"https://api.openai.com/auth": {"user_id": "user-a", "chatgpt_account_id": "workspace-a"}
});
let payload =
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.to_string());
CodexAuth::from_external_chatgpt_tokens(
&format!("header.{payload}.signature"),
"workspace-a",
/*chatgpt_plan_type*/ None,
)
.unwrap()
};
let initial = oauth("session-a", "initial");
let refreshed = oauth("session-a", "refreshed");
let source = Arc::new(RotatingAuth(std::sync::RwLock::new(initial.clone())));
let controller = NetworkPolicyController::default();
let policy = controller.policy();
let factory = HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault)
.with_network_policy(policy.clone());
let credentials = Arc::new(
AuthManager::new(
home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::Ephemeral,
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::Direct,
AuthRouteConfig::from_http_client_factory(factory.clone()),
)
.await,
);
credentials.set_external_auth(source.clone()).await?;
assert!(controller.publish(policy.revision(), DestinationPolicy::Unrestricted));
let runtime = (
Arc::new(std::sync::OnceLock::from(Arc::new(
RuntimeAuthManager::new(
Arc::clone(&credentials),
AgentHostAuthorization::InstallationId {
installation_id: uuid::Uuid::nil(),
},
RuntimeRegistrationMetadata {
harness: Some("codex".into()),
harness_version: None,
role: RuntimeRole::Orchestrator,
},
format!("{}/agent/register", server.uri()).parse()?,
),
))),
mode,
);
let context = McpRuntimeContext::new(
Arc::new(EnvironmentManager::without_environments(factory)),
home.path().to_path_buf(),
);
let deferred = McpAuthProvider::runtime(runtime.clone(), initial.clone());
assert!(server.received_requests().await.unwrap().is_empty());
let error = deferred
.resolve(context.clone(), "https://future.openai.com/mcp")
.await
.err()
.expect("unsupported OpenAI destination must fail before registration");
assert!(
error
.downcast_ref::<codex_protocol::error::CodexErr>()
.is_some_and(|error| matches!(
error.details(),
codex_protocol::error::CodexErrorDetails::UnsupportedOperation(_)
))
);
assert!(server.received_requests().await.unwrap().is_empty());
let (provider, selected_context) = deferred.resolve(context.clone(), &server.uri()).await?;
let first = provider
.resolve(&Default::default())
.await?
.apply_auth_headers(Default::default())
.await?;
*source.0.write().unwrap() = refreshed.clone();
let second = provider
.resolve(&Default::default())
.await?
.apply_auth_headers(Default::default())
.await?;
let assertion_header = match mode {
RuntimeAuthMode::DualHeader => "x-openai-agent-assertion",
RuntimeAuthMode::AssertionOnly => "authorization",
};
assert_ne!(first[assertion_header], second[assertion_header]);
if mode == RuntimeAuthMode::DualHeader {
assert_eq!(
first["authorization"],
format!("Bearer {}", initial.get_token()?)
);
assert_eq!(
second["authorization"],
format!("Bearer {}", refreshed.get_token()?)
);
}
let request = || HttpRequestParams {
method: "GET".into(),
url: format!("{}/mcp", server.uri()),
headers: Vec::new(),
body: None,
timeout_ms: Some(1000),
redirect_policy: HttpRedirectPolicy::Stop,
request_id: "mcp-policy-test".into(),
stream_response: false,
};
let client = selected_context.local_http_client();
assert_eq!(client.http_request(request()).await?.status, 200);
policy.invalidate();
assert!(controller.publish(policy.revision(), DestinationPolicy::Unrestricted));
assert!(client.http_request(request()).await.is_err());
// A new connection captures the new policy and reuses the same registered runtime.
let current = McpAuthProvider::runtime(runtime, refreshed);
let (_, current_context) = current.resolve(context, &server.uri()).await?;
assert_eq!(
current_context
.local_http_client()
.http_request(request())
.await?
.status,
200
);
let requests = server.received_requests().await.unwrap();
assert_eq!(
requests
.iter()
.filter(|request| request.url.path() == "/agent/register")
.count(),
1
);
*source.0.write().unwrap() = oauth("another-session", "replacement");
assert!(provider.resolve(&Default::default()).await.is_err());
assert_eq!(
requests
.iter()
.filter(|request| request.url.path() == "/mcp")
.count(),
2
);
}
Ok(())
}