mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Enable staging login issuer overrides in packaged builds (#42811)
## Why Packaged clients need to pair a staging OAuth client ID with the matching issuer. The issuer override was previously limited to debug builds. ## What changed - Apply `CODEX_APP_SERVER_LOGIN_ISSUER` in all builds when it contains a non-empty value. - Keep `CODEX_APP_SERVER_DEV_OPEN_APP_URL` debug-only. ## Testing Exercise the complete app-server OAuth flow with the issuer and client ID overrides, including authorization, token exchange, and login completion. GitOrigin-RevId: 13d290b9dae689cd811361b1b549953a3b52db42
This commit is contained in:
committed by
copyberry
parent
0ae02915bd
commit
86b1b359cf
@@ -21,9 +21,9 @@ const ACCOUNT_TOKEN_USAGE_FETCH_TIMEOUT: Duration = Duration::from_secs(/*secs*/
|
||||
const THREAD_USAGE_FETCH_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
|
||||
const ACCOUNT_WORKSPACE_MESSAGES_FETCH_TIMEOUT: Duration =
|
||||
Duration::from_millis(/*millis*/ 1000);
|
||||
// Login overrides are intentionally available only in debug builds.
|
||||
#[cfg(debug_assertions)]
|
||||
// Packaged clients use this together with the OAuth client ID override for staging login.
|
||||
const LOGIN_ISSUER_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_LOGIN_ISSUER";
|
||||
// The development success-page redirect remains debug-only.
|
||||
#[cfg(debug_assertions)]
|
||||
const LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_DEV_OPEN_APP_URL";
|
||||
|
||||
@@ -561,7 +561,7 @@ impl AccountRequestProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
let opts = LoginServerOptions {
|
||||
let mut opts = LoginServerOptions {
|
||||
open_browser: false,
|
||||
codex_streamlined_login,
|
||||
login_success_page,
|
||||
@@ -574,24 +574,20 @@ impl AccountRequestProcessor {
|
||||
config.auth_route_config(),
|
||||
)
|
||||
};
|
||||
if let Ok(issuer) = std::env::var(LOGIN_ISSUER_OVERRIDE_ENV_VAR)
|
||||
&& !issuer.trim().is_empty()
|
||||
{
|
||||
opts.issuer = issuer;
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
let opts = {
|
||||
let mut opts = opts;
|
||||
if let Ok(issuer) = std::env::var(LOGIN_ISSUER_OVERRIDE_ENV_VAR)
|
||||
&& !issuer.trim().is_empty()
|
||||
{
|
||||
opts.issuer = issuer;
|
||||
}
|
||||
if let LoginSuccessPage::Hosted { url, .. } = &mut opts.login_success_page
|
||||
&& let Ok(open_app_url) = std::env::var(LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR)
|
||||
&& !open_app_url.trim().is_empty()
|
||||
{
|
||||
*url = open_app_url
|
||||
.parse()
|
||||
.map_err(|err| internal_error(format!("invalid Codex open app URL: {err}")))?;
|
||||
}
|
||||
opts
|
||||
};
|
||||
if let LoginSuccessPage::Hosted { url, .. } = &mut opts.login_success_page
|
||||
&& let Ok(open_app_url) = std::env::var(LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR)
|
||||
&& !open_app_url.trim().is_empty()
|
||||
{
|
||||
*url = open_app_url
|
||||
.parse()
|
||||
.map_err(|err| internal_error(format!("invalid Codex open app URL: {err}")))?;
|
||||
}
|
||||
|
||||
Ok(opts)
|
||||
}
|
||||
|
||||
@@ -2337,16 +2337,27 @@ async fn login_account_chatgpt_start_can_be_cancelled() -> Result<()> {
|
||||
#[tokio::test]
|
||||
// Serialize tests that launch the login server since it binds to a fixed port.
|
||||
#[serial(login_port)]
|
||||
async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
async fn login_account_chatgpt_uses_oauth_overrides() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
let mock_server = MockServer::start().await;
|
||||
let id_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("staging@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
mock_oauth_token(&mock_server, &id_token).await;
|
||||
let issuer = mock_server.uri();
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
// Exercise packaged-build behavior without the debug-only startup flag.
|
||||
.with_plugin_startup_tasks()
|
||||
.with_env_overrides(&[
|
||||
(CLIENT_ID_OVERRIDE_ENV_VAR, Some("staging-client")),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some("https://auth.example.com")),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
])
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
@@ -2358,10 +2369,7 @@ async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
bail!("unexpected login response: {login:?}");
|
||||
};
|
||||
let auth_url = Url::parse(&auth_url)?;
|
||||
assert_eq!(
|
||||
auth_url.origin().ascii_serialization(),
|
||||
"https://auth.example.com"
|
||||
);
|
||||
assert_eq!(auth_url.origin().ascii_serialization(), issuer);
|
||||
assert_eq!(
|
||||
auth_url
|
||||
.query_pairs()
|
||||
@@ -2369,11 +2377,67 @@ async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
Some("staging-client".to_string())
|
||||
);
|
||||
|
||||
let cancel_id = mcp
|
||||
.send_cancel_login_account_request(CancelLoginAccountParams { login_id })
|
||||
.await?;
|
||||
let _: CancelLoginAccountResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(cancel_id)).await??;
|
||||
let callback_url = auth_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "redirect_uri").then_some(value.into_owned()))
|
||||
.ok_or_else(|| anyhow::anyhow!("missing redirect_uri"))?;
|
||||
let state = auth_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "state").then_some(value.into_owned()))
|
||||
.ok_or_else(|| anyhow::anyhow!("missing state"))?;
|
||||
let mut callback_url = Url::parse(&callback_url)?;
|
||||
callback_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("code", "test-code")
|
||||
.append_pair("state", &state);
|
||||
let client = HttpClientBuilder::new()
|
||||
.without_redirects()
|
||||
.build_direct()?;
|
||||
let response = client.get(callback_url.clone()).send().await?;
|
||||
assert_eq!(response.status(), 302);
|
||||
let success_url = Url::parse(response.headers()["location"].to_str()?)?;
|
||||
assert_eq!(success_url.origin(), callback_url.origin());
|
||||
let response = client.get(success_url).send().await?;
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
let requests = mock_server
|
||||
.received_requests()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to read OAuth requests"))?;
|
||||
let token_request = requests
|
||||
.iter()
|
||||
.find(|request| {
|
||||
request.url.path() == "/oauth/token"
|
||||
&& url::form_urlencoded::parse(&request.body)
|
||||
.any(|(key, value)| key == "grant_type" && value == "authorization_code")
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("missing authorization-code token exchange"))?;
|
||||
let token_form: std::collections::HashMap<_, _> =
|
||||
url::form_urlencoded::parse(&token_request.body)
|
||||
.into_owned()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
token_form.get("client_id").map(String::as_str),
|
||||
Some("staging-client")
|
||||
);
|
||||
|
||||
let notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("account/login/completed"),
|
||||
)
|
||||
.await??;
|
||||
let ServerNotification::AccountLoginCompleted(payload) = notification.try_into()? else {
|
||||
bail!("unexpected notification")
|
||||
};
|
||||
assert_eq!(
|
||||
payload,
|
||||
AccountLoginCompletedNotification {
|
||||
login_id: Some(login_id),
|
||||
success: true,
|
||||
error: None,
|
||||
onboarding_entrypoint: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user