Enforce managed authentication requirements locally (#37132)

## Why

Authentication restrictions must apply before stored or environment-provided credentials can be used, including during bootstrap before cloud requirements are fetched.

## What changed

- Add local `requirements.toml` allowlists for login methods and ChatGPT workspaces. Ignore these fields in cloud-provided requirements.
- Combine managed workspace allowlists with existing workspace restrictions by intersection, and fail closed when the resulting policy permits no usable login method.
- Centralize policy checks in the authentication manager so CLI, TUI, app-server, external-auth, and credential-loading paths consistently reject disallowed authentication before token hydration or network requests.

## Testing

- Cover policy composition, workspace intersection, invalid stored and external credentials, bootstrap enforcement, and login endpoint restrictions.

GitOrigin-RevId: efef22b248f3c3333e9aa55423e539efa2d2dd48
This commit is contained in:
Abhinav
2026-08-05 18:03:40 +00:00
committed by copyberry
parent 4cb8676d3a
commit 2994f545a7
36 changed files with 1121 additions and 328 deletions

View File

@@ -1,9 +1,8 @@
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadOptions;
use codex_core::config::bootstrap_auth_config;
use codex_core::config::load_config_toml_with_layer_stack;
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
use codex_core::config::resolve_bootstrap_auth_route_config;
use codex_utils_absolute_path::AbsolutePathBuf;
use toml::Value as TomlValue;
@@ -41,28 +40,9 @@ pub(super) async fn bootstrap_cloud_config_bundle(
},
)
.await?;
let bootstrap_config_toml = &bootstrap_config.config_toml;
let auth_route_config = resolve_bootstrap_auth_route_config(
bootstrap_config_toml,
bootstrap_config
.config_layer_stack
.requirements()
.feature_requirements
.as_ref(),
)?;
Ok(cloud_config_bundle_loader_for_storage(
codex_home.to_path_buf(),
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)?,
/*enable_codex_api_key_env*/ false,
bootstrap_config_toml
.cli_auth_credentials_store
.unwrap_or_default(),
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
bootstrap_config_toml
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
auth_route_config,
)
.await)
}

View File

