mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
feat: use encrypted local secrets for CLI auth (#27539)
## Why Windows Credential Manager limits generic credential blobs to 2,560 bytes. Large serialized ChatGPT auth payloads can exceed that limit, so keyring-mode CLI auth needs a backend that keeps only the encryption key in the OS keyring and stores the payload in Codex's encrypted local-secrets file. This is the third PR in the encrypted-auth stack: 1. #27504 — feature and config selection 2. #27535 — auth-specific local-secrets namespaces 3. This PR — CLI auth implementation and activation 4. MCP OAuth implementation and activation ## What Changed - Added encrypted CLI-auth storage using the `CliAuth` secrets namespace. - Preserved direct keyring storage for platforms/configurations where it remains selected. - Selected the backend consistently for login, logout, refresh, device-code login, auth loading, and login restrictions. - Threaded resolved bootstrap/full config through CLI, exec, TUI, app-server account handling, cloud config, and cloud tasks. - Removed stale `auth.json` fallback data after successful encrypted saves and removed encrypted, direct-keyring, and fallback data during logout. - Added storage and integration coverage for both direct and encrypted keyring modes. MCP OAuth persistence is intentionally left to the next PR. ## Validation - `just test -p codex-login` — 131 passed - `just test -p codex-cli` — 280 passed - `just test -p codex-app-server v2::account` — 25 passed - `just test -p codex-cloud-config service` — 21 passed, 7 skipped - `just fix -p codex-login` - `just fix -p codex-cli` - `just fmt`
This commit is contained in:
1 parent
576f603440
commit
56c97e3b5c
37 files changed
+857
-164
No files matched your search
+15
-9
@@ -7,7 +7,9 @@ use crate::legacy_core::check_execpolicy_for_warnings;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;
|
||||
use crate::legacy_core::config::ConfigTomlLoadResult;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
use crate::legacy_core::format_exec_policy_error_with_source;
|
||||
@@ -937,7 +939,7 @@ pub async fn run_main(
|
||||
loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
|
||||
let bootstrap_config_toml = load_config_toml_or_exit(
|
||||
let bootstrap_config = load_bootstrap_config_or_exit(
|
||||
&codex_home,
|
||||
config_cwd.as_ref(),
|
||||
cli_kv_overrides.clone(),
|
||||
@@ -946,6 +948,7 @@ pub async fn run_main(
|
||||
CloudConfigBundleLoader::default(),
|
||||
)
|
||||
.await;
|
||||
let bootstrap_config_toml = &bootstrap_config.config_toml;
|
||||
|
||||
let chatgpt_base_url = bootstrap_config_toml
|
||||
.chatgpt_base_url
|
||||
@@ -957,6 +960,7 @@ pub async fn run_main(
|
||||
bootstrap_config_toml
|
||||
.cli_auth_credentials_store
|
||||
.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await;
|
||||
@@ -969,12 +973,12 @@ pub async fn run_main(
|
||||
|
||||
let mut manually_selected_oss_provider = None;
|
||||
let model_provider_override = if cli.oss {
|
||||
let config_toml_with_cloud_config;
|
||||
let bootstrap_config_with_cloud_config;
|
||||
let config_toml_for_oss = if cli.oss_provider.is_none() {
|
||||
// The first load intentionally skips cloud config so we can read
|
||||
// auth/base-url settings needed to fetch the bundle. If OSS mode
|
||||
// needs a default provider from config, reload with the bundle.
|
||||
config_toml_with_cloud_config = load_config_toml_or_exit(
|
||||
bootstrap_config_with_cloud_config = load_bootstrap_config_or_exit(
|
||||
&codex_home,
|
||||
config_cwd.as_ref(),
|
||||
cli_kv_overrides.clone(),
|
||||
@@ -983,9 +987,9 @@ pub async fn run_main(
|
||||
cloud_config_bundle.clone(),
|
||||
)
|
||||
.await;
|
||||
&config_toml_with_cloud_config
|
||||
&bootstrap_config_with_cloud_config.config_toml
|
||||
} else {
|
||||
&bootstrap_config_toml
|
||||
bootstrap_config_toml
|
||||
};
|
||||
|
||||
let resolved = resolve_oss_provider(cli.oss_provider.as_deref(), config_toml_for_oss);
|
||||
@@ -1151,6 +1155,7 @@ pub async fn run_main(
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
keyring_backend_kind: config.auth_keyring_backend_kind(),
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
@@ -1419,6 +1424,7 @@ async fn run_ratatui_app(
|
||||
initial_config.codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
initial_config.cli_auth_credentials_store_mode,
|
||||
initial_config.auth_keyring_backend_kind(),
|
||||
initial_config.chatgpt_base_url.clone(),
|
||||
)
|
||||
.await;
|
||||
@@ -1917,15 +1923,15 @@ async fn load_config_or_exit_with_fallback_cwd(
|
||||
}
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
async fn load_config_toml_or_exit(
|
||||
async fn load_bootstrap_config_or_exit(
|
||||
codex_home: &Path,
|
||||
cwd: Option<&AbsolutePathBuf>,
|
||||
cli_kv_overrides: Vec<(String, codex_config::TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
strict_config: bool,
|
||||
cloud_config_bundle: CloudConfigBundleLoader,
|
||||
) -> codex_config::config_toml::ConfigToml {
|
||||
match load_config_as_toml_with_cli_and_load_options(
|
||||
) -> ConfigTomlLoadResult {
|
||||
match load_config_toml_with_layer_stack(
|
||||
codex_home,
|
||||
cwd,
|
||||
cli_kv_overrides,
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::load_auth_dot_json;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -18,9 +19,13 @@ pub(crate) fn load_local_chatgpt_auth(
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
forced_chatgpt_workspace_id: Option<&[String]>,
|
||||
) -> Result<LocalChatgptAuth, String> {
|
||||
let auth = load_auth_dot_json(codex_home, auth_credentials_store_mode)
|
||||
.map_err(|err| format!("failed to load local auth: {err}"))?
|
||||
.ok_or_else(|| "no local auth available".to_string())?;
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home,
|
||||
auth_credentials_store_mode,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.map_err(|err| format!("failed to load local auth: {err}"))?
|
||||
.ok_or_else(|| "no local auth available".to_string())?;
|
||||
if matches!(auth.auth_mode, Some(AuthMode::ApiKey)) || auth.openai_api_key.is_some() {
|
||||
return Err("local auth is not a ChatGPT login".to_string());
|
||||
}
|
||||
@@ -112,8 +117,13 @@ mod tests {
|
||||
personal_access_token: None,
|
||||
bedrock_api_key: None,
|
||||
};
|
||||
save_auth(codex_home, &auth, AuthCredentialsStoreMode::File)
|
||||
.expect("chatgpt auth should save");
|
||||
save_auth(
|
||||
codex_home,
|
||||
&auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("chatgpt auth should save");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -162,6 +172,7 @@ mod tests {
|
||||
bedrock_api_key: None,
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("api key auth should save");
|
||||
|
||||
|
||||
@@ -1014,6 +1014,7 @@ mod tests {
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
@@ -1037,6 +1038,7 @@ mod tests {
|
||||
codex_home_path.clone(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
"https://chatgpt.com/backend-api/".to_string(),
|
||||
)
|
||||
.await,
|
||||
|
||||
@@ -11,7 +11,8 @@ use crate::Cli;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options;
|
||||
use crate::legacy_core::config::load_config_toml_with_layer_stack;
|
||||
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
use crate::legacy_core::config::resolve_oss_provider;
|
||||
use crate::legacy_core::config::resolve_profile_v2_config_path;
|
||||
use codex_app_server_protocol::Thread as AppServerThread;
|
||||
@@ -310,7 +311,7 @@ async fn start_app_server_for_archive_command(
|
||||
loader_overrides.user_config_profile = Some(profile_v2.clone());
|
||||
}
|
||||
|
||||
let config_toml = load_config_as_toml_with_cli_and_load_options(
|
||||
let bootstrap_config = load_config_toml_with_layer_stack(
|
||||
codex_home.as_path(),
|
||||
config_cwd.as_ref(),
|
||||
cli_kv_overrides.clone(),
|
||||
@@ -322,6 +323,7 @@ async fn start_app_server_for_archive_command(
|
||||
)
|
||||
.await
|
||||
.wrap_err("failed to load config.toml")?;
|
||||
let config_toml = &bootstrap_config.config_toml;
|
||||
let chatgpt_base_url = config_toml
|
||||
.chatgpt_base_url
|
||||
.clone()
|
||||
@@ -330,12 +332,13 @@ async fn start_app_server_for_archive_command(
|
||||
codex_home.to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config_toml.cli_auth_credentials_store.unwrap_or_default(),
|
||||
resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
|
||||
chatgpt_base_url,
|
||||
)
|
||||
.await;
|
||||
|
||||
let model_provider = if cli.oss {
|
||||
resolve_oss_provider(cli.oss_provider.as_deref(), &config_toml)
|
||||
resolve_oss_provider(cli.oss_provider.as_deref(), config_toml)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user