Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions codex-rs/login/src/auth/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,17 @@ pub fn logout(
Ok(removed)
}

/// Delete persisted auth without mirroring the removal into the stored-account
/// catalog. Rollback paths use this to restore a prior "no active auth" state
/// without treating the operation as a user-initiated logout.
pub fn delete_auth(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> std::io::Result<bool> {
let storage = create_auth_storage(codex_home.to_path_buf(), auth_credentials_store_mode);
storage.delete()
}

pub async fn logout_with_revoke(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
Expand Down
153 changes: 113 additions & 40 deletions codex-rs/login/src/auth_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,50 @@ pub fn find_account(codex_home: &Path, account_id: &str) -> io::Result<Option<St
Ok(data.accounts.into_iter().find(|acc| acc.id == account_id))
}

pub fn auth_for_account(
codex_home: &Path,
account_id: &str,
) -> io::Result<(StoredAccount, AuthDotJson)> {
let account = find_account(codex_home, account_id)?
.ok_or_else(|| io::Error::other(format!("account with id {account_id} was not found")))?;

let auth =
match account.mode {
AuthMode::ApiKey => AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some(account.openai_api_key.clone().ok_or_else(|| {
io::Error::other("stored API key account is missing the key value")
})?),
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
},
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => AuthDotJson {
auth_mode: Some(account.mode),
openai_api_key: None,
tokens: Some(account.tokens.clone().ok_or_else(|| {
io::Error::other("stored ChatGPT account is missing token data")
})?),
last_refresh: account.last_refresh,
agent_identity: None,
personal_access_token: None,
},
AuthMode::AgentIdentity => {
return Err(io::Error::other(
"stored agent identity account activation is not supported",
));
}
AuthMode::PersonalAccessToken => {
return Err(io::Error::other(
"stored personal access token account activation is not supported",
));
}
};

Ok((account, auth))
}

