refactor: make ExternalAuth return CodexAuth (#31355)

## Summary

- refactor `ExternalAuth` to return `CodexAuth` directly
- remove the parallel `ExternalAuthTokens` wrapper
- preserve existing external bearer and app-server refresh behavior

This is a mechanical precursor refactor; it does not add auth
capabilities or change recovery behavior.

## Testing

- `cargo fmt --all`
- `cargo test -q -p codex-login --lib`
- `cargo test -q -p codex-app-server --test all
external_auth_refreshes_on_unauthorized`
This commit is contained in:
lt-oai authored and GitHub committed 2026-07-07 09:47:41 -07:00
1 parent f363ed70cc
commit a3f8b0b332
6 files changed
+76 -135

No files matched your search

+8 -11
View File
@@ -74,10 +74,10 @@ use codex_feedback::CodexFeedback;
use codex_goal_extension::GoalService; use codex_goal_extension::GoalService;
use codex_home::CodexHomeUserInstructionsProvider; use codex_home::CodexHomeUserInstructionsProvider;
use codex_login::AuthManager; use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::auth::ExternalAuth; use codex_login::auth::ExternalAuth;
use codex_login::auth::ExternalAuthRefreshContext; use codex_login::auth::ExternalAuthRefreshContext;
use codex_login::auth::ExternalAuthRefreshReason; use codex_login::auth::ExternalAuthRefreshReason;
use codex_login::auth::ExternalAuthTokens;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use codex_protocol::auth::AuthMode as LoginAuthMode; use codex_protocol::auth::AuthMode as LoginAuthMode;
use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SessionSource;
@@ -121,10 +121,7 @@ impl ExternalAuthRefreshBridge {
} }
} }
async fn refresh( async fn refresh(&self, context: ExternalAuthRefreshContext) -> std::io::Result<CodexAuth> {
&self,
context: ExternalAuthRefreshContext,
) -> std::io::Result<ExternalAuthTokens> {
let params = ChatgptAuthTokensRefreshParams { let params = ChatgptAuthTokensRefreshParams {
reason: Self::map_reason(context.reason), reason: Self::map_reason(context.reason),
previous_account_id: context.previous_account_id, previous_account_id: context.previous_account_id,
@@ -162,11 +159,11 @@ impl ExternalAuthRefreshBridge {
let response: ChatgptAuthTokensRefreshResponse = let response: ChatgptAuthTokensRefreshResponse =
serde_json::from_value(result).map_err(std::io::Error::other)?; serde_json::from_value(result).map_err(std::io::Error::other)?;
Ok(ExternalAuthTokens::chatgpt( CodexAuth::from_external_chatgpt_tokens(
response.access_token, response.access_token.as_str(),
response.chatgpt_account_id, response.chatgpt_account_id.as_str(),
response.chatgpt_plan_type, response.chatgpt_plan_type.as_deref(),
)) )
} }
} }
@@ -178,7 +175,7 @@ impl ExternalAuth for ExternalAuthRefreshBridge {
fn refresh( fn refresh(
&self, &self,
context: ExternalAuthRefreshContext, context: ExternalAuthRefreshContext,
) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { ) -> codex_login::ExternalAuthFuture<'_, CodexAuth> {
Box::pin(ExternalAuthRefreshBridge::refresh(self, context)) Box::pin(ExternalAuthRefreshBridge::refresh(self, context))
} }
} }
-13
View File
@@ -1015,19 +1015,6 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
assert_eq!(manager.refresh_failure_for_auth(&updated_auth), None); 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"
);
}
#[tokio::test] #[tokio::test]
async fn external_bearer_only_auth_manager_uses_cached_provider_token() { async fn external_bearer_only_auth_manager_uses_cached_provider_token() {
let script = ProviderAuthScript::new(&["provider-token", "next-token"]).unwrap(); let script = ProviderAuthScript::new(&["provider-token", "next-token"]).unwrap();
+9 -15
View File
@@ -1,7 +1,7 @@
use super::manager::CodexAuth;
use super::manager::ExternalAuth; use super::manager::ExternalAuth;
use super::manager::ExternalAuthFuture; use super::manager::ExternalAuthFuture;
use super::manager::ExternalAuthRefreshContext; use super::manager::ExternalAuthRefreshContext;
use super::manager::ExternalAuthTokens;
use codex_protocol::auth::AuthMode; use codex_protocol::auth::AuthMode;
use codex_protocol::config_types::ModelProviderAuthInfo; use codex_protocol::config_types::ModelProviderAuthInfo;
use std::fmt; use std::fmt;
@@ -30,7 +30,7 @@ impl BearerTokenRefresher {
clippy::await_holding_invalid_type, clippy::await_holding_invalid_type,
reason = "external bearer cache misses intentionally hold cached_token across the provider command to avoid duplicate refreshes" reason = "external bearer cache misses intentionally hold cached_token across the provider command to avoid duplicate refreshes"
)] )]
async fn resolve(&self) -> io::Result<Option<ExternalAuthTokens>> { async fn resolve(&self) -> io::Result<Option<CodexAuth>> {
let access_token = { let access_token = {
let mut cached = self.state.cached_token.lock().await; let mut cached = self.state.cached_token.lock().await;
if let Some(cached_token) = cached.as_ref() { if let Some(cached_token) = cached.as_ref() {
@@ -39,8 +39,8 @@ impl BearerTokenRefresher {
None => true, None => true,
}; };
if should_use_cached_token { if should_use_cached_token {
return Ok(Some(ExternalAuthTokens::access_token_only( return Ok(Some(CodexAuth::from_api_key(
cached_token.access_token.clone(), cached_token.access_token.as_str(),
))); )));
} }
} }
@@ -52,20 +52,17 @@ impl BearerTokenRefresher {
}); });
access_token access_token
}; };
Ok(Some(ExternalAuthTokens::access_token_only(access_token))) Ok(Some(CodexAuth::from_api_key(access_token.as_str())))
} }
async fn refresh( async fn refresh(&self, _context: ExternalAuthRefreshContext) -> io::Result<CodexAuth> {
&self,
_context: ExternalAuthRefreshContext,
) -> io::Result<ExternalAuthTokens> {
let access_token = run_provider_auth_command(&self.state.config).await?; let access_token = run_provider_auth_command(&self.state.config).await?;
let mut cached = self.state.cached_token.lock().await; let mut cached = self.state.cached_token.lock().await;
*cached = Some(CachedExternalBearerToken { *cached = Some(CachedExternalBearerToken {
access_token: access_token.clone(), access_token: access_token.clone(),
fetched_at: Instant::now(), fetched_at: Instant::now(),
}); });
Ok(ExternalAuthTokens::access_token_only(access_token)) Ok(CodexAuth::from_api_key(access_token.as_str()))
} }
} }
@@ -74,14 +71,11 @@ impl ExternalAuth for BearerTokenRefresher {
AuthMode::ApiKey AuthMode::ApiKey
} }
fn resolve(&self) -> ExternalAuthFuture<'_, Option<ExternalAuthTokens>> { fn resolve(&self) -> ExternalAuthFuture<'_, Option<CodexAuth>> {
Box::pin(BearerTokenRefresher::resolve(self)) Box::pin(BearerTokenRefresher::resolve(self))
} }
fn refresh( fn refresh(&self, context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> {
&self,
context: ExternalAuthRefreshContext,
) -> ExternalAuthFuture<'_, ExternalAuthTokens> {
Box::pin(BearerTokenRefresher::refresh(self, context)) Box::pin(BearerTokenRefresher::refresh(self, context))
} }
} }
+54 -80
View File
@@ -198,45 +198,6 @@ pub enum RefreshTokenError {
Transient(#[from] std::io::Error), Transient(#[from] std::io::Error),
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalAuthTokens {
pub access_token: String,
pub chatgpt_metadata: Option<ExternalAuthChatgptMetadata>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalAuthChatgptMetadata {
pub account_id: String,
pub plan_type: Option<String>,
}
impl ExternalAuthTokens {
pub fn access_token_only(access_token: impl Into<String>) -> Self {
Self {
access_token: access_token.into(),
chatgpt_metadata: None,
}
}
pub fn chatgpt(
access_token: impl Into<String>,
chatgpt_account_id: impl Into<String>,
chatgpt_plan_type: Option<String>,
) -> 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExternalAuthRefreshReason { pub enum ExternalAuthRefreshReason {
Unauthorized, Unauthorized,
@@ -251,22 +212,19 @@ pub struct ExternalAuthRefreshContext {
/// Pluggable auth provider used by `AuthManager` for externally managed auth flows. /// Pluggable auth provider used by `AuthManager` for externally managed auth flows.
/// ///
/// Implementations may either resolve auth eagerly via `resolve()` or provide refreshed /// Implementations may either resolve auth eagerly via `resolve()` or provide refreshed
/// credentials on demand via `refresh()`. /// auth on demand via `refresh()`.
pub trait ExternalAuth: Send + Sync { pub trait ExternalAuth: Send + Sync {
/// Indicates which top-level auth mode this external provider supplies. /// Indicates which top-level auth mode this external provider supplies.
fn auth_mode(&self) -> AuthMode; fn auth_mode(&self) -> AuthMode;
/// Returns cached or immediately available auth, if this provider can resolve it synchronously /// Returns cached or immediately available auth, if this provider can resolve it synchronously
/// from the caller's perspective. /// from the caller's perspective.
fn resolve(&self) -> ExternalAuthFuture<'_, Option<ExternalAuthTokens>> { fn resolve(&self) -> ExternalAuthFuture<'_, Option<CodexAuth>> {
Box::pin(async { Ok(None) }) Box::pin(async { Ok(None) })
} }
/// Refreshes auth in response to a manager-driven refresh attempt. /// Refreshes auth in response to a manager-driven refresh attempt.
fn refresh( fn refresh(&self, context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth>;
&self,
context: ExternalAuthRefreshContext,
) -> ExternalAuthFuture<'_, ExternalAuthTokens>;
} }
pub type ExternalAuthFuture<'a, T> = Pin<Box<dyn Future<Output = std::io::Result<T>> + Send + 'a>>; pub type ExternalAuthFuture<'a, T> = Pin<Box<dyn Future<Output = std::io::Result<T>> + Send + 'a>>;
@@ -747,6 +705,24 @@ impl CodexAuth {
Self::Chatgpt(ChatgptAuth { state, storage }) Self::Chatgpt(ChatgptAuth { state, storage })
} }
/// Constructs in-memory ChatGPT auth from externally managed tokens.
pub fn from_external_chatgpt_tokens(
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> std::io::Result<Self> {
let auth_dot_json = AuthDotJson::from_external_access_token(
access_token,
chatgpt_account_id,
chatgpt_plan_type,
)?;
let state = ChatgptAuthState {
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
client: create_client(),
};
Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state }))
}
pub fn from_api_key(api_key: &str) -> Self { pub fn from_api_key(api_key: &str) -> Self {
Self::ApiKey(ApiKeyAuth { Self::ApiKey(ApiKeyAuth {
api_key: api_key.to_owned(), api_key: api_key.to_owned(),
@@ -1456,26 +1432,23 @@ fn refresh_token_endpoint() -> String {
} }
impl AuthDotJson { impl AuthDotJson {
fn from_external_tokens(external: &ExternalAuthTokens) -> std::io::Result<Self> { fn from_external_access_token(
let Some(chatgpt_metadata) = external.chatgpt_metadata() else { access_token: &str,
return Err(std::io::Error::other( chatgpt_account_id: &str,
"external auth tokens are missing ChatGPT metadata", chatgpt_plan_type: Option<&str>,
)); ) -> std::io::Result<Self> {
};
let mut token_info = let mut token_info =
parse_chatgpt_jwt_claims(&external.access_token).map_err(std::io::Error::other)?; parse_chatgpt_jwt_claims(access_token).map_err(std::io::Error::other)?;
token_info.chatgpt_account_id = Some(chatgpt_metadata.account_id.clone()); token_info.chatgpt_account_id = Some(chatgpt_account_id.to_string());
token_info.chatgpt_plan_type = chatgpt_metadata token_info.chatgpt_plan_type = chatgpt_plan_type
.plan_type
.as_deref()
.map(InternalPlanType::from_raw_value) .map(InternalPlanType::from_raw_value)
.or(token_info.chatgpt_plan_type) .or(token_info.chatgpt_plan_type)
.or(Some(InternalPlanType::Unknown("unknown".to_string()))); .or(Some(InternalPlanType::Unknown("unknown".to_string())));
let tokens = TokenData { let tokens = TokenData {
id_token: token_info, id_token: token_info,
access_token: external.access_token.clone(), access_token: access_token.to_string(),
refresh_token: String::new(), refresh_token: String::new(),
account_id: Some(chatgpt_metadata.account_id.clone()), account_id: Some(chatgpt_account_id.to_string()),
}; };
Ok(Self { Ok(Self {
@@ -1489,19 +1462,6 @@ impl AuthDotJson {
}) })
} }
fn from_external_access_token(
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> std::io::Result<Self> {
let external = ExternalAuthTokens::chatgpt(
access_token,
chatgpt_account_id,
chatgpt_plan_type.map(str::to_string),
);
Self::from_external_tokens(&external)
}
pub(super) fn resolved_mode(&self) -> AuthMode { pub(super) fn resolved_mode(&self) -> AuthMode {
if let Some(mode) = self.auth_mode { if let Some(mode) = self.auth_mode {
return mode; return mode;
@@ -2361,7 +2321,14 @@ impl AuthManager {
let external_auth = self.external_auth()?; let external_auth = self.external_auth()?;
match external_auth.resolve().await { match external_auth.resolve().await {
Ok(Some(tokens)) => Some(CodexAuth::from_api_key(&tokens.access_token)), Ok(Some(auth)) if auth.api_auth_mode() == AuthMode::ApiKey => Some(auth),
Ok(Some(auth)) => {
tracing::error!(
resolved_mode = ?auth.api_auth_mode(),
"ignoring non-API-key auth from external API key provider"
);
None
}
Ok(None) => None, Ok(None) => None,
Err(err) => { Err(err) => {
tracing::error!("Failed to resolve external API key auth: {err}"); tracing::error!("Failed to resolve external API key auth: {err}");
@@ -2567,23 +2534,30 @@ impl AuthManager {
if external_auth.auth_mode() == AuthMode::ApiKey { if external_auth.auth_mode() == AuthMode::ApiKey {
return Ok(()); return Ok(());
} }
let Some(chatgpt_metadata) = refreshed.chatgpt_metadata() else { if refreshed.api_auth_mode() != AuthMode::ChatgptAuthTokens {
return Err(RefreshTokenError::Transient(std::io::Error::other( return Err(RefreshTokenError::Transient(std::io::Error::other(
"external auth refresh did not return ChatGPT metadata", "external auth refresh did not return ChatGPT auth tokens",
))); )));
}; }
let account_id = refreshed.get_account_id().ok_or_else(|| {
RefreshTokenError::Transient(std::io::Error::other(
"external ChatGPT auth tokens are missing an account id",
))
})?;
if let Some(expected_workspace_ids) = forced_chatgpt_workspace_id.as_deref() if let Some(expected_workspace_ids) = forced_chatgpt_workspace_id.as_deref()
&& !expected_workspace_ids.contains(&chatgpt_metadata.account_id) && !expected_workspace_ids.contains(&account_id)
{ {
return Err(RefreshTokenError::Transient(std::io::Error::other( return Err(RefreshTokenError::Transient(std::io::Error::other(
format!( format!(
"external auth refresh returned workspace {:?}, expected one of {:?}", "external auth refresh returned workspace {account_id:?}, expected one of {expected_workspace_ids:?}"
chatgpt_metadata.account_id, expected_workspace_ids,
), ),
))); )));
} }
let auth_dot_json = let auth_dot_json = refreshed.get_current_auth_json().ok_or_else(|| {
AuthDotJson::from_external_tokens(&refreshed).map_err(RefreshTokenError::Transient)?; RefreshTokenError::Transient(std::io::Error::other(
"external ChatGPT auth tokens are missing auth state",
))
})?;
save_auth( save_auth(
&self.codex_home, &self.codex_home,
&auth_dot_json, &auth_dot_json,
-2
View File
@@ -30,11 +30,9 @@ pub use auth::CODEX_ACCESS_TOKEN_ENV_VAR;
pub use auth::CODEX_API_KEY_ENV_VAR; pub use auth::CODEX_API_KEY_ENV_VAR;
pub use auth::CodexAuth; pub use auth::CodexAuth;
pub use auth::ExternalAuth; pub use auth::ExternalAuth;
pub use auth::ExternalAuthChatgptMetadata;
pub use auth::ExternalAuthFuture; pub use auth::ExternalAuthFuture;
pub use auth::ExternalAuthRefreshContext; pub use auth::ExternalAuthRefreshContext;
pub use auth::ExternalAuthRefreshReason; pub use auth::ExternalAuthRefreshReason;
pub use auth::ExternalAuthTokens;
pub use auth::OPENAI_API_KEY_ENV_VAR; pub use auth::OPENAI_API_KEY_ENV_VAR;
pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
pub use auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR; pub use auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
+5 -14
View File
@@ -7,7 +7,6 @@ use codex_login::AuthManager;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_login::ExternalAuth; use codex_login::ExternalAuth;
use codex_login::ExternalAuthRefreshContext; use codex_login::ExternalAuthRefreshContext;
use codex_login::ExternalAuthTokens;
use codex_login::TokenData; use codex_login::TokenData;
use codex_protocol::auth::AuthMode; use codex_protocol::auth::AuthMode;
use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ModelsResponse;
@@ -122,23 +121,15 @@ impl ExternalAuth for TestExternalApiKeyAuth {
AuthMode::ApiKey AuthMode::ApiKey
} }
fn resolve(&self) -> codex_login::ExternalAuthFuture<'_, Option<ExternalAuthTokens>> { fn resolve(&self) -> codex_login::ExternalAuthFuture<'_, Option<CodexAuth>> {
Box::pin(async { Box::pin(async { Ok(Some(CodexAuth::from_api_key("test-external-api-key"))) })
Ok(Some(ExternalAuthTokens::access_token_only(
"test-external-api-key",
)))
})
} }
fn refresh( fn refresh(
&self, &self,
_context: ExternalAuthRefreshContext, _context: ExternalAuthRefreshContext,
) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { ) -> codex_login::ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Box::pin(async { Ok(CodexAuth::from_api_key("test-external-api-key")) })
Ok(ExternalAuthTokens::access_token_only(
"test-external-api-key",
))
})
} }
} }
@@ -153,7 +144,7 @@ impl ExternalAuth for TestUnresolvedExternalApiKeyAuth {
fn refresh( fn refresh(
&self, &self,
_context: ExternalAuthRefreshContext, _context: ExternalAuthRefreshContext,
) -> codex_login::ExternalAuthFuture<'_, ExternalAuthTokens> { ) -> codex_login::ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Err(std::io::Error::other("unresolved test auth")) }) Box::pin(async { Err(std::io::Error::other("unresolved test auth")) })
} }
} }