mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
This PR adds support for configs to specify a forced login method (chatgpt or api) as well as a forced chatgpt account id. This lets enterprises uses [managed configs](https://developers.openai.com/codex/security#managed-configuration) to force all employees to use their company's workspace instead of their own or any other. When a workspace id is set, a query param is sent to the login flow which auto-selects the given workspace or errors if the user isn't a member of it. This PR is large but a large % of it is tests, wiring, and required formatting changes. API login with chatgpt forced <img width="1592" height="116" alt="CleanShot 2025-10-19 at 22 40 04" src="https://github.com/user-attachments/assets/560c6bb4-a20a-4a37-95af-93df39d057dd" /> ChatGPT login with api forced <img width="1018" height="100" alt="CleanShot 2025-10-19 at 22 40 29" src="https://github.com/user-attachments/assets/d010bbbb-9c8d-4227-9eda-e55bf043b4af" /> Onboarding with api forced <img width="892" height="460" alt="CleanShot 2025-10-19 at 22 41 02" src="https://github.com/user-attachments/assets/cc0ed45c-b257-4d62-a32e-6ca7514b5edd" /> Onboarding with ChatGPT forced <img width="1154" height="426" alt="CleanShot 2025-10-19 at 22 41 27" src="https://github.com/user-attachments/assets/41c41417-dc68-4bb4-b3e7-3b7769f7e6a1" /> Logging in with the wrong workspace <img width="2222" height="84" alt="CleanShot 2025-10-19 at 22 42 31" src="https://github.com/user-attachments/assets/0ff4222c-f626-4dd3-b035-0b7fe998a046" />
234 lines
6.9 KiB
Rust
234 lines
6.9 KiB
Rust
use codex_app_server_protocol::AuthMode;
|
|
use codex_common::CliConfigOverrides;
|
|
use codex_core::CodexAuth;
|
|
use codex_core::auth::CLIENT_ID;
|
|
use codex_core::auth::login_with_api_key;
|
|
use codex_core::auth::logout;
|
|
use codex_core::config::Config;
|
|
use codex_core::config::ConfigOverrides;
|
|
use codex_login::ServerOptions;
|
|
use codex_login::run_device_code_login;
|
|
use codex_login::run_login_server;
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
|
use std::io::IsTerminal;
|
|
use std::io::Read;
|
|
use std::path::PathBuf;
|
|
|
|
pub async fn login_with_chatgpt(
|
|
codex_home: PathBuf,
|
|
forced_chatgpt_workspace_id: Option<String>,
|
|
) -> std::io::Result<()> {
|
|
let opts = ServerOptions::new(
|
|
codex_home,
|
|
CLIENT_ID.to_string(),
|
|
forced_chatgpt_workspace_id,
|
|
);
|
|
let server = run_login_server(opts)?;
|
|
|
|
eprintln!(
|
|
"Starting local login server on http://localhost:{}.\nIf your browser did not open, navigate to this URL to authenticate:\n\n{}",
|
|
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;
|
|
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
|
eprintln!("ChatGPT login is disabled. Use API key login instead.");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
|
|
|
match login_with_chatgpt(config.codex_home, forced_chatgpt_workspace_id).await {
|
|
Ok(_) => {
|
|
eprintln!("Successfully logged in");
|
|
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;
|
|
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Chatgpt)) {
|
|
eprintln!("API key login is disabled. Use ChatGPT login instead.");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
match login_with_api_key(&config.codex_home, &api_key) {
|
|
Ok(_) => {
|
|
eprintln!("Successfully logged in");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging in: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn read_api_key_from_stdin() -> String {
|
|
let mut stdin = std::io::stdin();
|
|
|
|
if stdin.is_terminal() {
|
|
eprintln!(
|
|
"--with-api-key expects the API key on stdin. Try piping it, e.g. `printenv OPENAI_API_KEY | codex login --with-api-key`."
|
|
);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
eprintln!("Reading API key from stdin...");
|
|
|
|
let mut buffer = String::new();
|
|
if let Err(err) = stdin.read_to_string(&mut buffer) {
|
|
eprintln!("Failed to read API key from stdin: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let api_key = buffer.trim().to_string();
|
|
if api_key.is_empty() {
|
|
eprintln!("No API key provided via stdin.");
|
|
std::process::exit(1);
|
|
}
|
|
|
|
api_key
|
|
}
|
|
|
|
/// 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;
|
|
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
|
|
eprintln!("ChatGPT login is disabled. Use API key login instead.");
|
|
std::process::exit(1);
|
|
}
|
|
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
|
|
let mut opts = ServerOptions::new(
|
|
config.codex_home,
|
|
client_id.unwrap_or(CLIENT_ID.to_string()),
|
|
forced_chatgpt_workspace_id,
|
|
);
|
|
if let Some(iss) = issuer_base_url {
|
|
opts.issuer = iss;
|
|
}
|
|
match run_device_code_login(opts).await {
|
|
Ok(()) => {
|
|
eprintln!("Successfully logged in");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
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;
|
|
|
|
match CodexAuth::from_codex_home(&config.codex_home) {
|
|
Ok(Some(auth)) => match auth.mode {
|
|
AuthMode::ApiKey => match auth.get_token().await {
|
|
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 => {
|
|
eprintln!("Logged in using ChatGPT");
|
|
std::process::exit(0);
|
|
}
|
|
},
|
|
Ok(None) => {
|
|
eprintln!("Not logged in");
|
|
std::process::exit(1);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error checking login status: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
|
|
let config = load_config_or_exit(cli_config_overrides).await;
|
|
|
|
match logout(&config.codex_home) {
|
|
Ok(true) => {
|
|
eprintln!("Successfully logged out");
|
|
std::process::exit(0);
|
|
}
|
|
Ok(false) => {
|
|
eprintln!("Not logged in");
|
|
std::process::exit(0);
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error logging out: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
let config_overrides = ConfigOverrides::default();
|
|
match Config::load_with_cli_overrides(cli_overrides, config_overrides).await {
|
|
Ok(config) => config,
|
|
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 super::safe_format_key;
|
|
|
|
#[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), "***");
|
|
}
|
|
}
|