@@ -12,7 +12,6 @@ use codex_core::config::Config;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthRouteConfig;
use codex_login::CLIENT_ID;
use codex_login::CodexAuth;
use codex_login::ServerOptions;
use codex_login::login_with_access_token;
use codex_login::login_with_api_key;
@@ -169,15 +168,18 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting browser login flow");
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
if !config
.auth_config()
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
{
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
match login_with_chatgpt(
config.codex_home.to_path_buf(),
forced_chatgpt_workspace_id,
effective_chatgpt_workspaces,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
config.auth_route_config(),
@@ -203,7 +205,10 @@ pub async fn run_login_with_api_key(
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting api key login flow");
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Chatgpt)) {
if !config
.auth_config()
.is_login_method_allowed(ForcedLoginMethod::Api)
{
eprintln!("{API_KEY_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
@@ -233,17 +238,21 @@ pub async fn run_login_with_access_token(
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting access token login flow");
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
if !config
.auth_config()
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
{
eprintln!("{ACCESS_TOKEN_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
let auth_route_config = config.auth_route_config();
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
match login_with_access_token(
&config.codex_home,
&access_token,
config.cli_auth_credentials_store_mode,
config.forced_chatgpt_workspace_id.as_deref(),
effective_chatgpt_workspaces.as_deref(),
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
&auth_route_config,
@@ -311,7 +320,10 @@ pub async fn run_login_with_device_code(
let config = load_config_or_exit(cli_config_overrides).await;
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting device code login flow");
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
if !config
.auth_config()
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
{
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
@@ -323,11 +335,11 @@ pub async fn run_login_with_device_code(
&auth_route_config,
)
.await;
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
let mut opts = ServerOptions::new(
config.codex_home.to_path_buf(),
client_id.unwrap_or(CLIENT_ID.to_string()),
forced_chatgpt_workspace_id,
effective_chatgpt_workspaces,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config,
@@ -359,7 +371,10 @@ pub async fn run_login_with_device_code_fallback_to_browser(
let config = load_config_or_exit(cli_config_overrides).await;
let _login_log_guard = init_login_file_logging(&config);
tracing::info!("starting login flow with device code fallback");
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
if !config
.auth_config()
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
{
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
std::process::exit(1);
}
@@ -372,11 +387,11 @@ pub async fn run_login_with_device_code_fallback_to_browser(
)
.await;
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
let mut opts = ServerOptions::new(
config.codex_home.to_path_buf(),
client_id.unwrap_or(CLIENT_ID.to_string()),
forced_chatgpt_workspace_id,
effective_chatgpt_workspaces,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
auth_route_config,
@@ -423,16 +438,11 @@ 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;
let auth_route_config = config.auth_route_config();
match CodexAuth::from_auth_storage(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
&auth_route_config,
)
.await
match config
.auth_config()
.load_auth(/*enable_codex_api_key_env*/ false)
.await
{
Ok(Some(auth)) => match auth.auth_mode() {
AuthMode::ApiKey => match auth.get_token() {
@@ -469,8 +479,8 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
eprintln!("Not logged in");
std::process::exit(1);
}
Err(e) => {
eprintln!("Error checking login status: {e}");
Err(err) => {
eprintln!("Error checking login status: {err}");
std::process::exit(1);
}
}
@@ -513,7 +523,13 @@ async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config
};
match Config::load_with_cli_overrides(cli_overrides).await {
Ok(config) => config,
Ok(config) => match config.auth_config().validate() {
Ok(()) => config,
Err(e) => {
eprintln!("Error loading configuration: {e}");
std::process::exit(1);
}
},
Err(e) => {
eprintln!("Error loading configuration: {e}");
std::process::exit(1);

View File

@@ -1821,16 +1821,19 @@ async fn load_exec_server_remote_auth_provider(
use_agent_identity_auth: bool,
) -> anyhow::Result<codex_api::SharedAuthProvider> {
if use_agent_identity_auth {
let agent_identity_jwt = read_codex_access_token_from_env().ok_or_else(|| {
read_codex_access_token_from_env().ok_or_else(|| {
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
})?;
let auth_route_config = config.auth_route_config();
let auth = CodexAuth::from_agent_identity_jwt(
&agent_identity_jwt,
Some(&config.chatgpt_base_url),
&auth_route_config,
)
.await?;
let auth = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
.await
.auth()
.await
.ok_or_else(|| anyhow::anyhow!("Agent Identity authentication is unavailable"))?;
if !matches!(auth, CodexAuth::AgentIdentity(_)) {
anyhow::bail!(
"CODEX_ACCESS_TOKEN did not provide permitted Agent Identity authentication"
);
}
return Ok(codex_model_provider::auth_provider_from_auth(&auth));
}

View File

@@ -6,10 +6,9 @@ use codex_config::ConfigLoadOptions;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::LoaderOverrides;
use codex_core::config::bootstrap_auth_config;
use codex_core::config::find_codex_home;
use codex_core::config::load_config_toml_with_layer_stack;
use codex_core::config::resolve_bootstrap_auth_keyring_backend_kind;
use codex_core::config::resolve_bootstrap_auth_route_config;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_cli::CliConfigOverrides;
@@ -34,29 +33,10 @@ pub(super) async fn load_mcp_config(
)
.await
.context("failed to load bootstrap configuration")?;
let bootstrap_config_toml = &bootstrap_config.config_toml;
let auth_route_config = resolve_bootstrap_auth_route_config(
bootstrap_config_toml,
bootstrap_config
.config_layer_stack
.requirements()
.feature_requirements
.as_ref(),
)
.context("failed to resolve cloud configuration authentication")?;
let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
codex_home.to_path_buf(),
bootstrap_auth_config(codex_home.as_path(), &bootstrap_config)
.context("failed to resolve cloud configuration authentication")?,
/*enable_codex_api_key_env*/ false,
bootstrap_config_toml
.cli_auth_credentials_store
.unwrap_or_default(),
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)
.context("failed to resolve cloud configuration credential storage")?,
bootstrap_config_toml
.chatgpt_base_url
.clone()
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
auth_route_config,
)
.await;

View File

@@ -18,8 +18,7 @@ use codex_core_plugins::marketplace::MarketplacePluginAuthPolicy;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_login::CodexAuth;
use codex_login::auth::read_codex_api_key_from_env;
use codex_login::AuthManager;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_protocol::auth::AuthMode;
@@ -600,22 +599,11 @@ async fn load_plugin_command_context(
}
pub(crate) async fn load_cli_auth_mode(config: &Config) -> Option<AuthMode> {
if let Some(api_key) = read_codex_api_key_from_env() {
return Some(CodexAuth::from_api_key(&api_key).api_auth_mode());
}
let auth_route_config = config.auth_route_config();
CodexAuth::from_auth_storage(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Some(&config.chatgpt_base_url),
config.auth_keyring_backend_kind(),
&auth_route_config,
)
.await
.ok()
.flatten()
.map(|auth| auth.api_auth_mode())
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ true)
.await
.auth()
.await
.map(|auth| auth.api_auth_mode())
}
struct PluginSelection {

View File

@@ -66,6 +66,21 @@ fn login_with_api_key_reads_stdin_and_writes_auth_json() -> Result<()> {
Ok(())
}
#[test]
fn login_status_reports_auth_storage_errors() -> Result<()> {
let codex_home = TempDir::new()?;
write_file_auth_config(codex_home.path())?;
std::fs::write(codex_home.path().join("auth.json"), "{invalid json")?;
codex_command(codex_home.path())?
.args(["login", "status"])
.assert()
.failure()
.stderr(contains("Error checking login status:"));
Ok(())
}
#[test]
fn login_with_access_token_rejects_invalid_jwt() -> Result<()> {
let codex_home = TempDir::new()?;