diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 7f0e1d18f8..37ed7421cc 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -144,11 +144,11 @@ impl ExternalAuthRefresher for ExternalAuthRefreshBridge { let response: ChatgptAuthTokensRefreshResponse = serde_json::from_value(result).map_err(std::io::Error::other)?; - Ok(ExternalAuthTokens { - access_token: response.access_token, - chatgpt_account_id: response.chatgpt_account_id, - chatgpt_plan_type: response.chatgpt_plan_type, - }) + Ok(ExternalAuthTokens::chatgpt( + response.access_token, + response.chatgpt_account_id, + response.chatgpt_plan_type, + )) } } diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index ef0238d3cc..0d473d2fc2 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -252,6 +252,19 @@ fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() { assert_eq!(manager.refresh_failure_for_auth(&updated_auth), None); } +#[test] +fn external_auth_tokens_without_chatgpt_metadata_cannot_seed_chatgpt_auth() { + let err = AuthDotJson::from_external_tokens(&ExternalAuthTokens::access_token_only( + "test-access-token", + )) + .expect_err("bearer-only external auth should not seed ChatGPT auth"); + + assert_eq!( + err.to_string(), + "external auth tokens are missing ChatGPT metadata" + ); +} + struct AuthFileParams { openai_api_key: Option, chatgpt_plan_type: Option, diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 19d13fc08a..9d1080c073 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -93,8 +93,40 @@ pub enum RefreshTokenError { #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExternalAuthTokens { pub access_token: String, - pub chatgpt_account_id: String, - pub chatgpt_plan_type: Option, + pub chatgpt_metadata: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExternalAuthChatgptMetadata { + pub account_id: String, + pub plan_type: Option, +} + +impl ExternalAuthTokens { + pub fn access_token_only(access_token: impl Into) -> Self { + Self { + access_token: access_token.into(), + chatgpt_metadata: None, + } + } + + pub fn chatgpt( + access_token: impl Into, + chatgpt_account_id: impl Into, + chatgpt_plan_type: Option, + ) -> Self { + Self { + access_token: access_token.into(), + chatgpt_metadata: Some(ExternalAuthChatgptMetadata { + account_id: chatgpt_account_id.into(), + plan_type: chatgpt_plan_type, + }), + } + } + + pub fn chatgpt_metadata(&self) -> Option<&ExternalAuthChatgptMetadata> { + self.chatgpt_metadata.as_ref() + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -110,6 +142,10 @@ pub struct ExternalAuthRefreshContext { #[async_trait] pub trait ExternalAuthRefresher: Send + Sync { + async fn resolve(&self) -> std::io::Result> { + Ok(None) + } + async fn refresh( &self, context: ExternalAuthRefreshContext, @@ -736,11 +772,16 @@ fn refresh_token_endpoint() -> String { impl AuthDotJson { fn from_external_tokens(external: &ExternalAuthTokens) -> std::io::Result { + let Some(chatgpt_metadata) = external.chatgpt_metadata() else { + return Err(std::io::Error::other( + "external auth tokens are missing ChatGPT metadata", + )); + }; let mut token_info = parse_chatgpt_jwt_claims(&external.access_token).map_err(std::io::Error::other)?; - token_info.chatgpt_account_id = Some(external.chatgpt_account_id.clone()); - token_info.chatgpt_plan_type = external - .chatgpt_plan_type + token_info.chatgpt_account_id = Some(chatgpt_metadata.account_id.clone()); + token_info.chatgpt_plan_type = chatgpt_metadata + .plan_type .as_deref() .map(InternalPlanType::from_raw_value) .or(token_info.chatgpt_plan_type) @@ -749,7 +790,7 @@ impl AuthDotJson { id_token: token_info, access_token: external.access_token.clone(), refresh_token: String::new(), - account_id: Some(external.chatgpt_account_id.clone()), + account_id: Some(chatgpt_metadata.account_id.clone()), }; Ok(Self { @@ -765,11 +806,11 @@ impl AuthDotJson { chatgpt_account_id: &str, chatgpt_plan_type: Option<&str>, ) -> std::io::Result { - let external = ExternalAuthTokens { - access_token: access_token.to_string(), - chatgpt_account_id: chatgpt_account_id.to_string(), - chatgpt_plan_type: chatgpt_plan_type.map(str::to_string), - }; + let external = ExternalAuthTokens::chatgpt( + access_token, + chatgpt_account_id, + chatgpt_plan_type.map(str::to_string), + ); Self::from_external_tokens(&external) } @@ -1457,13 +1498,18 @@ impl AuthManager { }; let refreshed = refresher.refresh(context).await?; + let Some(chatgpt_metadata) = refreshed.chatgpt_metadata() else { + return Err(RefreshTokenError::Transient(std::io::Error::other( + "external auth refresh did not return ChatGPT metadata", + ))); + }; if let Some(expected_workspace_id) = forced_chatgpt_workspace_id.as_deref() - && refreshed.chatgpt_account_id != expected_workspace_id + && chatgpt_metadata.account_id != expected_workspace_id { return Err(RefreshTokenError::Transient(std::io::Error::other( format!( "external auth refresh returned workspace {:?}, expected {expected_workspace_id:?}", - refreshed.chatgpt_account_id, + chatgpt_metadata.account_id, ), ))); } diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 9ec6f1a1df..a8033cf888 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -22,6 +22,11 @@ pub use auth::AuthManager; pub use auth::CLIENT_ID; pub use auth::CODEX_API_KEY_ENV_VAR; pub use auth::CodexAuth; +pub use auth::ExternalAuthChatgptMetadata; +pub use auth::ExternalAuthRefreshContext; +pub use auth::ExternalAuthRefreshReason; +pub use auth::ExternalAuthRefresher; +pub use auth::ExternalAuthTokens; pub use auth::OPENAI_API_KEY_ENV_VAR; pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; pub use auth::RefreshTokenError;