diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts index caa03b896b..692648702e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/LoginAccountParams.ts @@ -6,7 +6,7 @@ export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type /** * Optional auth session logging ID propagated to authapi for analytics correlation. */ -authSessionLoggingId?: string, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens", +authSessionLoggingId?: string | null, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens", /** * Access token (JWT) supplied by the client. * This token is used for backend API requests and email extraction. diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 564dfe95ae..ea3ec9e2d8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -60,7 +60,7 @@ pub enum LoginAccountParams { codex_streamlined_login: bool, /// Optional auth session logging ID propagated to authapi for analytics correlation. #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[ts(optional = nullable)] auth_session_logging_id: Option, }, #[serde(rename = "chatgptDeviceCode")] diff --git a/codex-rs/login/src/device_code_auth.rs b/codex-rs/login/src/device_code_auth.rs index 4b9cb7c321..a337026469 100644 --- a/codex-rs/login/src/device_code_auth.rs +++ b/codex-rs/login/src/device_code_auth.rs @@ -199,6 +199,7 @@ pub async fn complete_device_code_login( &redirect_uri, &pkce, &code_resp.authorization_code, + opts.auth_session_logging_id.as_deref(), ) .await .map_err(|err| std::io::Error::other(format!("device code exchange failed: {err}")))?; diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index ca3b11dfa7..39b64562e5 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -338,8 +338,15 @@ async fn process_request( } }; - match exchange_code_for_tokens(&opts.issuer, &opts.client_id, redirect_uri, pkce, &code) - .await + match exchange_code_for_tokens( + &opts.issuer, + &opts.client_id, + redirect_uri, + pkce, + &code, + opts.auth_session_logging_id.as_deref(), + ) + .await { Ok(tokens) => { if let Err(message) = ensure_workspace_allowed( @@ -355,9 +362,14 @@ async fn process_request( ); } // Obtain API key via token-exchange and persist - let api_key = obtain_api_key(&opts.issuer, &opts.client_id, &tokens.id_token) - .await - .ok(); + let api_key = obtain_api_key( + &opts.issuer, + &opts.client_id, + &tokens.id_token, + opts.auth_session_logging_id.as_deref(), + ) + .await + .ok(); if let Err(err) = persist_tokens_async( &opts.codex_home, api_key.clone(), @@ -530,6 +542,16 @@ fn build_authorize_url( format!("{issuer}/oauth/authorize?{qs}") } +fn with_auth_session_logging_id(mut body: String, auth_session_logging_id: Option<&str>) -> String { + if let Some(auth_session_logging_id) = auth_session_logging_id + .filter(|auth_session_logging_id| !auth_session_logging_id.is_empty()) + { + body.push_str("&auth_session_logging_id="); + body.push_str(&urlencoding::encode(auth_session_logging_id)); + } + body +} + fn generate_state() -> String { let mut bytes = [0u8; 32]; rand::rng().fill_bytes(&mut bytes); @@ -729,6 +751,7 @@ pub(crate) async fn exchange_code_for_tokens( redirect_uri: &str, pkce: &PkceCodes, code: &str, + auth_session_logging_id: Option<&str>, ) -> io::Result { #[derive(serde::Deserialize)] struct TokenResponse { @@ -745,16 +768,20 @@ pub(crate) async fn exchange_code_for_tokens( redirect_uri = %redirect_uri, "starting oauth token exchange" ); - let resp = client - .post(token_endpoint) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(format!( + let body = with_auth_session_logging_id( + format!( "grant_type=authorization_code&code={}&redirect_uri={}&client_id={}&code_verifier={}", urlencoding::encode(code), urlencoding::encode(redirect_uri), urlencoding::encode(client_id), urlencoding::encode(&pkce.code_verifier) - )) + ), + auth_session_logging_id, + ); + let resp = client + .post(token_endpoint) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(body) .send() .await; let resp = match resp { @@ -1134,6 +1161,7 @@ pub(crate) async fn obtain_api_key( issuer: &str, client_id: &str, id_token: &str, + auth_session_logging_id: Option<&str>, ) -> io::Result { // Token exchange for an API key access token #[derive(serde::Deserialize)] @@ -1142,17 +1170,21 @@ pub(crate) async fn obtain_api_key( } let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?; let token_endpoint = format!("{}/oauth/token", issuer.trim_end_matches('/')); - let resp = client - .post(token_endpoint) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(format!( + let body = with_auth_session_logging_id( + format!( "grant_type={}&client_id={}&requested_token={}&subject_token={}&subject_token_type={}", urlencoding::encode("urn:ietf:params:oauth:grant-type:token-exchange"), urlencoding::encode(client_id), urlencoding::encode("openai-api-key"), urlencoding::encode(id_token), urlencoding::encode("urn:ietf:params:oauth:token-type:id_token") - )) + ), + auth_session_logging_id, + ); + let resp = client + .post(token_endpoint) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(body) .send() .await .map_err(io::Error::other)?; @@ -1202,6 +1234,7 @@ mod tests { use super::redact_sensitive_url_parts; use super::render_login_error_page; use super::sanitize_url_for_logging; + use super::with_auth_session_logging_id; #[serial_test::serial(logout_revoke)] #[tokio::test] @@ -1505,6 +1538,17 @@ mod tests { ); } + #[test] + fn token_form_body_includes_auth_session_logging_id_when_present() { + assert_eq!( + with_auth_session_logging_id( + "grant_type=authorization_code".to_string(), + Some("auth-session-123"), + ), + "grant_type=authorization_code&auth_session_logging_id=auth-session-123" + ); + } + #[test] fn render_login_error_page_escapes_dynamic_fields() { let body = String::from_utf8(render_login_error_page(