[codex-login] clear invalidated auth without account id [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-05-19 23:00:52 +00:00
parent 27acff776c
commit 96e9af9096
2 changed files with 70 additions and 8 deletions

View File

@@ -360,6 +360,41 @@ async fn invalidated_access_token_logout_clears_cached_auth() {
assert!(!codex_home.path().join("auth.json").exists());
}
#[tokio::test]
async fn invalidated_access_token_logout_clears_cached_auth_without_account_id() {
let codex_home = tempdir().unwrap();
write_auth_file(
AuthFileParams {
openai_api_key: None,
chatgpt_plan_type: Some("pro".to_string()),
chatgpt_account_id: None,
},
codex_home.path(),
)
.expect("failed to write auth file");
let manager = AuthManager::shared(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
)
.await;
let recovery = manager.unauthorized_recovery();
let failed = recovery
.handle_invalidated_access_token_auth()
.await
.expect_err("unchanged revoked auth without an account id should force login");
assert_eq!(failed.reason, RefreshTokenFailedReason::Revoked);
assert_eq!(
failed.message,
"Your ChatGPT session is no longer valid. Please sign in again."
);
assert!(manager.auth_cached().is_none());
assert!(!codex_home.path().join("auth.json").exists());
}
#[tokio::test]
async fn invalidated_access_token_preserves_reloaded_auth() {
let codex_home = tempdir().unwrap();

View File

@@ -1265,14 +1265,27 @@ impl UnauthorizedRecovery {
pub async fn handle_invalidated_access_token_auth(
&self,
) -> Result<UnauthorizedRecoveryStepResult, RefreshTokenFailedError> {
match self
.manager
.reload_if_account_id_matches(self.expected_account_id.as_deref())
.await
{
ReloadOutcome::ReloadedChanged => Ok(UnauthorizedRecoveryStepResult {
auth_state_changed: Some(true),
}),
let reload_outcome = match self.expected_account_id.as_deref() {
Some(expected_account_id) => {
self.manager
.reload_if_account_id_matches(Some(expected_account_id))
.await
}
None => self.manager.reload_if_auth_snapshot_changed().await,
};
match reload_outcome {
ReloadOutcome::ReloadedChanged => {
if self.manager.auth_cached().is_none() {
return Err(RefreshTokenFailedError::new(
RefreshTokenFailedReason::Revoked,
ACCESS_TOKEN_INVALIDATED_MESSAGE.to_string(),
));
}
Ok(UnauthorizedRecoveryStepResult {
auth_state_changed: Some(true),
})
}
ReloadOutcome::ReloadedNoChange => {
let message = match self.manager.logout().await {
Ok(_) => ACCESS_TOKEN_INVALIDATED_MESSAGE.to_string(),
@@ -1532,6 +1545,20 @@ impl AuthManager {
}
}
async fn reload_if_auth_snapshot_changed(&self) -> ReloadOutcome {
let new_auth = self.load_auth_from_storage().await;
let cached_before_reload = self.auth_cached();
let auth_changed =
!Self::auths_equal_for_refresh(cached_before_reload.as_ref(), new_auth.as_ref());
if !auth_changed {
return ReloadOutcome::ReloadedNoChange;
}
tracing::info!("Reloading auth because the persisted auth snapshot changed.");
self.set_cached_auth(new_auth);
ReloadOutcome::ReloadedChanged
}
fn auths_equal_for_refresh(a: Option<&CodexAuth>, b: Option<&CodexAuth>) -> bool {
match (a, b) {
(None, None) => true,