mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Fail closed when workload identity initialization fails (#38424)
## What changed - Treat workload identity environment markers as an explicit authentication selection, even when another process credential is present. - Return initialization errors from `AuthManager` and propagate them through commands and services instead of continuing with an unusable authentication state. - Make `codex login status` validate workload identity, keep the TUI on an embedded app server for local workload identity, and reject workload identity in `codex mcp-server`, where it is unsupported. ## Testing - Cover workload identity precedence and partial configuration errors. - Verify login status reports an unreadable identity assertion and app-server routing enforces the supported workload identity topology. GitOrigin-RevId: efc6b6b4cd4d61652617de82aaa3d7ffc75d6618
This commit is contained in:
@@ -44,7 +44,7 @@ pub(super) async fn bootstrap_cloud_config_bundle(
|
||||
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -350,8 +350,9 @@ async fn build_report(
|
||||
let config_result = load_config(root_config_overrides, interactive, arg0_paths).await;
|
||||
match &config_result {
|
||||
Ok(config) => {
|
||||
let auth_manager =
|
||||
let auth_manager_result =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await;
|
||||
let auth_manager = auth_manager_result.as_ref().ok().cloned();
|
||||
let reachability_plan = provider_reachability_plan(config);
|
||||
let (
|
||||
config_check,
|
||||
@@ -370,13 +371,27 @@ async fn build_report(
|
||||
reachability_check,
|
||||
) = tokio::join!(
|
||||
async { run_sync_check("config", progress.clone(), || config_check(config)) },
|
||||
async { run_sync_check("auth", progress.clone(), || auth_check(config)) },
|
||||
async {
|
||||
run_sync_check("auth", progress.clone(), || match &auth_manager_result {
|
||||
Ok(_) => auth_check(config),
|
||||
Err(error) => DoctorCheck::new(
|
||||
"auth.load",
|
||||
"auth",
|
||||
CheckStatus::Fail,
|
||||
"authentication could not be initialized",
|
||||
)
|
||||
.detail(error.to_string())
|
||||
.remediation(
|
||||
"Fix the reported authentication error, then rerun codex doctor.",
|
||||
),
|
||||
})
|
||||
},
|
||||
async { run_sync_check("updates", progress.clone(), || updates_check(config)) },
|
||||
async { run_sync_check("network", progress.clone(), network_check) },
|
||||
run_async_check(
|
||||
"websocket",
|
||||
progress.clone(),
|
||||
websocket_reachability_check(config, Some(auth_manager)),
|
||||
websocket_reachability_check(config, auth_manager),
|
||||
),
|
||||
run_async_check("MCP", progress.clone(), mcp_check(config)),
|
||||
async {
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::AuthRouteConfig;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::is_workload_identity_selected;
|
||||
use codex_login::login_with_access_token;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::logout_with_revoke;
|
||||
@@ -439,8 +441,21 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
let config = load_config_or_exit(cli_config_overrides).await;
|
||||
|
||||
match config
|
||||
.auth_config()
|
||||
if is_workload_identity_selected() {
|
||||
match AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await {
|
||||
Ok(_) => {
|
||||
eprintln!("Logged in using workload identity");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error checking login status: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let auth_config = config.auth_config();
|
||||
match auth_config
|
||||
.load_auth(/*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1826,7 +1826,7 @@ async fn load_exec_server_remote_auth_provider(
|
||||
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
|
||||
})?;
|
||||
let auth = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
|
||||
.await
|
||||
.await?
|
||||
.auth()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("Agent Identity authentication is unavailable"))?;
|
||||
@@ -1914,7 +1914,7 @@ async fn load_exec_server_remote_auth(
|
||||
missing_auth_error: &'static str,
|
||||
) -> anyhow::Result<codex_login::CodexAuth> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await?;
|
||||
|
||||
let auth = match auth_manager.auth().await {
|
||||
Some(auth) => auth,
|
||||
@@ -2076,7 +2076,7 @@ async fn run_debug_prompt_input_command(
|
||||
config.codex_home.clone(),
|
||||
));
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
|
||||
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
|
||||
codex_git_attribution::install(
|
||||
&mut extensions,
|
||||
@@ -2122,7 +2122,7 @@ async fn run_debug_models_command(
|
||||
.build()
|
||||
.await?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await;
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ true).await?;
|
||||
let models_manager = build_models_manager(&config, auth_manager);
|
||||
models_manager
|
||||
.raw_model_catalog(
|
||||
|
||||
@@ -212,7 +212,7 @@ async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceAr
|
||||
.await
|
||||
.context("failed to load configuration")?;
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await?);
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let marketplace_listing = manager
|
||||
.discover_marketplaces_for_config(&plugins_input, &[])
|
||||
|
||||
@@ -523,14 +523,14 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_mcp_manager(config: &Config) -> McpManager {
|
||||
async fn load_mcp_manager(config: &Config) -> Result<McpManager> {
|
||||
let plugins_manager = Arc::new(plugins_manager_for_config(config));
|
||||
plugins_manager.set_auth_mode(load_cli_auth_mode(config).await);
|
||||
McpManager::new(plugins_manager)
|
||||
plugins_manager.set_auth_mode(load_cli_auth_mode(config).await?);
|
||||
Ok(McpManager::new(plugins_manager))
|
||||
}
|
||||
|
||||
async fn run_login(config: &Config, login_args: LoginArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let LoginArgs {
|
||||
@@ -599,7 +599,7 @@ async fn run_login(config: &Config, login_args: LoginArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_logout(config: &Config, logout_args: LogoutArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let LogoutArgs { name } = logout_args;
|
||||
@@ -629,9 +629,9 @@ async fn run_logout(config: &Config, logout_args: LogoutArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_list(config: &Config, list_args: ListArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await;
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true).await?;
|
||||
let auth = auth_manager.auth().await;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
let effective_mcp_servers = mcp_manager.effective_servers(config, auth.as_ref()).await;
|
||||
@@ -894,7 +894,7 @@ async fn run_list(config: &Config, list_args: ListArgs) -> Result<()> {
|
||||
}
|
||||
|
||||
async fn run_get(config: &Config, get_args: GetArgs) -> Result<()> {
|
||||
let mcp_manager = load_mcp_manager(config).await;
|
||||
let mcp_manager = load_mcp_manager(config).await?;
|
||||
let mcp_servers = mcp_manager.configured_servers(config).await;
|
||||
|
||||
let Some(server) = mcp_servers.get(&get_args.name) else {
|
||||
|
||||
@@ -38,7 +38,8 @@ pub(super) async fn load_mcp_config(
|
||||
.context("failed to resolve cloud configuration authentication")?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.context("failed to initialize cloud configuration authentication")?;
|
||||
|
||||
ConfigBuilder::default()
|
||||
.codex_home(codex_home.to_path_buf())
|
||||
|
||||
@@ -591,7 +591,7 @@ async fn load_plugin_command_context(
|
||||
.context("failed to load configuration")?;
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let manager = plugins_manager_for_config(&config);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await);
|
||||
manager.set_auth_mode(load_cli_auth_mode(&config).await?);
|
||||
Ok(PluginCommandContext {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
plugins_input,
|
||||
@@ -599,12 +599,14 @@ async fn load_plugin_command_context(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn load_cli_auth_mode(config: &Config) -> Option<AuthMode> {
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true)
|
||||
.await
|
||||
.auth()
|
||||
.await
|
||||
.map(|auth| auth.api_auth_mode())
|
||||
pub(crate) async fn load_cli_auth_mode(config: &Config) -> Result<Option<AuthMode>> {
|
||||
Ok(
|
||||
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true)
|
||||
.await?
|
||||
.auth()
|
||||
.await
|
||||
.map(|auth| auth.api_auth_mode()),
|
||||
)
|
||||
}
|
||||
|
||||
struct PluginSelection {
|
||||
|
||||
@@ -9,7 +9,10 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CODEX_ACCESS_TOKEN_ENV_VAR;
|
||||
use codex_login::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_protocol::shell_environment::OPENAI_FEDERATION_RULE_ID_ENV_VAR;
|
||||
use codex_protocol::shell_environment::OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR;
|
||||
use predicates::str::contains;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
@@ -81,6 +84,24 @@ fn login_status_reports_auth_storage_errors() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_status_validates_configured_workload_identity() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_file_auth_config(codex_home.path())?;
|
||||
let missing_assertion = codex_home.path().join("missing-identity-token");
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.env_remove(CODEX_ACCESS_TOKEN_ENV_VAR)
|
||||
.env(OPENAI_FEDERATION_RULE_ID_ENV_VAR, "rule-test")
|
||||
.env(OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR, &missing_assertion)
|
||||
.args(["login", "status"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("could not read workload identity assertion file"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_access_token_rejects_invalid_jwt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
Reference in New Issue
Block a user