Skip to content
Open
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
9 changes: 7 additions & 2 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ use codex_feedback::CodexFeedback;
use codex_goal_extension::GoalService;
use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager;
use codex_login::default_client::CodexClientIdentity;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;
Expand Down Expand Up @@ -287,7 +288,7 @@ impl MessageProcessor {
Some(analytics_events_client.clone()),
Arc::clone(&thread_store),
codex_core::local_agent_graph_store_from_state_db(state_db.as_ref()),
installation_id,
installation_id.clone(),
Some(app_server_attestation_provider(
outgoing.clone(),
thread_state_manager.clone(),
Expand Down Expand Up @@ -318,6 +319,7 @@ impl MessageProcessor {
outgoing.clone(),
Arc::clone(&config),
config_manager.clone(),
installation_id,
);
let apps_processor = AppsRequestProcessor::new(
auth_manager.clone(),
Expand Down Expand Up @@ -1315,8 +1317,11 @@ impl MessageProcessor {
.await
}
ClientRequest::LoginAccount { params, .. } => {
let client_identity = app_server_client_name
.zip(client_version)
.map(|(name, version)| CodexClientIdentity::new(name, version));
self.account_processor
.login_account(request_id.clone(), params)
.login_account(request_id.clone(), params, client_identity)
.await
}
ClientRequest::LogoutAccount { .. } => {
Expand Down
33 changes: 28 additions & 5 deletions codex-rs/app-server/src/request_processors/account_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use super::*;
use crate::auth_mode::auth_mode_to_api;
use crate::external_auth::ExternalAuthBridge;
use chrono::DateTime;
use codex_login::DeviceAuthMetadata;
use codex_login::default_client::CodexClientIdentity;

mod rate_limit_resets;

Expand Down Expand Up @@ -71,6 +73,7 @@ pub(crate) struct AccountRequestProcessor {
config: Arc<Config>,
config_manager: ConfigManager,
active_login: Arc<Mutex<Option<ActiveLogin>>>,
installation_id: String,
}

impl AccountRequestProcessor {
Expand All @@ -80,6 +83,7 @@ impl AccountRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
config: Arc<Config>,
config_manager: ConfigManager,
installation_id: String,
) -> Self {
Self {
auth_manager,
Expand All @@ -88,15 +92,19 @@ impl AccountRequestProcessor {
config,
config_manager,
active_login: Arc::new(Mutex::new(None)),
installation_id,
}
}

pub(crate) async fn login_account(
&self,
request_id: ConnectionRequestId,
params: LoginAccountParams,
client_identity: Option<CodexClientIdentity>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.login_v2(request_id, params).await.map(|()| None)
self.login_v2(request_id, params, client_identity)
.await
.map(|()| None)
}

pub(crate) async fn logout_account(
Expand Down Expand Up @@ -249,6 +257,7 @@ impl AccountRequestProcessor {
&self,
request_id: ConnectionRequestId,
params: LoginAccountParams,
client_identity: Option<CodexClientIdentity>,
) -> Result<(), JSONRPCErrorError> {
match params {
LoginAccountParams::ApiKey { api_key } => {
Expand Down Expand Up @@ -278,7 +287,8 @@ impl AccountRequestProcessor {
.await;
}
LoginAccountParams::ChatgptDeviceCode => {
self.login_chatgpt_device_code_v2(request_id).await;
self.login_chatgpt_device_code_v2(request_id, client_identity)
.await;
}
LoginAccountParams::ChatgptAuthTokens {
access_token,
Expand Down Expand Up @@ -378,6 +388,10 @@ impl AccountRequestProcessor {
open_browser: false,
codex_streamlined_login,
login_success_page,
device_auth_metadata: DeviceAuthMetadata {
installation_id: Some(self.installation_id.clone()),
..Default::default()
},
..LoginServerOptions::new(
config.codex_home.to_path_buf(),
oauth_client_id(),
Expand All @@ -394,6 +408,7 @@ impl AccountRequestProcessor {
&& !issuer.trim().is_empty()
{
opts.issuer = issuer;
opts.device_auth_metadata.installation_id = None;
}
if let LoginSuccessPage::Hosted { url, .. } = &mut opts.login_success_page
&& let Ok(open_app_url) = std::env::var(LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR)
Expand Down Expand Up @@ -500,20 +515,28 @@ impl AccountRequestProcessor {
})
}

async fn login_chatgpt_device_code_v2(&self, request_id: ConnectionRequestId) {
let result = self.login_chatgpt_device_code_response().await;
async fn login_chatgpt_device_code_v2(
&self,
request_id: ConnectionRequestId,
client_identity: Option<CodexClientIdentity>,
) {
let result = self
.login_chatgpt_device_code_response(client_identity)
.await;
self.outgoing.send_result(request_id, result).await;
}

async fn login_chatgpt_device_code_response(
&self,
client_identity: Option<CodexClientIdentity>,
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
let opts = self
let mut opts = self
.login_chatgpt_common(
/*codex_streamlined_login*/ false,
LoginSuccessPage::default(),
)
.await?;
opts.device_auth_metadata.client_identity = client_identity;
let device_code = request_device_code(&opts)
.await
.map_err(Self::login_chatgpt_device_code_start_error)?;
Expand Down
36 changes: 35 additions & 1 deletion codex-rs/app-server/tests/suite/v2/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use codex_app_server_protocol::CancelLoginAccountResponse;
use codex_app_server_protocol::CancelLoginAccountStatus;
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::GetAuthStatusParams;
Expand Down Expand Up @@ -1172,7 +1173,17 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
])
.build()
.await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let client_name = "device-auth-app-server-test";
let client_version = "1.2.3";
timeout(
DEFAULT_READ_TIMEOUT,
mcp.initialize_with_client_info(ClientInfo {
name: client_name.to_string(),
title: None,
version: client_version.to_string(),
}),
)
.await??;

let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
let resp: JSONRPCResponse = timeout(
Expand Down Expand Up @@ -1220,6 +1231,29 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
codex_home.path().join("auth.json").exists(),
"auth.json should be created when device code login succeeds"
);
let requests = mock_server.received_requests().await.unwrap();
let device_requests: Vec<_> = requests
.iter()
.filter(|request| request.url.path().starts_with("/api/accounts/deviceauth/"))
.collect();
assert_eq!(device_requests.len(), 2);
for request in device_requests {
assert_eq!(
request
.headers
.get("originator")
.and_then(|value| value.to_str().ok()),
Some(client_name)
);
assert!(
request
.headers
.get("user-agent")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.contains(&format!("({client_name}; {client_version})")))
);
assert!(request.headers.get("x-codex-installation-id").is_none());
}
Ok(())
}

Expand Down
22 changes: 22 additions & 0 deletions codex-rs/cli/src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,22 @@ fn read_stdin_secret(terminal_message: &str, reading_message: &str, empty_messag
secret
}

async fn resolve_device_auth_installation_id(
config: &Config,
issuer_base_url: Option<&str>,
) -> Option<String> {
if issuer_base_url.is_some() {
return None;
}
match codex_core::resolve_installation_id(&config.codex_home).await {
Ok(installation_id) => Some(installation_id),
Err(err) => {
eprintln!("Error resolving installation ID: {err}");
std::process::exit(1);
}
}
}

/// Login using the OAuth device code flow.
pub async fn run_login_with_device_code(
cli_config_overrides: CliConfigOverrides,
Expand All @@ -316,6 +332,8 @@ pub async fn run_login_with_device_code(
std::process::exit(1);
}
let auth_route_config = config.auth_route_config();
let installation_id =
resolve_device_auth_installation_id(&config, issuer_base_url.as_deref()).await;
clear_existing_auth_before_login(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Expand All @@ -335,6 +353,7 @@ pub async fn run_login_with_device_code(
if let Some(iss) = issuer_base_url {
opts.issuer = iss;
}
opts.device_auth_metadata.installation_id = installation_id;
match run_device_code_login(opts).await {
Ok(()) => {
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
Expand Down Expand Up @@ -364,6 +383,8 @@ pub async fn run_login_with_device_code_fallback_to_browser(
std::process::exit(1);
}
let auth_route_config = config.auth_route_config();
let installation_id =
resolve_device_auth_installation_id(&config, issuer_base_url.as_deref()).await;
clear_existing_auth_before_login(
&config.codex_home,
config.cli_auth_credentials_store_mode,
Expand All @@ -384,6 +405,7 @@ pub async fn run_login_with_device_code_fallback_to_browser(
if let Some(iss) = issuer_base_url {
opts.issuer = iss;
}
opts.device_auth_metadata.installation_id = installation_id;
opts.open_browser = false;

match run_device_code_login(opts.clone()).await {
Expand Down
21 changes: 21 additions & 0 deletions codex-rs/cli/tests/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,27 @@ async fn device_login_revokes_existing_auth_before_requesting_new_tokens() -> Re
"client_id": CLIENT_ID,
})
);
let expected_originator =
std::env::var(codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR)
.unwrap_or_else(|_| codex_login::default_client::DEFAULT_ORIGINATOR.to_string());
for request in &requests[1..=2] {
assert_eq!(
request
.headers
.get("originator")
.and_then(|value| value.to_str().ok()),
Some(expected_originator.as_str())
);
assert!(
request
.headers
.get("user-agent")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with(&format!("{expected_originator}/")))
);
assert!(request.headers.get("x-codex-installation-id").is_none());
}
assert!(!codex_home.path().join("installation_id").exists());

let auth = read_auth_json(codex_home.path())?;
assert_eq!(auth["tokens"]["refresh_token"], "new-refresh");
Expand Down
51 changes: 44 additions & 7 deletions codex-rs/login/src/auth/default_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ pub struct Originator {
pub value: String,
pub header_value: HeaderValue,
}

#[derive(Debug, Clone)]
pub struct CodexClientIdentity {
originator: String,
user_agent_suffix: String,
}

impl CodexClientIdentity {
pub fn new(client_name: String, client_version: String) -> Self {
Self {
originator: client_name.clone(),
user_agent_suffix: format!("{client_name}; {client_version}"),
}
}

pub(crate) fn headers(&self) -> HeaderMap {
default_headers_for_identity(Some(self))
}
}
static ORIGINATOR: LazyLock<RwLock<Option<Originator>>> = LazyLock::new(|| RwLock::new(None));
static REQUIREMENTS_RESIDENCY: LazyLock<RwLock<Option<ResidencyRequirement>>> =
LazyLock::new(|| RwLock::new(None));
Expand Down Expand Up @@ -137,9 +156,15 @@ pub fn is_first_party_chat_originator(originator_value: &str) -> bool {
}

pub fn get_codex_user_agent() -> String {
get_codex_user_agent_for_identity(/*identity*/ None)
}

fn get_codex_user_agent_for_identity(identity: Option<&CodexClientIdentity>) -> String {
let build_version = env!("CARGO_PKG_VERSION");
let os_info = os_info::get();
let originator = originator();
let originator = identity.map_or_else(originator, |identity| {
get_originator_value(Some(identity.originator.clone()))
});
let prefix = format!(
"{}/{build_version} ({} {}; {}) {}",
originator.value.as_str(),
Expand All @@ -148,10 +173,15 @@ pub fn get_codex_user_agent() -> String {
os_info.architecture().unwrap_or("unknown"),
user_agent()
);
let suffix = USER_AGENT_SUFFIX
.lock()
.ok()
.and_then(|guard| guard.clone());
let global_suffix = || {
USER_AGENT_SUFFIX
.lock()
.ok()
.and_then(|guard| guard.clone())
};
let suffix = identity
.map(|identity| identity.user_agent_suffix.clone())
.or_else(global_suffix);
let suffix = suffix
.as_deref()
.map(str::trim)
Expand Down Expand Up @@ -313,9 +343,16 @@ fn auth_http_client_factory(auth_route_config: Option<&AuthRouteConfig>) -> Http
}

pub fn default_headers() -> HeaderMap {
default_headers_for_identity(/*identity*/ None)
}

fn default_headers_for_identity(identity: Option<&CodexClientIdentity>) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("originator", originator().header_value);
if let Ok(user_agent) = HeaderValue::from_str(&get_codex_user_agent()) {
let originator = identity.map_or_else(originator, |identity| {
get_originator_value(Some(identity.originator.clone()))
});
headers.insert("originator", originator.header_value);
if let Ok(user_agent) = HeaderValue::from_str(&get_codex_user_agent_for_identity(identity)) {
headers.insert(USER_AGENT, user_agent);
}
if let Ok(guard) = REQUIREMENTS_RESIDENCY.read()
Expand Down
Loading
Loading