codex: simplify device auth metadata plumbing

This commit is contained in:
Eric Traut
2026-07-08 15:43:42 -07:00
parent f0401629d3
commit d35d0c2b4b
10 changed files with 86 additions and 118 deletions
+5 -6
View File
@@ -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;
@@ -1316,13 +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,
app_server_client_name,
client_version,
)
.login_account(request_id.clone(), params, client_identity)
.await
}
ClientRequest::LogoutAccount { .. } => {
@@ -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;
@@ -98,10 +100,9 @@ impl AccountRequestProcessor {
&self,
request_id: ConnectionRequestId,
params: LoginAccountParams,
app_server_client_name: Option<String>,
client_version: Option<String>,
client_identity: Option<CodexClientIdentity>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.login_v2(request_id, params, app_server_client_name, client_version)
self.login_v2(request_id, params, client_identity)
.await
.map(|()| None)
}
@@ -256,8 +257,7 @@ impl AccountRequestProcessor {
&self,
request_id: ConnectionRequestId,
params: LoginAccountParams,
app_server_client_name: Option<String>,
client_version: Option<String>,
client_identity: Option<CodexClientIdentity>,
) -> Result<(), JSONRPCErrorError> {
match params {
LoginAccountParams::ApiKey { api_key } => {
@@ -287,12 +287,8 @@ impl AccountRequestProcessor {
.await;
}
LoginAccountParams::ChatgptDeviceCode => {
self.login_chatgpt_device_code_v2(
request_id,
app_server_client_name,
client_version,
)
.await;
self.login_chatgpt_device_code_v2(request_id, client_identity)
.await;
}
LoginAccountParams::ChatgptAuthTokens {
access_token,
@@ -392,7 +388,10 @@ impl AccountRequestProcessor {
open_browser: false,
codex_streamlined_login,
login_success_page,
device_auth_installation_id: Some(self.installation_id.clone()),
device_auth_metadata: DeviceAuthMetadata {
installation_id: Some(self.installation_id.clone()),
..Default::default()
},
..LoginServerOptions::new(
config.codex_home.to_path_buf(),
oauth_client_id(),
@@ -409,7 +408,7 @@ impl AccountRequestProcessor {
&& !issuer.trim().is_empty()
{
opts.issuer = issuer;
opts.device_auth_installation_id = None;
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)
@@ -519,19 +518,17 @@ impl AccountRequestProcessor {
async fn login_chatgpt_device_code_v2(
&self,
request_id: ConnectionRequestId,
app_server_client_name: Option<String>,
client_version: Option<String>,
client_identity: Option<CodexClientIdentity>,
) {
let result = self
.login_chatgpt_device_code_response(app_server_client_name, client_version)
.login_chatgpt_device_code_response(client_identity)
.await;
self.outgoing.send_result(request_id, result).await;
}
async fn login_chatgpt_device_code_response(
&self,
app_server_client_name: Option<String>,
client_version: Option<String>,
client_identity: Option<CodexClientIdentity>,
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
let mut opts = self
.login_chatgpt_common(
@@ -539,12 +536,7 @@ impl AccountRequestProcessor {
LoginSuccessPage::default(),
)
.await?;
if let (Some(client_name), Some(client_version)) = (app_server_client_name, client_version)
{
opts.device_auth_client_identity = Some(
codex_login::default_client::CodexClientIdentity::new(client_name, client_version),
);
}
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)?;
@@ -3,7 +3,6 @@ use std::sync::atomic::Ordering;
use axum::http::HeaderValue;
use codex_analytics::AppServerRpcTransport;
use codex_login::default_client::CodexClientIdentity;
use codex_login::default_client::SetOriginatorError;
use codex_login::default_client::USER_AGENT_SUFFIX;
use codex_login::default_client::get_codex_user_agent;
@@ -87,9 +86,8 @@ impl InitializeRequestProcessor {
"Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value."
)));
}
let client_identity = CodexClientIdentity::new(name.clone(), version.clone());
let originator = client_identity.originator().to_string();
let user_agent_suffix = client_identity.user_agent_suffix().to_string();
let originator = name.clone();
let user_agent_suffix = format!("{name}; {version}");
let mutates_global_identity = !NON_ORIGINATING_CLIENT_NAMES.contains(&name.as_str());
let codex_home = self.config.codex_home.clone();
if session
+22 -24
View File
@@ -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,
@@ -316,17 +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 = if issuer_base_url.is_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);
}
}
} else {
None
};
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,
@@ -346,7 +353,7 @@ pub async fn run_login_with_device_code(
if let Some(iss) = issuer_base_url {
opts.issuer = iss;
}
opts.device_auth_installation_id = installation_id;
opts.device_auth_metadata.installation_id = installation_id;
match run_device_code_login(opts).await {
Ok(()) => {
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
@@ -376,17 +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 = if issuer_base_url.is_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);
}
}
} else {
None
};
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,
@@ -407,7 +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_installation_id = installation_id;
opts.device_auth_metadata.installation_id = installation_id;
opts.open_browser = false;
match run_device_code_login(opts.clone()).await {
@@ -65,14 +65,6 @@ impl CodexClientIdentity {
}
}
pub fn originator(&self) -> &str {
&self.originator
}
pub fn user_agent_suffix(&self) -> &str {
&self.user_agent_suffix
}
pub(crate) fn headers(&self) -> HeaderMap {
default_headers_for_identity(Some(self))
}
+22 -27
View File
@@ -6,9 +6,9 @@ use serde::de::{self};
use std::time::Duration;
use std::time::Instant;
use crate::default_client::CodexClientIdentity;
use crate::default_client::build_default_auth_reqwest_client;
use crate::pkce::PkceCodes;
use crate::server::DeviceAuthMetadata;
use crate::server::ServerOptions;
use std::io;
@@ -17,6 +17,19 @@ const ANSI_GRAY: &str = "\x1b[90m";
const ANSI_RESET: &str = "\x1b[0m";
const X_CODEX_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id";
fn with_device_auth_headers(
mut request: reqwest::RequestBuilder,
metadata: &DeviceAuthMetadata,
) -> reqwest::RequestBuilder {
if let Some(installation_id) = metadata.installation_id.as_deref() {
request = request.header(X_CODEX_INSTALLATION_ID_HEADER, installation_id);
}
if let Some(client_identity) = metadata.client_identity.as_ref() {
request = request.headers(client_identity.headers());
}
request
}
#[derive(Debug, Clone)]
pub struct DeviceCode {
pub verification_url: String,
@@ -65,24 +78,18 @@ async fn request_user_code(
client: &reqwest::Client,
auth_base_url: &str,
client_id: &str,
installation_id: Option<&str>,
client_identity: Option<&CodexClientIdentity>,
metadata: &DeviceAuthMetadata,
) -> std::io::Result<UserCodeResp> {
let url = format!("{auth_base_url}/deviceauth/usercode");
let body = serde_json::to_string(&UserCodeReq {
client_id: client_id.to_string(),
})
.map_err(std::io::Error::other)?;
let mut request = client
let request = client
.post(url)
.header("Content-Type", "application/json")
.body(body);
if let Some(installation_id) = installation_id {
request = request.header(X_CODEX_INSTALLATION_ID_HEADER, installation_id);
}
if let Some(client_identity) = client_identity {
request = request.headers(client_identity.headers());
}
let request = with_device_auth_headers(request, metadata);
let resp = request.send().await.map_err(std::io::Error::other)?;
if !resp.status().is_success() {
@@ -110,8 +117,7 @@ async fn poll_for_token(
device_auth_id: &str,
user_code: &str,
interval: u64,
installation_id: Option<&str>,
client_identity: Option<&CodexClientIdentity>,
metadata: &DeviceAuthMetadata,
) -> std::io::Result<CodeSuccessResp> {
let url = format!("{auth_base_url}/deviceauth/token");
let max_wait = Duration::from_secs(15 * 60);
@@ -123,16 +129,11 @@ async fn poll_for_token(
user_code: user_code.to_string(),
})
.map_err(std::io::Error::other)?;
let mut request = client
let request = client
.post(&url)
.header("Content-Type", "application/json")
.body(body);
if let Some(installation_id) = installation_id {
request = request.header(X_CODEX_INSTALLATION_ID_HEADER, installation_id);
}
if let Some(client_identity) = client_identity {
request = request.headers(client_identity.headers());
}
let request = with_device_auth_headers(request, metadata);
let resp = request.send().await.map_err(std::io::Error::other)?;
let status = resp.status();
@@ -176,13 +177,11 @@ pub async fn request_device_code(opts: &ServerOptions) -> std::io::Result<Device
// paths are not resolved separately.
let client = build_default_auth_reqwest_client(base_url, opts.auth_route_config.as_ref())?;
let api_base_url = format!("{base_url}/api/accounts");
let installation_id = opts.device_auth_installation_id.as_deref();
let uc = request_user_code(
&client,
&api_base_url,
&opts.client_id,
installation_id,
opts.device_auth_client_identity.as_ref(),
&opts.device_auth_metadata,
)
.await?;
@@ -201,17 +200,13 @@ pub async fn complete_device_code_login(
let base_url = opts.issuer.trim_end_matches('/');
let client = build_default_auth_reqwest_client(base_url, opts.auth_route_config.as_ref())?;
let api_base_url = format!("{base_url}/api/accounts");
let installation_id = opts.device_auth_installation_id.as_deref();
let client_identity = opts.device_auth_client_identity.as_ref();
let code_resp = poll_for_token(
&client,
&api_base_url,
&device_code.device_auth_id,
&device_code.user_code,
device_code.interval,
installation_id,
client_identity,
&opts.device_auth_metadata,
)
.await?;
+1
View File
@@ -14,6 +14,7 @@ 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::DeviceAuthMetadata;
pub use server::LoginServer;
pub use server::ServerOptions;
pub use server::ShutdownHandle;
+8 -6
View File
@@ -64,6 +64,12 @@ static LOGIN_ERROR_PAGE_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
.unwrap_or_else(|err| panic!("login error page template must parse: {err}"))
});
#[derive(Debug, Clone, Default)]
pub struct DeviceAuthMetadata {
pub installation_id: Option<String>,
pub client_identity: Option<CodexClientIdentity>,
}
/// Options for launching the local login callback server.
#[derive(Debug, Clone)]
pub struct ServerOptions {
@@ -79,10 +85,7 @@ pub struct ServerOptions {
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
pub auth_keyring_backend_kind: AuthKeyringBackendKind,
pub auth_route_config: Option<AuthRouteConfig>,
/// Installation ID attached only to device authorization requests sent to a trusted issuer.
pub device_auth_installation_id: Option<String>,
/// Per-caller identity for device authorization requests, when available.
pub device_auth_client_identity: Option<CodexClientIdentity>,
pub device_auth_metadata: DeviceAuthMetadata,
}
impl ServerOptions {
@@ -108,8 +111,7 @@ impl ServerOptions {
cli_auth_credentials_store_mode,
auth_keyring_backend_kind,
auth_route_config,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: DeviceAuthMetadata::default(),
}
}
}
@@ -146,7 +146,7 @@ async fn device_code_login_integration_succeeds() -> anyhow::Result<()> {
let issuer = mock_server.uri();
let installation_id = "123e4567-e89b-42d3-a456-426614174099";
let mut opts = server_opts(&codex_home, issuer, AuthCredentialsStoreMode::File);
opts.device_auth_installation_id = Some(installation_id.to_string());
opts.device_auth_metadata.installation_id = Some(installation_id.to_string());
run_device_code_login(opts)
.await
+9 -18
View File
@@ -125,8 +125,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
codex_home: server_home,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -192,8 +191,7 @@ async fn hosted_login_redirects_to_configured_open_app_url() -> Result<()> {
codex_home: tmp.path().to_path_buf(),
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -247,8 +245,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
codex_home: server_home,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -292,8 +289,7 @@ async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result<
codex_home,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -338,8 +334,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
codex_home: codex_home.clone(),
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -403,8 +398,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
codex_home: codex_home.clone(),
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -476,8 +470,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<
codex_home: codex_home.clone(),
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
@@ -628,8 +621,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
codex_home: first_codex_home,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer: issuer.clone(),
port: 0,
@@ -654,8 +646,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
codex_home: second_codex_home,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
auth_route_config: None,
device_auth_installation_id: None,
device_auth_client_identity: None,
device_auth_metadata: Default::default(),
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: login_port,