Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions codex-rs/cli/src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,67 @@ pub async fn run_login_with_device_code(
}
}

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;
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,
config.cli_auth_credentials_store_mode,
);
if let Some(iss) = issuer_base_url {
opts.issuer = iss;
}
opts.open_browser = false;

match run_device_code_login(opts.clone()).await {
Ok(()) => {
eprintln!("Successfully logged in");
std::process::exit(0);
}
Err(e) => {
if e.to_string().contains("device code login is not enabled") {
eprintln!("Device code login is not enabled; falling back to browser login.");
match run_login_server(opts) {
Ok(server) => {
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,
);
match server.block_until_done().await {
Ok(()) => {
eprintln!("Successfully logged in");
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;

Expand Down
31 changes: 31 additions & 0 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use codex_cli::login::run_login_status;
use codex_cli::login::run_login_with_api_key;
use codex_cli::login::run_login_with_chatgpt;
use codex_cli::login::run_login_with_device_code;
use codex_cli::login::run_login_with_device_code_fallback_to_browser;
use codex_cli::login::run_logout;
use codex_cloud_tasks::Cli as CloudTasksCli;
use codex_common::CliConfigOverrides;
Expand Down Expand Up @@ -533,6 +534,13 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
} else if login_cli.with_api_key {
let api_key = read_api_key_from_stdin();
run_login_with_api_key(login_cli.config_overrides, api_key).await;
} else if is_headless_environment() {
run_login_with_device_code_fallback_to_browser(
login_cli.config_overrides,
login_cli.issuer_base_url,
login_cli.client_id,
)
.await;
} else {
run_login_with_chatgpt(login_cli.config_overrides).await;
}
Expand Down Expand Up @@ -773,6 +781,29 @@ fn print_completion(cmd: CompletionCommand) {
generate(cmd.shell, &mut app, name, &mut std::io::stdout());
}

fn env_var_set(key: &str) -> bool {
std::env::var(key).is_ok_and(|v| !v.trim().is_empty())
}

fn is_headless_environment() -> bool {
if env_var_set("CI")
|| env_var_set("SSH_CONNECTION")
|| env_var_set("SSH_CLIENT")
|| env_var_set("SSH_TTY")
{
return true;
}

#[cfg(target_os = "linux")]
{
if !env_var_set("DISPLAY") && !env_var_set("WAYLAND_DISPLAY") {
return true;
}
}

false
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
49 changes: 38 additions & 11 deletions codex-rs/login/src/device_code_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const ANSI_BLUE: &str = "\x1b[94m";
const ANSI_GRAY: &str = "\x1b[90m";
const ANSI_RESET: &str = "\x1b[0m";

#[derive(Debug, Clone)]
pub struct DeviceCode {
pub verification_url: String,
pub user_code: String,
device_auth_id: String,
interval: u64,
}

#[derive(Deserialize)]
struct UserCodeResp {
device_auth_id: String,
Expand Down Expand Up @@ -137,33 +145,45 @@ async fn poll_for_token(
}
}

fn print_device_code_prompt(code: &str) {
fn print_device_code_prompt(verification_url: &str, code: &str) {
let version = env!("CARGO_PKG_VERSION");
println!(
"\nWelcome to Codex [v{ANSI_GRAY}{version}{ANSI_RESET}]\n{ANSI_GRAY}OpenAI's command-line coding agent{ANSI_RESET}\n\
\nFollow these steps to sign in with ChatGPT using device code authorization:\n\
\n1. Open this link in your browser and sign in to your account\n {ANSI_BLUE}https://auth.openai.com/codex/device{ANSI_RESET}\n\
\n1. Open this link in your browser and sign in to your account\n {ANSI_BLUE}{verification_url}{ANSI_RESET}\n\
\n2. Enter this one-time code {ANSI_GRAY}(expires in 15 minutes){ANSI_RESET}\n {ANSI_BLUE}{code}{ANSI_RESET}\n\
\n{ANSI_GRAY}Device codes are a common phishing target. Never share this code.{ANSI_RESET}\n",
version = env!("CARGO_PKG_VERSION"),
code = code
);
}

/// Full device code login flow.
pub async fn run_device_code_login(opts: ServerOptions) -> std::io::Result<()> {
pub async fn request_device_code(opts: &ServerOptions) -> std::io::Result<DeviceCode> {
let client = reqwest::Client::new();
let base_url = opts.issuer.trim_end_matches('/');
let api_base_url = format!("{}/api/accounts", opts.issuer.trim_end_matches('/'));
let api_base_url = format!("{base_url}/api/accounts");
let uc = request_user_code(&client, &api_base_url, &opts.client_id).await?;

print_device_code_prompt(&uc.user_code);
Ok(DeviceCode {
verification_url: format!("{base_url}/codex/device"),
user_code: uc.user_code,
device_auth_id: uc.device_auth_id,
interval: uc.interval,
})
}

pub async fn complete_device_code_login(
opts: ServerOptions,
device_code: DeviceCode,
) -> std::io::Result<()> {
let client = reqwest::Client::new();
let base_url = opts.issuer.trim_end_matches('/');
let api_base_url = format!("{base_url}/api/accounts");

let code_resp = poll_for_token(
&client,
&api_base_url,
&uc.device_auth_id,
&uc.user_code,
uc.interval,
&device_code.device_auth_id,
&device_code.user_code,
device_code.interval,
)
.await?;

Expand Down Expand Up @@ -200,3 +220,10 @@ pub async fn run_device_code_login(opts: ServerOptions) -> std::io::Result<()> {
)
.await
}

/// Full device code login flow.
pub async fn run_device_code_login(opts: ServerOptions) -> std::io::Result<()> {
let device_code = request_device_code(&opts).await?;
print_device_code_prompt(&device_code.verification_url, &device_code.user_code);
complete_device_code_login(opts, device_code).await
}
3 changes: 3 additions & 0 deletions codex-rs/login/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ mod device_code_auth;
mod pkce;
mod server;

pub use device_code_auth::DeviceCode;
pub use device_code_auth::complete_device_code_login;
pub use device_code_auth::request_device_code;
pub use device_code_auth::run_device_code_login;
pub use server::LoginServer;
pub use server::ServerOptions;
Expand Down
Loading
Loading