[codex-login] clear all native HTTP state on auth changes [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-03 09:54:36 +00:00
parent 236fc646dc
commit cf81d6f8ff
9 changed files with 67 additions and 141 deletions
+1 -1
View File
@@ -1290,7 +1290,7 @@ impl MessageProcessor {
}
ClientRequest::LogoutAccount { .. } => {
self.account_processor
.logout_account(request_id.clone(), app_server_client_name.as_deref())
.logout_account(request_id.clone())
.await
}
ClientRequest::CancelLoginAccount { params, .. } => {
@@ -341,9 +341,9 @@ use codex_login::CLIENT_ID;
use codex_login::CodexAuth;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
use codex_login::auth::login_with_chatgpt_auth_tokens_for_surface;
use codex_login::auth::login_with_chatgpt_auth_tokens;
use codex_login::complete_device_code_login;
use codex_login::login_with_api_key_for_surface;
use codex_login::login_with_api_key;
use codex_login::request_device_code;
use codex_login::run_login_server;
use codex_mcp::McpRuntimeContext;
@@ -100,11 +100,8 @@ impl AccountRequestProcessor {
pub(crate) async fn logout_account(
&self,
request_id: ConnectionRequestId,
app_server_client_name: Option<&str>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.logout_v2(request_id, Self::http_state_surface(app_server_client_name))
.await
.map(|()| None)
self.logout_v2(request_id).await.map(|()| None)
}
pub(crate) async fn cancel_login_account(
@@ -229,12 +226,8 @@ impl AccountRequestProcessor {
) -> Result<(), JSONRPCErrorError> {
match params {
LoginAccountParams::ApiKey { api_key } => {
self.login_api_key_v2(
request_id,
LoginApiKeyParams { api_key },
http_state_surface,
)
.await;
self.login_api_key_v2(request_id, LoginApiKeyParams { api_key })
.await;
}
LoginAccountParams::Chatgpt {
codex_streamlined_login,
@@ -256,7 +249,6 @@ impl AccountRequestProcessor {
access_token,
chatgpt_account_id,
chatgpt_plan_type,
http_state_surface,
)
.await;
}
@@ -273,7 +265,6 @@ impl AccountRequestProcessor {
async fn login_api_key_common(
&self,
params: &LoginApiKeyParams,
http_state_surface: HttpStateSurface,
) -> std::result::Result<(), JSONRPCErrorError> {
if self.auth_manager.is_external_chatgpt_auth_active() {
return Err(self.external_auth_active_error());
@@ -296,11 +287,10 @@ impl AccountRequestProcessor {
}
}
match login_with_api_key_for_surface(
match login_with_api_key(
&self.config.codex_home,
&params.api_key,
self.config.cli_auth_credentials_store_mode,
http_state_surface,
) {
Ok(()) => {
self.auth_manager.reload().await;
@@ -310,14 +300,9 @@ impl AccountRequestProcessor {
}
}
async fn login_api_key_v2(
&self,
request_id: ConnectionRequestId,
params: LoginApiKeyParams,
http_state_surface: HttpStateSurface,
) {
async fn login_api_key_v2(&self, request_id: ConnectionRequestId, params: LoginApiKeyParams) {
let result = self
.login_api_key_common(&params, http_state_surface)
.login_api_key_common(&params)
.await
.map(|()| LoginAccountResponse::ApiKey {});
let logged_in = result.is_ok();
@@ -578,15 +563,9 @@ impl AccountRequestProcessor {
access_token: String,
chatgpt_account_id: String,
chatgpt_plan_type: Option<String>,
http_state_surface: HttpStateSurface,
) {
let result = self
.login_chatgpt_auth_tokens_response(
access_token,
chatgpt_account_id,
chatgpt_plan_type,
http_state_surface,
)
.login_chatgpt_auth_tokens_response(access_token, chatgpt_account_id, chatgpt_plan_type)
.await;
let logged_in = result.is_ok();
self.outgoing.send_result(request_id, result).await;
@@ -602,7 +581,6 @@ impl AccountRequestProcessor {
access_token: String,
chatgpt_account_id: String,
chatgpt_plan_type: Option<String>,
http_state_surface: HttpStateSurface,
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
if matches!(
self.config.forced_login_method,
@@ -629,12 +607,11 @@ impl AccountRequestProcessor {
)));
}
login_with_chatgpt_auth_tokens_for_surface(
login_with_chatgpt_auth_tokens(
&self.config.codex_home,
&access_token,
&chatgpt_account_id,
chatgpt_plan_type.as_deref(),
http_state_surface,
)
.map_err(|err| internal_error(format!("failed to set external auth: {err}")))?;
self.auth_manager.reload().await;
@@ -719,10 +696,7 @@ impl AccountRequestProcessor {
}
}
async fn logout_common(
&self,
http_state_surface: HttpStateSurface,
) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
async fn logout_common(&self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
// Cancel any active login attempt.
{
let mut guard = self.active_login.lock().await;
@@ -731,11 +705,7 @@ impl AccountRequestProcessor {
}
}
match self
.auth_manager
.logout_with_revoke_for_surface(http_state_surface)
.await
{
match self.auth_manager.logout_with_revoke().await {
Ok(_) => {}
Err(err) => {
return Err(internal_error(format!("logout failed: {err}")));
@@ -757,12 +727,8 @@ impl AccountRequestProcessor {
.map(CodexAuth::api_auth_mode))
}
async fn logout_v2(
&self,
request_id: ConnectionRequestId,
http_state_surface: HttpStateSurface,
) -> Result<(), JSONRPCErrorError> {
let result = self.logout_common(http_state_surface).await;
async fn logout_v2(&self, request_id: ConnectionRequestId) -> Result<(), JSONRPCErrorError> {
let result = self.logout_common().await;
let account_updated =
result
.as_ref()
@@ -217,6 +217,7 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
HttpStateSurface::CodexDesktopSsh,
INTEGRITY_STATE.to_string(),
)?;
http_state.set(HttpStateSurface::CodexCli, INTEGRITY_STATE.to_string())?;
assert!(codex_home.path().join("auth.json").exists());
let mut mcp =
@@ -256,6 +257,7 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
"auth.json should be deleted"
);
assert_eq!(http_state.get(HttpStateSurface::CodexDesktopSsh)?, None);
assert_eq!(http_state.get(HttpStateSurface::CodexCli)?, None);
let get_id = mcp
.send_get_account_request(GetAccountParams {
@@ -942,6 +944,7 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
HttpStateSurface::CodexDesktopSsh,
INTEGRITY_STATE.to_string(),
)?;
http_state.set(HttpStateSurface::CodexCli, INTEGRITY_STATE.to_string())?;
let mut mcp = TestAppServer::new(codex_home.path()).await?;
let initialized = timeout(
DEFAULT_READ_TIMEOUT,
@@ -988,6 +991,7 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
assert!(codex_home.path().join("auth.json").exists());
assert_eq!(http_state.get(HttpStateSurface::CodexDesktopSsh)?, None);
assert_eq!(http_state.get(HttpStateSurface::CodexCli)?, None);
Ok(())
}
+10
View File
@@ -29,6 +29,16 @@ pub enum HttpStateSurface {
}
impl HttpStateSurface {
pub const ALL: [Self; 7] = [
Self::CodexCli,
Self::CodexTui,
Self::CodexExec,
Self::CodexVscode,
Self::CodexDesktop,
Self::CodexDesktopSsh,
Self::CodexRemoteControl,
];
pub fn try_from_app_server_client_name(client_name: &str) -> Option<Self> {
match client_name {
"codex_cli" => Some(Self::CodexCli),
+12
View File
@@ -3,6 +3,8 @@ use crate::auth::storage::FileAuthStorage;
use crate::auth::storage::get_auth_file;
use crate::token_data::IdTokenInfo;
use codex_app_server_protocol::AuthMode;
use codex_http_state::HttpStateStore;
use codex_http_state::HttpStateSurface;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::auth::KnownPlan as InternalKnownPlan;
use codex_protocol::auth::PlanType as InternalPlanType;
@@ -791,6 +793,10 @@ async fn enforce_login_restrictions_logs_out_for_method_mismatch() {
let _access_token_guard = remove_access_token_env_var();
login_with_api_key(codex_home.path(), "sk-test", AuthCredentialsStoreMode::File)
.expect("seed api key");
let http_state = HttpStateStore::new(codex_home.path().to_path_buf());
http_state
.set(HttpStateSurface::CodexTui, "stale-state".to_string())
.expect("seed TUI state");
let config = build_config(
codex_home.path(),
@@ -807,6 +813,12 @@ async fn enforce_login_restrictions_logs_out_for_method_mismatch() {
!codex_home.path().join("auth.json").exists(),
"auth.json should be removed on mismatch"
);
assert_eq!(
http_state
.get(HttpStateSurface::CodexTui)
.expect("TUI state should load"),
None,
);
}
#[tokio::test]
+7 -83
View File
@@ -21,7 +21,6 @@ use codex_agent_identity::decode_agent_identity_jwt;
use codex_agent_identity::fetch_agent_identity_jwks;
use codex_app_server_protocol::AuthMode;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
use codex_http_state::HttpStateSurface;
use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ModelProviderAuthInfo;
@@ -516,19 +515,6 @@ pub fn logout(
pub async fn logout_with_revoke(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> std::io::Result<bool> {
logout_with_revoke_for_surface(
codex_home,
auth_credentials_store_mode,
HttpStateSurface::CodexCli,
)
.await
}
pub async fn logout_with_revoke_for_surface(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
http_state_surface: HttpStateSurface,
) -> std::io::Result<bool> {
AuthManager::new(
codex_home.to_path_buf(),
@@ -537,7 +523,7 @@ pub async fn logout_with_revoke_for_surface(
/*chatgpt_base_url*/ None,
)
.await
.logout_with_revoke_for_surface(http_state_surface)
.logout_with_revoke()
.await
}
@@ -546,20 +532,6 @@ pub fn login_with_api_key(
codex_home: &Path,
api_key: &str,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> std::io::Result<()> {
login_with_api_key_for_surface(
codex_home,
api_key,
auth_credentials_store_mode,
HttpStateSurface::CodexCli,
)
}
pub fn login_with_api_key_for_surface(
codex_home: &Path,
api_key: &str,
auth_credentials_store_mode: AuthCredentialsStoreMode,
http_state_surface: HttpStateSurface,
) -> std::io::Result<()> {
let auth_dot_json = AuthDotJson {
auth_mode: Some(ApiAuthMode::ApiKey),
@@ -569,7 +541,7 @@ pub fn login_with_api_key_for_surface(
agent_identity: None,
};
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)?;
crate::http_state::clear(codex_home, http_state_surface);
crate::http_state::clear_all(codex_home);
Ok(())
}
@@ -579,23 +551,6 @@ pub async fn login_with_access_token(
access_token: &str,
auth_credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: Option<&str>,
) -> std::io::Result<()> {
login_with_access_token_for_surface(
codex_home,
access_token,
auth_credentials_store_mode,
chatgpt_base_url,
HttpStateSurface::CodexCli,
)
.await
}
pub async fn login_with_access_token_for_surface(
codex_home: &Path,
access_token: &str,
auth_credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: Option<&str>,
http_state_surface: HttpStateSurface,
) -> std::io::Result<()> {
let base_url = chatgpt_base_url
.unwrap_or(DEFAULT_CHATGPT_BACKEND_BASE_URL)
@@ -610,7 +565,7 @@ pub async fn login_with_access_token_for_surface(
agent_identity: Some(access_token.to_string()),
};
save_auth(codex_home, &auth_dot_json, auth_credentials_store_mode)?;
crate::http_state::clear(codex_home, http_state_surface);
crate::http_state::clear_all(codex_home);
Ok(())
}
@@ -620,22 +575,6 @@ pub fn login_with_chatgpt_auth_tokens(
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> std::io::Result<()> {
login_with_chatgpt_auth_tokens_for_surface(
codex_home,
access_token,
chatgpt_account_id,
chatgpt_plan_type,
HttpStateSurface::CodexCli,
)
}
pub fn login_with_chatgpt_auth_tokens_for_surface(
codex_home: &Path,
access_token: &str,
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
http_state_surface: HttpStateSurface,
) -> std::io::Result<()> {
let auth_dot_json = AuthDotJson::from_external_access_token(
access_token,
@@ -647,7 +586,7 @@ pub fn login_with_chatgpt_auth_tokens_for_surface(
&auth_dot_json,
AuthCredentialsStoreMode::Ephemeral,
)?;
crate::http_state::clear(codex_home, http_state_surface);
crate::http_state::clear_all(codex_home);
Ok(())
}
@@ -778,7 +717,7 @@ fn logout_with_message(
// External auth tokens live in the ephemeral store, but persistent auth may still exist
// from earlier logins. Clear both so a forced logout truly removes all active auth.
let removal_result = logout_all_stores(codex_home, auth_credentials_store_mode);
crate::http_state::clear(codex_home, HttpStateSurface::CodexCli);
crate::http_state::clear_all(codex_home);
let error_message = match removal_result {
Ok(_) => message,
Err(err) => format!("{message}. Failed to remove auth.json: {err}"),
@@ -1841,29 +1780,14 @@ impl AuthManager {
/// reloads the in‑memory auth cache so callers immediately observe the
/// unauthenticated state.
pub async fn logout(&self) -> std::io::Result<bool> {
self.logout_for_surface(HttpStateSurface::CodexCli).await
}
pub async fn logout_for_surface(
&self,
http_state_surface: HttpStateSurface,
) -> std::io::Result<bool> {
let result = logout_all_stores(&self.codex_home, self.auth_credentials_store_mode);
crate::http_state::clear(&self.codex_home, http_state_surface);
crate::http_state::clear_all(&self.codex_home);
// Always reload to clear any cached auth (even if file absent).
self.reload().await;
result
}
pub async fn logout_with_revoke(&self) -> std::io::Result<bool> {
self.logout_with_revoke_for_surface(HttpStateSurface::CodexCli)
.await
}
pub async fn logout_with_revoke_for_surface(
&self,
http_state_surface: HttpStateSurface,
) -> std::io::Result<bool> {
let auth_dot_json = self
.auth_cached()
.and_then(|auth| auth.get_current_auth_json());
@@ -1871,7 +1795,7 @@ impl AuthManager {
tracing::warn!("failed to revoke auth tokens during logout: {err}");
}
let result = logout_all_stores(&self.codex_home, self.auth_credentials_store_mode);
crate::http_state::clear(&self.codex_home, http_state_surface);
crate::http_state::clear_all(&self.codex_home);
// Always reload to clear any cached auth (even if file absent).
self.reload().await;
result
+19 -8
View File
@@ -10,18 +10,20 @@ pub(crate) fn replace_after_login(
state: Option<String>,
) {
let store = HttpStateStore::new(codex_home.to_path_buf());
let result = match state {
Some(state) => store.set(surface, state),
None => store.clear(surface),
};
if let Err(err) = result {
clear_all(codex_home);
if let Some(state) = state
&& let Err(err) = store.set(surface, state)
{
warn!(%surface, "failed to reset HTTP state after login: {err}");
}
}
pub(crate) fn clear(codex_home: &Path, surface: HttpStateSurface) {
if let Err(err) = HttpStateStore::new(codex_home.to_path_buf()).clear(surface) {
warn!(%surface, "failed to clear HTTP state: {err}");
pub(crate) fn clear_all(codex_home: &Path) {
let store = HttpStateStore::new(codex_home.to_path_buf());
for surface in HttpStateSurface::ALL {
if let Err(err) = store.clear(surface) {
warn!(%surface, "failed to clear HTTP state: {err}");
}
}
}
@@ -34,12 +36,21 @@ mod tests {
fn replace_after_login_sets_and_clears_surface_state() {
let codex_home = TempDir::new().expect("tempdir");
let store = HttpStateStore::new(codex_home.path().to_path_buf());
store
.set(HttpStateSurface::CodexCli, "stale-cli-state".to_string())
.expect("CLI state should store");
replace_after_login(
codex_home.path(),
HttpStateSurface::CodexDesktop,
Some("minted-state".to_string()),
);
assert_eq!(
store
.get(HttpStateSurface::CodexCli)
.expect("CLI state should load"),
None,
);
assert_eq!(
store
.get(HttpStateSurface::CodexDesktop)
-1
View File
@@ -41,7 +41,6 @@ pub use auth::enforce_login_restrictions;
pub use auth::load_auth_dot_json;
pub use auth::login_with_access_token;
pub use auth::login_with_api_key;
pub use auth::login_with_api_key_for_surface;
pub use auth::logout;
pub use auth::logout_with_revoke;
pub use auth::read_codex_access_token_from_env;