[codex] Support multiple forced ChatGPT workspaces (#18161)

## Summary

This change lets `forced_chatgpt_workspace_id` accept multiple workspace
IDs instead of a single value.

It keeps the existing config key name, adds backward-compatible parsing
for a single string in `config.toml`, and normalizes the setting into an
allowed workspace list across login enforcement, app-server config
surfaces, and local ChatGPT auth helpers.

## Why

Workspace-restricted deployments may need to allow more than one ChatGPT
workspace without dropping the guardrail entirely.

## Server-side impact

Codex's local server and app-server protocol needed changes because they
previously assumed a single workspace ID. The local login flow now
matches the auth backend interface by sending the allowed workspace list
as a single comma-separated `allowed_workspace_id` query parameter.

## Validation

This was tested with:

- A single workspace config
- With multi-workspace configs
- With multiple workspaces in the config
- The user only being a part of a subset of them

All were successful.

Automated coverage:

- `cargo test -p codex-login`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-tui local_chatgpt_auth`
- `cargo test --locked -p codex-app-server
login_account_chatgpt_includes_forced_workspace_allowlist_query_param`
This commit is contained in:
rreichel3-oai authored and GitHub committed 2026-05-14 17:11:36 -04:00
1 parent 32b45a43e2
commit 02a7205250
22 files changed
+673 -117

No files matched your search

+107 -12
View File
@@ -22,6 +22,10 @@ use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
const WORKSPACE_ID_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174000";
const WORKSPACE_ID_SECOND_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174001";
const WORKSPACE_ID_DISALLOWED: &str = "123e4567-e89b-42d3-a456-426614174002";
#[tokio::test]
async fn refresh_without_id_token() {
let codex_home = tempdir().unwrap();
@@ -87,7 +91,7 @@ fn login_with_api_key_overwrites_existing_auth_json() {
async fn login_with_access_token_writes_only_token() {
let dir = tempdir().unwrap();
let auth_path = dir.path().join("auth.json");
let record = agent_identity_record("account-123");
let record = agent_identity_record(WORKSPACE_ID_ALLOWED);
let agent_identity =
signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("signed agent identity");
let server = MockServer::start().await;
@@ -145,7 +149,7 @@ async fn login_with_access_token_rejects_invalid_jwt() {
#[tokio::test]
async fn login_with_access_token_rejects_unsigned_jwt() {
let dir = tempdir().unwrap();
let record = agent_identity_record("account-123");
let record = agent_identity_record(WORKSPACE_ID_ALLOWED);
let agent_identity = fake_agent_identity_jwt(&record).expect("fake agent identity");
let server = MockServer::start().await;
Mock::given(method("GET"))
@@ -329,7 +333,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
AuthFileParams {
openai_api_key: None,
chatgpt_plan_type: Some("pro".to_string()),
chatgpt_account_id: Some("org_mine".to_string()),
chatgpt_account_id: Some(WORKSPACE_ID_ALLOWED.to_string()),
},
codex_home.path(),
)
@@ -654,7 +658,7 @@ fn fake_jwt_for_auth_file_params(params: &AuthFileParams) -> std::io::Result<Str
async fn build_config(
codex_home: &Path,
forced_login_method: Option<ForcedLoginMethod>,
forced_chatgpt_workspace_id: Option<String>,
forced_chatgpt_workspace_id: Option<Vec<String>>,
) -> AuthConfig {
AuthConfig {
codex_home: codex_home.to_path_buf(),
@@ -712,7 +716,7 @@ fn remove_access_token_env_var() -> EnvVarGuard {
#[serial(codex_auth_env)]
async fn load_auth_reads_access_token_from_env() {
let codex_home = tempdir().unwrap();
let expected_record = agent_identity_record("account-123");
let expected_record = agent_identity_record(WORKSPACE_ID_ALLOWED);
let agent_identity =
signed_agent_identity_jwt(&expected_record, json!(expected_record.plan_type))
.expect("signed agent identity");
@@ -762,7 +766,7 @@ async fn load_auth_reads_access_token_from_env() {
#[serial(codex_auth_env)]
async fn load_auth_keeps_codex_api_key_env_precedence() {
let codex_home = tempdir().unwrap();
let record = agent_identity_record("account-123");
let record = agent_identity_record(WORKSPACE_ID_ALLOWED);
let agent_identity = fake_agent_identity_jwt(&record).expect("fake agent identity");
let _access_token_guard = EnvVarGuard::set(CODEX_ACCESS_TOKEN_ENV_VAR, &agent_identity);
let _api_key_guard = EnvVarGuard::set(CODEX_API_KEY_ENV_VAR, "sk-env");
@@ -814,7 +818,7 @@ async fn enforce_login_restrictions_logs_out_for_workspace_mismatch() {
AuthFileParams {
openai_api_key: None,
chatgpt_plan_type: Some("pro".to_string()),
chatgpt_account_id: Some("org_another_org".to_string()),
chatgpt_account_id: Some(WORKSPACE_ID_DISALLOWED.to_string()),
},
codex_home.path(),
)
@@ -823,14 +827,17 @@ async fn enforce_login_restrictions_logs_out_for_workspace_mismatch() {
let config = build_config(
codex_home.path(),
/*forced_login_method*/ None,
Some("org_mine".to_string()),
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
)
.await;
let err = super::enforce_login_restrictions(&config)
.await
.expect_err("expected workspace mismatch to error");
assert!(err.to_string().contains("workspace org_mine"));
assert!(
err.to_string()
.contains(&format!("workspace(s) {WORKSPACE_ID_ALLOWED}"))
);
assert!(
!codex_home.path().join("auth.json").exists(),
"auth.json should be removed on mismatch"
@@ -846,7 +853,7 @@ async fn enforce_login_restrictions_allows_matching_workspace() {
AuthFileParams {
openai_api_key: None,
chatgpt_plan_type: Some("pro".to_string()),
chatgpt_account_id: Some("org_mine".to_string()),
chatgpt_account_id: Some(WORKSPACE_ID_ALLOWED.to_string()),
},
codex_home.path(),
)
@@ -855,7 +862,7 @@ async fn enforce_login_restrictions_allows_matching_workspace() {
let config = build_config(
codex_home.path(),
/*forced_login_method*/ None,
Some("org_mine".to_string()),
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
)
.await;
@@ -870,6 +877,94 @@ async fn enforce_login_restrictions_allows_matching_workspace() {
#[tokio::test]
#[serial(codex_auth_env)]
async fn enforce_login_restrictions_allows_any_matching_workspace_in_list() {
let codex_home = tempdir().unwrap();
let _jwt = write_auth_file(
AuthFileParams {
openai_api_key: None,
chatgpt_plan_type: Some("pro".to_string()),
chatgpt_account_id: Some(WORKSPACE_ID_ALLOWED.to_string()),
},
codex_home.path(),
)
.expect("failed to write auth file");
let config = build_config(
codex_home.path(),
/*forced_login_method*/ None,
Some(vec![
WORKSPACE_ID_SECOND_ALLOWED.to_string(),
WORKSPACE_ID_ALLOWED.to_string(),
]),
)
.await;
super::enforce_login_restrictions(&config)
.await
.expect("any matching workspace in the allowed list should succeed");
}
#[tokio::test]
#[serial(codex_auth_env)]
async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismatch() {
let codex_home = tempdir().unwrap();
let _access_token_guard = remove_access_token_env_var();
let record = agent_identity_record(WORKSPACE_ID_DISALLOWED);
let agent_identity =
signed_agent_identity_jwt(&record, json!(record.plan_type)).expect("signed agent identity");
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/backend-api/wham/agent-identities/jwks"))
.respond_with(ResponseTemplate::new(200).set_body_json(test_jwks_body()))
.expect(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/backend-api/v1/agent/agent-runtime-id/task/register"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"task_id": "task-123",
})))
.expect(1)
.mount(&server)
.await;
let chatgpt_base_url = format!("{}/backend-api", server.uri());
let _authapi_guard =
EnvVarGuard::set("CODEX_AGENT_IDENTITY_AUTHAPI_BASE_URL", &chatgpt_base_url);
save_auth(
codex_home.path(),
&AuthDotJson {
auth_mode: Some(ApiAuthMode::AgentIdentity),
openai_api_key: None,
tokens: None,
last_refresh: None,
agent_identity: Some(agent_identity),
},
AuthCredentialsStoreMode::File,
)
.expect("seed agent identity auth");
let config = AuthConfig {
codex_home: codex_home.path().to_path_buf(),
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
forced_login_method: None,
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
chatgpt_base_url: Some(chatgpt_base_url),
};
let err = super::enforce_login_restrictions(&config)
.await
.expect_err("expected workspace mismatch to error");
assert!(err.to_string().contains(&format!(
"current credentials belong to {WORKSPACE_ID_DISALLOWED}"
)));
assert!(
!codex_home.path().join("auth.json").exists(),
"auth.json should be removed on mismatch"
);
server.verify().await;
}
#[tokio::test]
async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set()
{
let codex_home = tempdir().unwrap();
@@ -880,7 +975,7 @@ async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_f
let config = build_config(
codex_home.path(),
/*forced_login_method*/ None,
Some("org_mine".to_string()),
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
)
.await;
+23 -16
View File
@@ -610,8 +610,8 @@ pub struct AuthConfig {
pub codex_home: PathBuf,
pub auth_credentials_store_mode: AuthCredentialsStoreMode,
pub forced_login_method: Option<ForcedLoginMethod>,
pub forced_chatgpt_workspace_id: Option<String>,
pub chatgpt_base_url: Option<String>,
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
}
pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
@@ -653,9 +653,8 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
}
}
if let Some(expected_account_id) = config.forced_chatgpt_workspace_id.as_deref() {
// workspace is the external identifier for account id.
let chatgpt_account_id = match auth {
if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
let chatgpt_account_id = match &auth {
CodexAuth::ApiKey(_) => return Ok(()),
CodexAuth::AgentIdentity(_) => auth.get_account_id(),
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
@@ -674,13 +673,21 @@ pub async fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<
token_data.id_token.chatgpt_account_id
}
};
if chatgpt_account_id.as_deref() != Some(expected_account_id) {
// workspace is the external identifier for account id.
let chatgpt_account_id = chatgpt_account_id.as_deref();
if !chatgpt_account_id.is_some_and(|actual| {
expected_account_ids
.iter()
.any(|expected| expected == actual)
}) {
let expected_workspaces = expected_account_ids.join(", ");
let message = match chatgpt_account_id {
Some(actual) => format!(
"Login is restricted to workspace {expected_account_id}, but current credentials belong to {actual}. Logging out."
"Login is restricted to workspace(s) {expected_workspaces}, but current credentials belong to {actual}. Logging out."
),
None => format!(
"Login is restricted to workspace {expected_account_id}, but current credentials lack a workspace identifier. Logging out."
"Login is restricted to workspace(s) {expected_workspaces}, but current credentials lack a workspace identifier. Logging out."
),
};
return logout_with_message(
@@ -1247,7 +1254,7 @@ pub struct AuthManager {
inner: RwLock<CachedAuth>,
enable_codex_api_key_env: bool,
auth_credentials_store_mode: AuthCredentialsStoreMode,
forced_chatgpt_workspace_id: RwLock<Option<String>>,
forced_chatgpt_workspace_id: RwLock<Option<Vec<String>>>,
chatgpt_base_url: Option<String>,
refresh_lock: Semaphore,
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
@@ -1266,8 +1273,8 @@ pub trait AuthManagerConfig {
/// Returns the CLI auth credential storage mode for auth loading.
fn cli_auth_credentials_store_mode(&self) -> AuthCredentialsStoreMode;
/// Returns the workspace ID that ChatGPT auth should be restricted to, if any.
fn forced_chatgpt_workspace_id(&self) -> Option<String>;
/// Returns the workspace IDs that ChatGPT auth should be restricted to, if any.
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>>;
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
fn chatgpt_base_url(&self) -> String;
@@ -1548,7 +1555,7 @@ impl AuthManager {
}
}
pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option<String>) {
pub fn set_forced_chatgpt_workspace_id(&self, workspace_id: Option<Vec<String>>) {
if let Ok(mut guard) = self.forced_chatgpt_workspace_id.write()
&& *guard != workspace_id
{
@@ -1556,7 +1563,7 @@ impl AuthManager {
}
}
pub fn forced_chatgpt_workspace_id(&self) -> Option<String> {
pub fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>> {
self.forced_chatgpt_workspace_id
.read()
.ok()
@@ -1836,13 +1843,13 @@ impl AuthManager {
"external auth refresh did not return ChatGPT metadata",
)));
};
if let Some(expected_workspace_id) = forced_chatgpt_workspace_id.as_deref()
&& chatgpt_metadata.account_id != expected_workspace_id
if let Some(expected_workspace_ids) = forced_chatgpt_workspace_id.as_deref()
&& !expected_workspace_ids.contains(&chatgpt_metadata.account_id)
{
return Err(RefreshTokenError::Transient(std::io::Error::other(
format!(
"external auth refresh returned workspace {:?}, expected {expected_workspace_id:?}",
chatgpt_metadata.account_id,
"external auth refresh returned workspace {:?}, expected one of {:?}",
chatgpt_metadata.account_id, expected_workspace_ids,
),
)));
}
+11 -8
View File
@@ -69,7 +69,7 @@ pub struct ServerOptions {
pub port: u16,
pub open_browser: bool,
pub force_state: Option<String>,
pub forced_chatgpt_workspace_id: Option<String>,
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
pub codex_streamlined_login: bool,
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
}
@@ -79,7 +79,7 @@ impl ServerOptions {
pub fn new(
codex_home: PathBuf,
client_id: String,
forced_chatgpt_workspace_id: Option<String>,
forced_chatgpt_workspace_id: Option<Vec<String>>,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> Self {
Self {
@@ -486,7 +486,7 @@ fn build_authorize_url(
redirect_uri: &str,
pkce: &PkceCodes,
state: &str,
forced_chatgpt_workspace_id: Option<&str>,
forced_chatgpt_workspace_ids: Option<&[String]>,
) -> String {
let mut query = vec![
("response_type".to_string(), "code".to_string()),
@@ -507,8 +507,8 @@ fn build_authorize_url(
("state".to_string(), state.to_string()),
("originator".to_string(), originator().value),
];
if let Some(workspace_id) = forced_chatgpt_workspace_id {
query.push(("allowed_workspace_id".to_string(), workspace_id.to_string()));
if let Some(workspace_ids) = forced_chatgpt_workspace_ids {
query.push(("allowed_workspace_id".to_string(), workspace_ids.join(",")));
}
let qs = query
.into_iter()
@@ -928,7 +928,7 @@ fn jwt_auth_claims(jwt: &str) -> serde_json::Map<String, serde_json::Value> {
/// Validates the ID token against an optional workspace restriction.
pub(crate) fn ensure_workspace_allowed(
expected: Option<&str>,
expected: Option<&[String]>,
id_token: &str,
) -> Result<(), String> {
let Some(expected) = expected else {
@@ -940,10 +940,13 @@ pub(crate) fn ensure_workspace_allowed(
return Err("Login is restricted to a specific workspace, but the token did not include an chatgpt_account_id claim.".to_string());
};
if actual == expected {
if expected.iter().any(|workspace_id| workspace_id == actual) {
Ok(())
} else {
Err(format!("Login is restricted to workspace id {expected}."))
Err(format!(
"Login is restricted to workspace id(s) {}.",
expected.join(", ")
))
}
}
@@ -23,6 +23,9 @@ use core_test_support::skip_if_no_network;
// ---------- Small helpers ----------
const WORKSPACE_ID_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174000";
const WORKSPACE_ID_DISALLOWED: &str = "123e4567-e89b-42d3-a456-426614174002";
fn make_jwt(payload: serde_json::Value) -> String {
let header = json!({ "alg": "none", "typ": "JWT" });
let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
@@ -131,7 +134,7 @@ async fn device_code_login_integration_succeeds() -> anyhow::Result<()> {
let jwt = make_jwt(json!({
"https://api.openai.com/auth": {
"chatgpt_account_id": "acct_321"
"chatgpt_account_id": WORKSPACE_ID_ALLOWED
}
}));
@@ -152,7 +155,7 @@ async fn device_code_login_integration_succeeds() -> anyhow::Result<()> {
assert_eq!(tokens.access_token, "access-token-123");
assert_eq!(tokens.refresh_token, "refresh-token-123");
assert_eq!(tokens.id_token.raw_jwt, jwt);
assert_eq!(tokens.account_id.as_deref(), Some("acct_321"));
assert_eq!(tokens.account_id.as_deref(), Some(WORKSPACE_ID_ALLOWED));
Ok(())
}
@@ -174,8 +177,8 @@ async fn device_code_login_rejects_workspace_mismatch() -> anyhow::Result<()> {
let jwt = make_jwt(json!({
"https://api.openai.com/auth": {
"chatgpt_account_id": "acct_321",
"organization_id": "org-actual"
"chatgpt_account_id": WORKSPACE_ID_DISALLOWED,
"organization_id": WORKSPACE_ID_DISALLOWED
}
}));
@@ -183,7 +186,7 @@ async fn device_code_login_rejects_workspace_mismatch() -> anyhow::Result<()> {
let issuer = mock_server.uri();
let mut opts = server_opts(&codex_home, issuer, AuthCredentialsStoreMode::File);
opts.forced_chatgpt_workspace_id = Some("org-required".to_string());
opts.forced_chatgpt_workspace_id = Some(vec![WORKSPACE_ID_ALLOWED.to_string()]);
let err = run_device_code_login(opts)
.await
+58 -10
View File
@@ -12,10 +12,15 @@ use codex_config::types::AuthCredentialsStoreMode;
use codex_login::ServerOptions;
use codex_login::run_login_server;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
use url::Url;
const DEFAULT_LOGIN_PORT: u16 = 1455;
const FALLBACK_LOGIN_PORT: u16 = 1457;
const WORKSPACE_ID_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174000";
const WORKSPACE_ID_SECOND_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174001";
const WORKSPACE_ID_DISALLOWED: &str = "123e4567-e89b-42d3-a456-426614174002";
// See spawn.rs for details
@@ -121,7 +126,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
port: 0,
open_browser: false,
force_state: Some(state),
forced_chatgpt_workspace_id: Some(chatgpt_account_id.to_string()),
forced_chatgpt_workspace_id: Some(vec![chatgpt_account_id.to_string()]),
codex_streamlined_login: false,
};
let server = run_login_server(opts)?;
@@ -165,7 +170,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
async fn creates_missing_codex_home_dir() -> Result<()> {
skip_if_no_network!(Ok(()));
let (issuer_addr, _issuer_handle) = start_mock_issuer("org-123");
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_ALLOWED);
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
let tmp = tempdir()?;
@@ -204,11 +209,52 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn login_server_includes_forced_workspaces_as_one_query_param() -> 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 codex_home = tmp.path().to_path_buf();
let state = "state-multi".to_string();
let opts = ServerOptions {
codex_home,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
client_id: codex_login::CLIENT_ID.to_string(),
issuer,
port: 0,
open_browser: false,
force_state: Some(state),
forced_chatgpt_workspace_id: Some(vec![
WORKSPACE_ID_ALLOWED.to_string(),
WORKSPACE_ID_SECOND_ALLOWED.to_string(),
]),
codex_streamlined_login: false,
};
let server = run_login_server(opts)?;
let auth_url = Url::parse(&server.auth_url)?;
let allowed_workspace_ids = auth_url
.query_pairs()
.filter_map(|(key, value)| (key == "allowed_workspace_id").then(|| value.into_owned()))
.collect::<Vec<_>>();
assert_eq!(
allowed_workspace_ids,
vec![format!(
"{WORKSPACE_ID_ALLOWED},{WORKSPACE_ID_SECOND_ALLOWED}"
)]
);
Ok(())
}
#[tokio::test]
async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
skip_if_no_network!(Ok(()));
let (issuer_addr, _issuer_handle) = start_mock_issuer("org-actual");
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_DISALLOWED);
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
let tmp = tempdir()?;
@@ -223,14 +269,14 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
port: 0,
open_browser: false,
force_state: Some(state.clone()),
forced_chatgpt_workspace_id: Some("org-required".to_string()),
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
codex_streamlined_login: false,
};
let server = run_login_server(opts)?;
assert!(
server
.auth_url
.contains("allowed_workspace_id=org-required"),
.contains(&format!("allowed_workspace_id={WORKSPACE_ID_ALLOWED}")),
"auth URL should include forced workspace parameter"
);
let login_port = server.actual_port;
@@ -241,7 +287,9 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
assert!(resp.status().is_success());
let body = resp.text().await?;
assert!(
body.contains("Login is restricted to workspace id org-required"),
body.contains(&format!(
"Login is restricted to workspace id(s) {WORKSPACE_ID_ALLOWED}"
)),
"error body should mention workspace restriction"
);
@@ -266,7 +314,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error() -> Result<()> {
skip_if_no_network!(Ok(()));
let (issuer_addr, _issuer_handle) = start_mock_issuer("org-123");
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_ALLOWED);
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
let tmp = tempdir()?;
@@ -334,7 +382,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<()> {
skip_if_no_network!(Ok(()));
let (issuer_addr, _issuer_handle) = start_mock_issuer("org-123");
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_ALLOWED);
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
let tmp = tempdir()?;
@@ -442,7 +490,7 @@ async fn falls_back_to_registered_fallback_port_when_default_port_is_in_use() ->
})
};
let (issuer_addr, _issuer_handle) = start_mock_issuer("org-123");
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_ALLOWED);
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
let tmp = tempdir()?;
@@ -480,7 +528,7 @@ async fn falls_back_to_registered_fallback_port_when_default_port_is_in_use() ->
async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
skip_if_no_network!(Ok(()));
let (issuer_addr, _issuer_handle) = start_mock_issuer("org-123");
let (issuer_addr, _issuer_handle) = start_mock_issuer(WORKSPACE_ID_ALLOWED);
let issuer = format!("http://{}:{}", issuer_addr.ip(), issuer_addr.port());
let first_tmp = tempdir()?;