mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[login] support hosted success redirects (#28745)
## What Adds an optional hosted login-success redirect path for app-server login requests. - Keeps the existing localhost success page as the default. - Lets app-server callers opt into a hosted success page with an optional protocol field. - Persists credentials before redirecting to the hosted success page. - Keeps org setup and existing CLI/device-code login flows on the local success page. - Accepts an optional typed `appBrand` value and forwards it to the hosted page as `app_brand` so web can select the correct asset. - Generates the app-server protocol schema updates for the new optional fields. ## Why This supports the hosted Codex login success page rollout without changing existing login behavior by default. The Codex Apps frontend can gate the opt-in with Statsig after the hosted web page. ## Rollout safety - Old callers omit the new field and continue using localhost. - New callers talking to old app-server builds remain safe because the Codex Apps side treats the field as optional and defaults the flag off. - Missing brand values default to Codex. - The hosted redirect always uses the app-login source so the hosted page can reopen Codex; the existing streamlined-login visual flag remains separate. ## Validation - `just fmt` - `just fix -p codex-login -p codex-app-server-protocol -p codex-app-server -p codex-app-server-test-client -p codex-tui` - `just test -p codex-login` - `just test -p codex-app-server-protocol` - `just write-app-server-schema` - `git diff HEAD --check` The focused login and protocol run passed all 380 tests. I also started the broader `just test -p codex-app-server`; it compiled successfully, then many tests failed on this machine because spawned test servers tried to use the ambient `/Users/rafaelj/.codex/sqlite` state DB, which is read-only in this sandbox. I stopped that run after confirming the failures shared that environment issue.
This commit is contained in:
1 parent
ff06ab7172
commit
1fd0858e86
21 files changed
+680
-155
No files matched your search
@@ -10,6 +10,8 @@ use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::LoginSuccessPage;
|
||||
use codex_login::LoginSuccessPageBrand;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::run_login_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
@@ -131,6 +133,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
forced_chatgpt_workspace_id: Some(vec![chatgpt_account_id.to_string()]),
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -141,13 +144,20 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
);
|
||||
let login_port = server.actual_port;
|
||||
|
||||
// Simulate browser callback, and follow redirect to /success
|
||||
// Simulate browser callback and assert the local success redirect before following it.
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
let url = format!("http://127.0.0.1:{login_port}/auth/callback?code=abc&state=test_state_123");
|
||||
let resp = client.get(&url).send().await?;
|
||||
assert!(resp.status().is_success());
|
||||
assert_eq!(resp.status(), 302);
|
||||
let success_url = resp.headers()["location"].to_str()?;
|
||||
let success_url = Url::parse(success_url)?;
|
||||
assert_eq!(success_url.host_str(), Some("localhost"));
|
||||
assert_eq!(success_url.path(), "/success");
|
||||
|
||||
let success_resp = client.get(success_url).send().await?;
|
||||
assert!(success_resp.status().is_success());
|
||||
|
||||
// Wait for server shutdown
|
||||
server.block_until_done().await?;
|
||||
@@ -169,6 +179,52 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hosted_login_redirects_to_configured_open_app_url() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_ALLOWED);
|
||||
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
|
||||
let tmp = tempdir()?;
|
||||
let server = run_login_server(ServerOptions {
|
||||
codex_home: tmp.path().to_path_buf(),
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
auth_route_config: None,
|
||||
client_id: codex_login::CLIENT_ID.to_string(),
|
||||
issuer,
|
||||
port: 0,
|
||||
open_browser: false,
|
||||
force_state: Some("streamlined_state".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
login_success_page: LoginSuccessPage::Hosted {
|
||||
url: Url::parse("http://localhost:3000/codex/open-app?source=old")?,
|
||||
app_brand: LoginSuccessPageBrand::Chatgpt,
|
||||
},
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
})?;
|
||||
let login_port = server.actual_port;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
.get(format!(
|
||||
"http://127.0.0.1:{login_port}/auth/callback?code=abc&state=streamlined_state"
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
assert_eq!(response.status(), 302);
|
||||
assert_eq!(
|
||||
response.headers()["location"].to_str()?,
|
||||
"http://localhost:3000/codex/open-app?source=login&app_brand=chatgpt"
|
||||
);
|
||||
tokio::time::timeout(Duration::from_secs(1), server.block_until_done()).await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creates_missing_codex_home_dir() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -195,6 +251,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -240,6 +297,7 @@ async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result<
|
||||
]),
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let auth_url = Url::parse(&server.auth_url)?;
|
||||
@@ -280,6 +338,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -342,6 +401,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -412,6 +472,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -561,6 +622,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
|
||||
let first_server = run_login_server(first_opts)?;
|
||||
@@ -584,6 +646,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
login_success_page: LoginSuccessPage::Local,
|
||||
};
|
||||
|
||||
let second_server = run_login_server(second_opts)?;
|
||||
|
||||
Reference in new issue
Block a user