mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
## Summary External auth had two paths: provider-command credentials were resolved through `ExternalAuth`, while app-provided ChatGPT credentials were installed separately and only used the provider for refresh. `ExternalAuth` also declared an auth mode independently from the `CodexAuth` value it returned, so the declaration and credentials could disagree. This change makes the provider-owned `CodexAuth` authoritative for initial resolution, credential kind, and refresh. - remove `ExternalAuth::auth_mode` and require providers to return their current auth from `resolve` - route external auth registration, resolution, and unauthorized refresh through `AuthManager::set_external_auth` - keep the last resolved credential with its provider so synchronous consumers and unauthorized recovery observe the credential's actual mode - make the app-server bridge own both initial and refreshed ChatGPT credentials, and detach it on logout - keep model listing free of auth-refresh side effects, including in offline mode Provider-command auth still follows its configured cache interval. App-provided ChatGPT auth still asks the parent app once after a `401` and retries the request once. Stacked on #31355. ## Testing - `just test -p codex-login` - `just test -p codex-models-manager` - focused `codex-app-server` tests for external login/logout, unauthorized refresh, and workspace mismatch
96 lines
3.4 KiB
Rust
96 lines
3.4 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
|
|
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
|
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
|
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
|
use codex_app_server_protocol::ServerRequestPayload;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::ExternalAuthFuture;
|
|
use codex_login::auth::ExternalAuth;
|
|
use codex_login::auth::ExternalAuthRefreshContext;
|
|
use codex_login::auth::ExternalAuthRefreshReason;
|
|
use tokio::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
use crate::outgoing_message::OutgoingMessageSender;
|
|
|
|
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
|
|
|
|
pub(crate) struct ExternalAuthBridge {
|
|
outgoing: Arc<OutgoingMessageSender>,
|
|
auth: RwLock<CodexAuth>,
|
|
}
|
|
|
|
impl ExternalAuthBridge {
|
|
pub(crate) fn new(outgoing: Arc<OutgoingMessageSender>, auth: CodexAuth) -> Self {
|
|
Self {
|
|
outgoing,
|
|
auth: RwLock::new(auth),
|
|
}
|
|
}
|
|
|
|
async fn refresh(&self, context: ExternalAuthRefreshContext) -> std::io::Result<CodexAuth> {
|
|
let reason = match context.reason {
|
|
ExternalAuthRefreshReason::Unauthorized => ChatgptAuthTokensRefreshReason::Unauthorized,
|
|
};
|
|
let params = ChatgptAuthTokensRefreshParams {
|
|
reason,
|
|
previous_account_id: context.previous_account_id,
|
|
};
|
|
|
|
let (request_id, rx) = self
|
|
.outgoing
|
|
.send_request(ServerRequestPayload::ChatgptAuthTokensRefresh(params))
|
|
.await;
|
|
let result = match timeout(EXTERNAL_AUTH_REFRESH_TIMEOUT, rx).await {
|
|
Ok(result) => {
|
|
let result = result.map_err(|err| {
|
|
std::io::Error::other(format!("auth refresh request canceled: {err}"))
|
|
})?;
|
|
result.map_err(|err| {
|
|
std::io::Error::other(format!(
|
|
"auth refresh request failed: code={} message={}",
|
|
err.code, err.message
|
|
))
|
|
})?
|
|
}
|
|
Err(_) => {
|
|
let _canceled = self.outgoing.cancel_request(&request_id).await;
|
|
return Err(std::io::Error::other(format!(
|
|
"auth refresh request timed out after {}s",
|
|
EXTERNAL_AUTH_REFRESH_TIMEOUT.as_secs()
|
|
)));
|
|
}
|
|
};
|
|
|
|
let response: ChatgptAuthTokensRefreshResponse =
|
|
serde_json::from_value(result).map_err(std::io::Error::other)?;
|
|
let auth = CodexAuth::from_external_chatgpt_tokens(
|
|
response.access_token.as_str(),
|
|
response.chatgpt_account_id.as_str(),
|
|
response.chatgpt_plan_type.as_deref(),
|
|
)?;
|
|
*self
|
|
.auth
|
|
.write()
|
|
.map_err(|_| std::io::Error::other("external auth lock is poisoned"))? = auth.clone();
|
|
Ok(auth)
|
|
}
|
|
}
|
|
|
|
impl ExternalAuth for ExternalAuthBridge {
|
|
fn resolve(&self) -> ExternalAuthFuture<'_, CodexAuth> {
|
|
Box::pin(async {
|
|
self.auth
|
|
.read()
|
|
.map(|auth| auth.clone())
|
|
.map_err(|_| std::io::Error::other("external auth lock is poisoned"))
|
|
})
|
|
}
|
|
|
|
fn refresh(&self, context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> {
|
|
Box::pin(ExternalAuthBridge::refresh(self, context))
|
|
}
|
|
}
|