| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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_protocol::auth::AuthMode; |
| use codex_protocol::config_types::ForcedLoginMethod; |
| 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"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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); |
|
|
| |
| |
| |
| 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 |
| } |
|
|
| |
| 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); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| 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, 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( 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(); |
|
|
| 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); |
| } |
| }; |
|
|
| 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), "***"); |
| } |
| } |
|
|