Support staging OAuth client ID overrides (#28257)

## Summary

- allow app-server ChatGPT login to use a configured OAuth client ID
- reuse the same client ID for refresh and revoke requests
- cover staging login, refresh, and revoke request payloads

## Tests

- `just test -p codex-login`
- `just test -p codex-app-server
login_account_chatgpt_uses_debug_oauth_overrides`
- `just test -p codex-login
logout_with_revoke_revokes_refresh_token_then_removes_auth`
- `just fix -p codex-login`
- `just fix -p codex-app-server`
- `just fmt`
This commit is contained in:
Anton Panasenko authored and GitHub committed 2026-06-15 10:56:53 -07:00
1 parent aae4c0b02b
commit 336f907ec1
8 files changed
+108 -31

No files matched your search

+30 -18
View File
@@ -8,6 +8,7 @@ use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthDotJson;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthManager;
use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_login::RefreshTokenError;
use codex_login::load_auth_dot_json;
@@ -31,11 +32,12 @@ use wiremock::matchers::path;
const INITIAL_ACCESS_TOKEN: &str = "initial-access-token";
const INITIAL_REFRESH_TOKEN: &str = "initial-refresh-token";
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_succeeds_updates_storage() -> Result<()> {
skip_if_no_network!(Ok(()));
let _client_id_guard = EnvGuard::set(CLIENT_ID_OVERRIDE_ENV_VAR, "staging-client".to_string());
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
@@ -66,6 +68,16 @@ async fn refresh_token_succeeds_updates_storage() -> Result<()> {
.await
.context("refresh should succeed")?;
let requests = server.received_requests().await.unwrap_or_default();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&requests[0].body)?,
json!({
"client_id": "staging-client",
"grant_type": "refresh_token",
"refresh_token": INITIAL_REFRESH_TOKEN,
})
);
let refreshed_tokens = TokenData {
access_token: "new-access-token".to_string(),
refresh_token: "new-refresh-token".to_string(),
@@ -97,7 +109,7 @@ async fn refresh_token_succeeds_updates_storage() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -163,7 +175,7 @@ async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn auth_refreshes_when_access_token_is_near_expiry() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -225,7 +237,7 @@ async fn auth_refreshes_when_access_token_is_near_expiry() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn auth_skips_access_token_outside_refresh_window() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -263,7 +275,7 @@ async fn auth_skips_access_token_outside_refresh_window() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -324,7 +336,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -399,7 +411,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn returns_fresh_tokens_as_is() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -448,7 +460,7 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refreshes_token_when_access_token_is_expired() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -510,7 +522,7 @@ async fn refreshes_token_when_access_token_is_expired() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -568,7 +580,7 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -634,7 +646,7 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -689,7 +701,7 @@ async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Re
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -758,7 +770,7 @@ async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_does_not_retry_after_bad_request_reused_failure() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -827,7 +839,7 @@ async fn refresh_token_does_not_retry_after_bad_request_reused_failure() -> Resu
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -915,7 +927,7 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -969,7 +981,7 @@ async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()>
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -1068,7 +1080,7 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -1154,7 +1166,7 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn unauthorized_recovery_requires_chatgpt_auth() -> Result<()> {
skip_if_no_network!(Ok(()));
+7 -5
View File
@@ -7,6 +7,7 @@ use codex_login::AuthDotJson;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthManager;
use codex_login::CLIENT_ID;
use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR;
use codex_login::CODEX_ACCESS_TOKEN_ENV_VAR;
use codex_login::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_login::logout_with_revoke;
@@ -28,11 +29,12 @@ use wiremock::matchers::path;
const ACCESS_TOKEN: &str = "access-token";
const REFRESH_TOKEN: &str = "refresh-token";
#[serial_test::serial(logout_revoke)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn logout_with_revoke_revokes_refresh_token_then_removes_auth() -> Result<()> {
skip_if_no_network!(Ok(()));
let _client_id_guard = EnvGuard::set(CLIENT_ID_OVERRIDE_ENV_VAR, "staging-client".to_string());
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/revoke"))
@@ -77,14 +79,14 @@ async fn logout_with_revoke_revokes_refresh_token_then_removes_auth() -> Result<
json!({
"token": REFRESH_TOKEN,
"token_type_hint": "refresh_token",
"client_id": CLIENT_ID,
"client_id": "staging-client",
})
);
server.verify().await;
Ok(())
}
#[serial_test::serial(logout_revoke)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn logout_with_revoke_uses_stored_auth_when_access_token_env_is_set() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -126,7 +128,7 @@ async fn logout_with_revoke_uses_stored_auth_when_access_token_env_is_set() -> R
Ok(())
}
#[serial_test::serial(logout_revoke)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn logout_with_revoke_removes_auth_when_revoke_fails() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -169,7 +171,7 @@ async fn logout_with_revoke_removes_auth_when_revoke_fails() -> Result<()> {
Ok(())
}
#[serial_test::serial(logout_revoke)]
#[serial_test::serial(auth_env)]
#[tokio::test]
async fn auth_manager_logout_with_revoke_uses_cached_auth() -> Result<()> {
skip_if_no_network!(Ok(()));