mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
Reference in New Issue
Block a user