mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
## Why The app server previously rejected OAuth login for enterprise-managed MCP servers. These servers need an explicit sign-in flow that ties saved authorization to the active Codex user and workspace and prevents canceled or superseded attempts from saving credentials. ## What changed - Enable enterprise sign-in through `mcpServer/oauth/login` for configured servers with a connected thread, trusted IdP registration, and `features.use_xaa` enabled. Scope shared enterprise credentials by user, workspace, issuer, and client ID. - Add `loginId` to MCP OAuth login responses and completion notifications, preserving compatibility with older servers that omit it. - Support enterprise login replacement and `account/login/cancel`, wait for callback listener shutdown before replacement, and recheck account and configuration before saving grants. Fence stale attempts across processes and account changes. - Remove shared enterprise authorization during account logout and `codex mcp logout`, including when the feature is disabled. Continue account logout if enterprise cleanup fails, and invalidate MCP runtimes on account changes. Start a fresh session after successful enterprise sign-in to use the saved grant. ## Testing Add regression coverage for login replacement and cancellation, callback port reuse, account changes during setup, cross-process logout invalidation, persisted workspace policy selection, cleanup failures, and responses from older servers without `loginId`. GitOrigin-RevId: 7ed4caaab4659ed1e67064262cf5cee48e3a26ac
674 lines
22 KiB
Rust
674 lines
22 KiB
Rust
//! CLI login commands and their direct-user observability surfaces.
|
|
//!
|
|
//! The TUI path already installs a broader tracing stack with feedback, OpenTelemetry, and other
|
|
//! interactive-session layers. Direct `codex login` intentionally does less: it preserves the
|
|
//! existing stderr/browser UX and adds only a small file-backed tracing layer for login-specific
|
|
//! targets. Keeping that setup local avoids pulling the TUI's session-oriented logging machinery
|
|
//! into a one-shot CLI command while still producing a durable `codex-login.log` artifact that
|
|
//! support can request from users.
|
|
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_core::config::Config;
|
|
use codex_core::config::edit::ConfigEdit;
|
|
use codex_core::config::edit::ConfigEditsBuilder;
|
|
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;
|
|
use codex_login::run_device_code_login;
|
|
use codex_login::run_login_server;
|
|
use codex_mcp::ema_auth_scope;
|
|
use codex_protocol::auth::AuthMode;
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
|
use codex_rmcp_client::EnterpriseOAuthCredentialGuard;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use std::fs::OpenOptions;
|
|
use std::io::IsTerminal;
|
|
use std::io::Read;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use tracing_appender::non_blocking;
|
|
use tracing_appender::non_blocking::WorkerGuard;
|
|
use tracing_subscriber::EnvFilter;
|
|
use tracing_subscriber::Layer;
|
|
use tracing_subscriber::layer::SubscriberExt;
|
|
use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
const CHATGPT_LOGIN_DISABLED_MESSAGE: &str =
|
|
"ChatGPT login is disabled. Use API key login instead.";
|
|
const API_KEY_LOGIN_DISABLED_MESSAGE: &str =
|
|
"API key login is disabled. Use ChatGPT login instead.";
|
|
const ACCESS_TOKEN_LOGIN_DISABLED_MESSAGE: &str =
|
|
"Access token login is disabled. Use API key login instead.";
|
|
const LOGIN_SUCCESS_MESSAGE: &str = "Successfully logged in";
|
|
|
|
/// Installs a small file-backed tracing layer for direct `codex login` flows.
|
|
///
|
|
/// This deliberately duplicates a narrow slice of the TUI logging setup instead of reusing it
|
|
/// wholesale. The TUI stack includes session-oriented layers that are valuable for interactive
|
|
/// runs but unnecessary for a one-shot login command. Keeping the direct CLI path local lets this
|
|
/// command produce a durable `codex-login.log` artifact without coupling it to the TUI's broader
|
|
/// telemetry and feedback initialization.
|
|
fn init_login_file_logging(config: &Config) -> Option<WorkerGuard> {
|
|
let log_dir = match codex_core::config::log_dir(config) {
|
|
Ok(log_dir) => log_dir,
|
|
Err(err) => {
|
|
eprintln!("Warning: failed to resolve login log directory: {err}");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
if let Err(err) = std::fs::create_dir_all(&log_dir) {
|
|
eprintln!(
|
|
"Warning: failed to create login log directory {}: {err}",
|
|
log_dir.display()
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let mut log_file_opts = OpenOptions::new();
|
|
log_file_opts.create(true).append(true);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
log_file_opts.mode(0o600);
|
|
}
|
|
|
|
let log_path = log_dir.join("codex-login.log");
|
|
let log_file = match log_file_opts.open(&log_path) {
|
|
Ok(log_file) => log_file,
|
|
Err(err) => {
|
|
eprintln!(
|
|
"Warning: failed to open login log file {}: {err}",
|
|
log_path.display()
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let (non_blocking, guard) = non_blocking(log_file);
|
|
let env_filter = EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| EnvFilter::new("codex_cli=info,codex_core=info,codex_login=info"));
|
|
let file_layer = tracing_subscriber::fmt::layer()
|
|
.with_writer(non_blocking)
|
|
.with_target(true)
|
|
.with_ansi(false)
|
|
.with_filter(env_filter);
|
|
|
|
// Direct `codex login` otherwise relies on ephemeral stderr and browser output.
|
|
// Persist the same login targets to a file so support can inspect auth failures
|
|
// without reproducing them through TUI or app-server.
|
|
if let Err(err) = tracing_subscriber::registry().with(file_layer).try_init() {
|
|
eprintln!(
|
|
"Warning: failed to initialize login log file {}: {err}",
|
|
log_path.display()
|
|
);
|
|
return None;
|
|
}
|
|
|
|
Some(guard)
|
|
}
|
|
|
|
fn print_login_server_start(actual_port: u16, auth_url: &str) {
|
|
eprintln!(
|
|
"Starting local login server on http://localhost:{actual_port}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{auth_url}\n\nOn a remote or headless machine? Use `codex login --device-auth` instead."
|
|
);
|
|
}
|
|
|
|
async fn clear_existing_auth_before_login(
|
|
codex_home: &Path,
|
|
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
|
auth_route_config: &AuthRouteConfig,
|
|
) {
|
|
if let Err(err) = logout_with_revoke(
|
|
codex_home,
|
|
auth_credentials_store_mode,
|
|
auth_keyring_backend_kind,
|
|
auth_route_config,
|
|
)
|
|
.await
|
|
{
|
|
tracing::warn!("failed to clear existing auth before login: {err}");
|
|
}
|
|
}
|
|
|
|
pub async fn login_with_chatgpt(
|
|
codex_home: PathBuf,
|
|
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
|
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
|
auth_route_config: AuthRouteConfig,
|
|
) -> std::io::Result<()> {
|
|
clear_existing_auth_before_login(
|
|
&codex_home,
|
|
cli_auth_credentials_store_mode,
|
|
auth_keyring_backend_kind,
|
|
&auth_route_config,
|
|
)
|
|
.await;
|
|
|
|
let opts = ServerOptions::new(
|
|
codex_home,
|
|
CLIENT_ID.to_string(),
|
|
forced_chatgpt_workspace_id,
|
|
cli_auth_credentials_store_mode,
|
|
auth_keyring_backend_kind,
|
|
auth_route_config,
|
|
);
|
|
let server = run_login_server(opts)?;
|
|
|
|
print_login_server_start(server.actual_port, &server.auth_url);
|
|
|
|
server.block_until_done().await
|
|
}
|
|
|
|
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting browser login flow");
|
|
|
|
if !config
|
|
.auth_config()
|
|
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
|
{
|
|
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let effective_chatgpt_workspaces = config.auth_config().effective_chatgpt_workspaces();
|
|
match login_with_chatgpt(
|
|
config.codex_home.to_path_buf(),
|
|
effective_chatgpt_workspaces,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
config.auth_route_config(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_login_with_api_key(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
api_key: String,
|
|
) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting api key login flow");
|
|
|
|
if !config
|
|
.auth_config()
|
|
.is_login_method_allowed(ForcedLoginMethod::Api)
|
|
{
|
|
eprintln!("{API_KEY_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
match login_with_api_key(
|
|
&config.codex_home,
|
|
&api_key,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
) {
|
|
Ok(_) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_login_with_access_token(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
access_token: String,
|
|
) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let _login_log_guard = init_login_file_logging(&config);
|
|
tracing::info!("starting access token login flow");
|
|
|
|
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,
|
|
effective_chatgpt_workspaces.as_deref(),
|
|
Some(&config.chatgpt_base_url),
|
|
config.auth_keyring_backend_kind(),
|
|
&auth_route_config,
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in with access token: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn read_api_key_from_stdin() -> String {
|
|
read_stdin_secret(
|
|
"--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`.",
|
|
"Reading API key from stdin...",
|
|
"No API key provided via stdin.",
|
|
)
|
|
}
|
|
|
|
pub fn read_access_token_from_stdin() -> String {
|
|
read_stdin_secret(
|
|
"--with-access-token expects the access token on stdin. Try piping it, e.g. `printenv CODEX_ACCESS_TOKEN | codex login --with-access-token`.",
|
|
"Reading access token from stdin...",
|
|
"No access token provided via stdin.",
|
|
)
|
|
}
|
|
|
|
fn read_stdin_secret(terminal_message: &str, reading_message: &str, empty_message: &str) -> String {
|
|
let mut stdin = std::io::stdin();
|
|
|
|
if stdin.is_terminal() {
|
|
eprintln!("{terminal_message}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
eprintln!("{reading_message}");
|
|
|
|
let mut buffer = String::new();
|
|
if let Err(err) = stdin.read_to_string(&mut buffer) {
|
|
eprintln!("Failed to read stdin: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let secret = buffer.trim().to_string();
|
|
if secret.is_empty() {
|
|
eprintln!("{empty_message}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
secret
|
|
}
|
|
|
|
/// Login using the OAuth device code flow.
|
|
pub async fn run_login_with_device_code(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
issuer_base_url: Option<String>,
|
|
client_id: Option<String>,
|
|
) -> ! {
|
|
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 !config
|
|
.auth_config()
|
|
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
|
{
|
|
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
let auth_route_config = config.auth_route_config();
|
|
clear_existing_auth_before_login(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
&auth_route_config,
|
|
)
|
|
.await;
|
|
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()),
|
|
effective_chatgpt_workspaces,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config,
|
|
);
|
|
if let Some(iss) = issuer_base_url {
|
|
opts.issuer = iss;
|
|
}
|
|
match run_device_code_login(opts).await {
|
|
Ok(()) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in with device code: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Prefers device-code login (with `open_browser = false`) when headless environment is detected, but keeps
|
|
/// `codex login` working in environments where device-code may be disabled/feature-gated.
|
|
/// If `run_device_code_login` returns `ErrorKind::NotFound` ("device-code unsupported"), this
|
|
/// falls back to starting the local browser login server.
|
|
pub async fn run_login_with_device_code_fallback_to_browser(
|
|
cli_config_overrides: CliConfigOverrides,
|
|
issuer_base_url: Option<String>,
|
|
client_id: Option<String>,
|
|
) -> ! {
|
|
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 !config
|
|
.auth_config()
|
|
.is_login_method_allowed(ForcedLoginMethod::Chatgpt)
|
|
{
|
|
eprintln!("{CHATGPT_LOGIN_DISABLED_MESSAGE}");
|
|
std::process::exit(1);
|
|
}
|
|
let auth_route_config = config.auth_route_config();
|
|
clear_existing_auth_before_login(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
&auth_route_config,
|
|
)
|
|
.await;
|
|
|
|
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()),
|
|
effective_chatgpt_workspaces,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
auth_route_config,
|
|
);
|
|
if let Some(iss) = issuer_base_url {
|
|
opts.issuer = iss;
|
|
}
|
|
opts.open_browser = false;
|
|
|
|
match run_device_code_login(opts.clone()).await {
|
|
Ok(()) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
if e.kind() == std::io::ErrorKind::NotFound {
|
|
eprintln!("Device code login is not enabled; falling back to browser login.");
|
|
match run_login_server(opts) {
|
|
Ok(server) => {
|
|
print_login_server_start(server.actual_port, &server.auth_url);
|
|
match server.block_until_done().await {
|
|
Ok(()) => {
|
|
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("Error logging in with device code: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
|
|
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
|
|
{
|
|
Ok(Some(auth)) => match auth.auth_mode() {
|
|
AuthMode::ApiKey => match auth.get_token() {
|
|
Ok(api_key) => {
|
|
eprintln!("Logged in using an API key - {}", safe_format_key(&api_key));
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Unexpected error retrieving API key: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
},
|
|
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => {
|
|
eprintln!("Logged in using ChatGPT");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::Headers => {
|
|
unreachable!("header auth cannot be loaded from auth storage")
|
|
}
|
|
AuthMode::AgentIdentity => {
|
|
eprintln!("Logged in using access token");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::PersonalAccessToken => {
|
|
eprintln!("Logged in using personal access token");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::BedrockApiKey => {
|
|
eprintln!("Logged in using Amazon Bedrock API key");
|
|
std::process::exit(0);
|
|
}
|
|
AuthMode::BedrockAccessKeys => {
|
|
eprintln!("Logged in using Amazon Bedrock AWS access keys");
|
|
std::process::exit(0);
|
|
}
|
|
},
|
|
Ok(None) => {
|
|
eprintln!("Not logged in");
|
|
std::process::exit(1);
|
|
}
|
|
Err(err) => {
|
|
eprintln!("Error checking login status: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
let auth_route_config = config.auth_route_config();
|
|
|
|
// Cleanup is independent of the current feature gate. Retain the grant lock
|
|
// through account removal so another process cannot commit a late login.
|
|
let enterprise_guard = async {
|
|
let Some(profile) = &config.mcp_enterprise_managed_auth else {
|
|
return Ok::<_, anyhow::Error>(None);
|
|
};
|
|
let auth = config
|
|
.auth_config()
|
|
.load_auth(/*enable_codex_api_key_env*/ false)
|
|
.await?;
|
|
let Some(scope) = ema_auth_scope(auth.as_ref()) else {
|
|
return Ok(None);
|
|
};
|
|
EnterpriseOAuthCredentialGuard::acquire(
|
|
&profile.idp.credential_name(&scope),
|
|
&profile.idp.issuer,
|
|
config.auth_keyring_backend_kind(),
|
|
)
|
|
.await
|
|
.map(Some)
|
|
}
|
|
.await;
|
|
let cleanup_failed = match &enterprise_guard {
|
|
Ok(Some(guard)) => guard.delete_tokens().is_err(),
|
|
Ok(None) => false,
|
|
Err(_) => true,
|
|
};
|
|
if cleanup_failed {
|
|
eprintln!("Warning: failed to remove enterprise authorization; continuing account logout");
|
|
}
|
|
|
|
let logged_out = match logout_with_revoke(
|
|
&config.codex_home,
|
|
config.cli_auth_credentials_store_mode,
|
|
config.auth_keyring_backend_kind(),
|
|
&auth_route_config,
|
|
)
|
|
.await
|
|
{
|
|
Ok(logged_out) => logged_out,
|
|
Err(err) => {
|
|
eprintln!("Error logging out: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
drop(enterprise_guard);
|
|
|
|
let cleared_bedrock_config =
|
|
if let Some(paths) = ConfigEditsBuilder::bedrock_provider_config_paths_to_clear(&config) {
|
|
let edits = paths
|
|
.into_iter()
|
|
.map(|segments| ConfigEdit::ClearPath { segments });
|
|
if let Err(err) = ConfigEditsBuilder::for_config(&config)
|
|
.with_edits(edits)
|
|
.apply()
|
|
.await
|
|
{
|
|
eprintln!("Error clearing Amazon Bedrock configuration after logout: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if logged_out || cleared_bedrock_config {
|
|
eprintln!("Successfully logged out");
|
|
} else {
|
|
eprintln!("Not logged in");
|
|
}
|
|
std::process::exit(0);
|
|
}
|
|
|
|
async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config {
|
|
let cli_overrides = match cli_config_overrides.parse_overrides() {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("Error parsing -c overrides: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
match Config::load_with_cli_overrides(cli_overrides).await {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn safe_format_key(key: &str) -> String {
|
|
if key.len() <= 13 {
|
|
return "***".to_string();
|
|
}
|
|
let prefix = &key[..8];
|
|
let suffix = &key[key.len() - 5..];
|
|
format!("{prefix}***{suffix}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_login::AuthKeyringBackendKind;
|
|
use codex_login::load_auth_dot_json;
|
|
use codex_login::login_with_api_key;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::tempdir;
|
|
|
|
use super::clear_existing_auth_before_login;
|
|
use super::safe_format_key;
|
|
|
|
#[tokio::test]
|
|
async fn clears_existing_auth_before_login() {
|
|
let codex_home = tempdir().expect("create temporary Codex home");
|
|
login_with_api_key(
|
|
codex_home.path(),
|
|
"sk-existing",
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.expect("save existing auth");
|
|
|
|
clear_existing_auth_before_login(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
&codex_login::test_support::transport_default_auth_route_config(),
|
|
)
|
|
.await;
|
|
|
|
let auth = load_auth_dot_json(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.expect("load auth after cleanup");
|
|
assert_eq!(auth, None);
|
|
}
|
|
|
|
#[test]
|
|
fn formats_long_key() {
|
|
let key = "sk-proj-1234567890ABCDE";
|
|
assert_eq!(safe_format_key(key), "sk-proj-***ABCDE");
|
|
}
|
|
|
|
#[test]
|
|
fn short_key_returns_stars() {
|
|
let key = "sk-proj-12345";
|
|
assert_eq!(safe_format_key(key), "***");
|
|
}
|
|
}
|