Files
codex/codex-rs/rmcp-client/src/oauth/test_support.rs
T
Nick Steele b090e901f8 Add staged enterprise OIDC login and coordinated logout (#43844)
## Why

Enterprise sign-in needs to keep browser completion separate from credential storage so callers can recheck the active account and configuration before saving a grant. Logout must also prevent an earlier sign-in from restoring credentials, including from another process sharing `CODEX_HOME`.

## What changed

- Add enterprise login APIs in `rmcp-client` that return an authorization URL and stage validated credentials for an explicit `commit_if` call. Store grants only in the keyring after rechecking caller authority under the credential lock.
- Require a registered client ID, published metadata matching the configured issuer, HTTP loopback callbacks, a refresh token, and a valid OIDC identity assertion. Request `openid` and `offline_access` with `prompt=consent`, and omit MCP resource indicators from authorization and code exchange.
- Persist a login generation under the credential lock so logout invalidates pending and staged sign-ins across processes, even when no grant is stored.
- Keep credentials and account identifiers out of enterprise error chains and logs, and avoid logging callback payloads when the receiver has closed.

## Testing

Add coverage for discovery validation, loopback callbacks, PKCE, staged keyring storage, cancellation, stale attempts, cross-process logout, and error/log privacy. Preserve ordinary MCP OAuth login without a refresh token, and adjust the terminal polling test deadline to include the minimum empty-poll wait.

GitOrigin-RevId: 2a27b9a26505a2f6fdecce8877f6e2c21e148f72
2026-09-08 16:07:38 +00:00

70 lines
2.1 KiB
Rust

use std::path::Path;
use std::sync::Mutex;
use std::sync::MutexGuard;
use std::sync::OnceLock;
use std::sync::PoisonError;
use tempfile::tempdir;
/// Build the actual client's Stop-policy route pool before measuring protocol deadlines.
/// Client construction loads platform/custom CA state and can be slow on developer hosts.
/// Use a separate local endpoint so warmup cannot affect the test server's request assertions;
/// the same production HTTP capability still applies all proxy and certificate policy.
pub(crate) async fn warm_http_client(
client: &dyn codex_exec_server::HttpClient,
) -> anyhow::Result<()> {
let server = wiremock::MockServer::start().await;
client
.http_request(codex_exec_server::HttpRequestParams {
method: "GET".to_string(),
url: server.uri(),
headers: Vec::new(),
body: None,
timeout_ms: None,
redirect_policy: codex_exec_server::HttpRedirectPolicy::Stop,
request_id: "test-client-warmup".to_string(),
stream_response: false,
})
.await?;
Ok(())
}
/// Serializes tests that mutate process-wide CODEX_HOME.
///
/// Keep OAuth tests on this one guard instead of defining per-module helpers; otherwise
/// concurrently running test modules can point File/Secrets storage at different homes.
pub(crate) struct TempCodexHome {
_guard: MutexGuard<'static, ()>,
_dir: tempfile::TempDir,
}
impl TempCodexHome {
pub(crate) fn new() -> Self {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let guard = LOCK
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(PoisonError::into_inner);
let dir = tempdir().expect("create CODEX_HOME temp dir");
unsafe {
std::env::set_var("CODEX_HOME", dir.path());
}
Self {
_guard: guard,
_dir: dir,
}
}
pub(super) fn path(&self) -> &Path {
self._dir.path()
}
}
impl Drop for TempCodexHome {
fn drop(&mut self) {
unsafe {
std::env::remove_var("CODEX_HOME");
}
}
}