Files
codex/codex-rs/cli/tests/cloud_auth.rs
Won Park 3ae4225b17 Restrict cloud task credentials to trusted origins (#41403)
## Why

Cloud task requests can use saved ChatGPT credentials, so their destination must be constrained before authentication is loaded or any request is made.

## What changed

- Validate `CODEX_CLOUD_TASKS_BASE_URL` against trusted ChatGPT HTTPS origins on port 443 and reject URLs containing user information, queries, or fragments.
- Disable redirects for cloud task backend and environment-discovery clients so credentials are never forwarded to a redirect destination.
- Normalize the validated base URL before configuring authentication and HTTP clients.

## Testing

Add a CLI integration test that verifies a custom cloud backend is rejected without receiving a request, while the staging origin continues to use explicit token authentication.

GitOrigin-RevId: f0dddf363b4e44ea3530f81d9a817e978c15ead2
2026-08-28 19:44:50 +00:00

92 lines
3.4 KiB
Rust

//! Exercise Cloud credential isolation through the CLI using a synthetic login.
use anyhow::Result;
use predicates::str::contains;
use serde_json::json;
use tempfile::TempDir;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::header;
use wiremock::matchers::path;
#[tokio::test]
async fn cloud_list_only_allows_trusted_credential_destinations() -> Result<()> {
let server = MockServer::start().await;
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
"cli_auth_credentials_store = 'file'\n",
)?;
std::fs::write(
codex_home.path().join("auth.json"),
serde_json::to_vec(&json!({
"auth_mode": "chatgpt",
"tokens": {
"id_token": "eyJhbGciOiJub25lIn0.e30.c2ln",
"access_token": "synthetic-cloud-access-token",
"refresh_token": "synthetic-cloud-refresh-token",
"account_id": "synthetic-cloud-account",
},
"last_refresh": chrono::Utc::now(),
}))?,
)?;
let command = || -> Result<assert_cmd::Command> {
let mut command = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
command
.current_dir(codex_home.path())
.env("CODEX_HOME", codex_home.path())
.env_remove("CODEX_ACCESS_TOKEN")
.env_remove("OPENAI_API_KEY")
.env_remove("CODEX_CLOUD_TASKS_MODE")
.env("NO_PROXY", "127.0.0.1,localhost")
.env("no_proxy", "127.0.0.1,localhost")
.timeout(std::time::Duration::from_secs(15));
Ok(command)
};
// Establish that the isolated profile has a usable ChatGPT login.
command()?
.args(["login", "status"])
.assert()
.success()
.stderr(contains("Logged in using ChatGPT"));
let output = command()?
.env(
"CODEX_CLOUD_TASKS_BASE_URL",
format!("{}/backend-api", server.uri()),
)
.args(["cloud", "list", "--limit", "1", "--json"])
.assert()
.failure()
.get_output()
.stderr
.clone();
insta::assert_snapshot!(String::from_utf8(output)?, @"Error: CODEX_CLOUD_TASKS_BASE_URL must use a trusted HTTPS origin on port 443, without user information, a query, or a fragment; custom backends cannot use saved ChatGPT credentials");
assert!(server.received_requests().await.unwrap().is_empty());
// Staging must reach explicit PAT authentication. Reject the synthetic PAT locally so
// this test never sends a request to the real staging backend.
let auth_server = MockServer::start().await;
Mock::given(path("/v1/user-auth-credential/whoami"))
.and(header("authorization", "Bearer at-synthetic-cloud"))
.respond_with(ResponseTemplate::new(401))
.expect(1)
.mount(&auth_server)
.await;
command()?
.env(
"CODEX_CLOUD_TASKS_BASE_URL",
"https://chatgpt-staging.com/backend-api",
)
.env("CODEX_ACCESS_TOKEN", "at-synthetic-cloud")
.env("CODEX_AUTHAPI_BASE_URL", auth_server.uri())
.args(["cloud", "list", "--limit", "1", "--json"])
.assert()
.failure()
.stderr(contains("Not signed in. Please run 'codex login'"));
auth_server.verify().await;
Ok(())
}