pub fn update_account_last_refresh(
codex_home: &Path,
account_id: &str,
Expand Down Expand Up @@ -419,49 +463,78 @@ pub fn activate_account(
account_id: &str,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> io::Result<StoredAccount> {
let account = find_account(codex_home, account_id)?
.ok_or_else(|| io::Error::other(format!("account with id {account_id} was not found")))?;
let (_account, auth) = auth_for_account(codex_home, account_id)?;
commit_active_account(codex_home, account_id, &auth, auth_credentials_store_mode)
}

let auth =
match account.mode {
AuthMode::ApiKey => AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some(account.openai_api_key.clone().ok_or_else(|| {
io::Error::other("stored API key account is missing the key value")
})?),
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
},
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => AuthDotJson {
auth_mode: Some(account.mode),
openai_api_key: None,
tokens: Some(account.tokens.clone().ok_or_else(|| {
io::Error::other("stored ChatGPT account is missing token data")
})?),
last_refresh: account.last_refresh,
agent_identity: None,
personal_access_token: None,
},
AuthMode::AgentIdentity => {
return Err(io::Error::other(
"stored agent identity account activation is not supported",
));
fn restore_previous_auth(
codex_home: &Path,
previous_auth: Option<AuthDotJson>,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> io::Result<()> {
if let Some(previous_auth) = previous_auth {
save_auth(codex_home, &previous_auth, auth_credentials_store_mode)
} else {
crate::delete_auth(codex_home, auth_credentials_store_mode).map(|_| ())
}
}

fn restore_previous_activation(
codex_home: &Path,
previous_auth: Option<AuthDotJson>,
previous_active_account_id: Option<String>,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> io::Result<()> {
let auth_result = restore_previous_auth(codex_home, previous_auth, auth_credentials_store_mode);
let active_account_result = set_active_account_id(codex_home, previous_active_account_id);

auth_result?;
active_account_result?;
Ok(())
}

pub fn commit_active_account(
codex_home: &Path,
account_id: &str,
auth: &AuthDotJson,
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> io::Result<StoredAccount> {
let previous_auth = crate::load_auth_dot_json(codex_home, auth_credentials_store_mode)?;
let previous_active_account_id = get_active_account_id(codex_home)?;

save_auth(codex_home, auth, auth_credentials_store_mode)?;
match set_active_account_id(codex_home, Some(account_id.to_string())) {
Ok(Some(activated)) => Ok(activated),
Ok(None) => {
let rollback_result = restore_previous_activation(
codex_home,
previous_auth,
previous_active_account_id,
auth_credentials_store_mode,
);
if let Err(rollback_err) = rollback_result {
tracing::warn!(
"failed to roll back missing stored account activation: {rollback_err}"
);
}
AuthMode::PersonalAccessToken => {
return Err(io::Error::other(
"stored personal access token account activation is not supported",
));
Err(io::Error::other(format!(
"account with id {account_id} disappeared before activation"
)))
}
Err(err) => {
if let Err(rollback_err) = restore_previous_activation(
codex_home,
previous_auth,
previous_active_account_id,
auth_credentials_store_mode,
) {
tracing::warn!(
"failed to roll back stored account activation error: {rollback_err}"
);
}
};

save_auth(codex_home, &auth, auth_credentials_store_mode)?;
set_active_account_id(codex_home, Some(account.id))?.ok_or_else(|| {
io::Error::other(format!(
"account with id {account_id} disappeared before activation"
))
})
Err(err)
}
}
}

pub fn remove_account(codex_home: &Path, account_id: &str) -> io::Result<Option<StoredAccount>> {
Expand Down
125 changes: 125 additions & 0 deletions codex-rs/login/src/auth_accounts_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,131 @@ fn activate_api_key_account_writes_auth_and_marks_active() {
);
}

#[test]
fn auth_for_account_returns_auth_without_persisting_activation() {
let temp = TempDir::new().expect("tempdir");
let stored = upsert_api_key_account(
temp.path(),
"sk-test".to_string(),
Some("Work".to_string()),
/*make_active*/ false,
)
.expect("upsert api key");

let (account, auth) = auth_for_account(temp.path(), &stored.id).expect("account auth");

assert_eq!(stored, account);
assert_eq!(
crate::AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some("sk-test".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
},
auth
);
assert_eq!(None, get_active_account_id(temp.path()).expect("active id"));
assert_eq!(
None,
crate::load_auth_dot_json(temp.path(), AuthCredentialsStoreMode::File)
.expect("read auth json")
);
}

#[test]
fn commit_active_account_rolls_back_auth_and_active_id_when_account_is_missing() {
let temp = TempDir::new().expect("tempdir");
let stored = upsert_api_key_account(
temp.path(),
"sk-previous".to_string(),
Some("Previous".to_string()),
/*make_active*/ true,
)
.expect("upsert previous api key");
let previous_auth = crate::AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some("sk-previous".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
};
crate::save_auth(temp.path(), &previous_auth, AuthCredentialsStoreMode::File)
.expect("save previous auth");
let new_auth = crate::AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some("sk-new".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
};

let err = commit_active_account(
temp.path(),
"missing",
&new_auth,
AuthCredentialsStoreMode::File,
)
.expect_err("missing account should fail");

assert_eq!(io::ErrorKind::Other, err.kind());
assert_eq!(
Some(stored.id.as_str()),
get_active_account_id(temp.path())
.expect("active id")
.as_deref()
);
assert_eq!(
previous_auth,
crate::load_auth_dot_json(temp.path(), AuthCredentialsStoreMode::File)
.expect("read auth json")
.expect("auth json should exist")
);
}

#[test]
fn commit_active_account_rollback_without_previous_auth_preserves_stored_accounts() {
let temp = TempDir::new().expect("tempdir");
let stored = upsert_api_key_account(
temp.path(),
"sk-new".to_string(),
Some("New".to_string()),
/*make_active*/ false,
)
.expect("upsert api key");
let auth = crate::AuthDotJson {
auth_mode: Some(AuthMode::ApiKey),
openai_api_key: Some("sk-new".to_string()),
tokens: None,
last_refresh: None,
agent_identity: None,
personal_access_token: None,
};

let err = commit_active_account(
temp.path(),
"missing",
&auth,
AuthCredentialsStoreMode::File,
)
.expect_err("missing account should fail");

assert_eq!(io::ErrorKind::Other, err.kind());
assert_eq!(None, get_active_account_id(temp.path()).expect("active id"));
assert_eq!(
None,
crate::load_auth_dot_json(temp.path(), AuthCredentialsStoreMode::File)
.expect("read auth json")
);
assert_eq!(
vec![stored],
list_accounts(temp.path()).expect("list accounts")
);
}

#[test]
fn activate_chatgpt_account_writes_auth_and_marks_active() {
let temp = TempDir::new().expect("tempdir");
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/login/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub use auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
pub use auth::RefreshTokenError;
pub use auth::UnauthorizedRecovery;
pub use auth::default_client;
pub use auth::delete_auth;
pub use auth::enforce_login_restrictions;
pub use auth::load_auth_dot_json;
pub use auth::login_with_access_token;
Expand All @@ -56,6 +57,8 @@ pub use auth_account_import::SkippedAuthAccountImport;
pub use auth_account_import::import_auth_accounts_from_auth_homes;
pub use auth_accounts::StoredAccount;
pub use auth_accounts::activate_account;
pub use auth_accounts::auth_for_account;
pub use auth_accounts::commit_active_account;
pub use auth_accounts::find_account;
pub use auth_accounts::get_active_account_id;
pub use auth_accounts::list_accounts;
Expand Down
Loading
Loading