login: configure ChatGPT OAuth explicitly

This commit is contained in:
Katia Bazzi
2026-06-18 16:33:57 -07:00
parent acfba94ffe
commit 2da46c3575
15 changed files with 196 additions and 110 deletions
+20 -18
View File
@@ -11,11 +11,12 @@ use codex_app_server_protocol::AuthMode;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::config::Config;
use codex_login::AuthKeyringBackendKind;
use codex_login::ChatgptOAuthConfig;
use codex_login::CodexAuth;
use codex_login::ServerOptions;
use codex_login::login_with_access_token;
use codex_login::login_with_api_key;
use codex_login::logout_with_revoke_for_chatgpt_base_url;
use codex_login::logout_with_revoke_for_oauth_config;
use codex_login::run_device_code_login;
use codex_login::run_login_server;
use codex_protocol::config_types::ForcedLoginMethod;
@@ -118,13 +119,13 @@ async fn clear_existing_auth_before_login(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
auth_keyring_backend_kind: AuthKeyringBackendKind,
chatgpt_base_url: &str,
oauth_config: &ChatgptOAuthConfig,
) {
if let Err(err) = logout_with_revoke_for_chatgpt_base_url(
if let Err(err) = logout_with_revoke_for_oauth_config(
codex_home,
auth_credentials_store_mode,
auth_keyring_backend_kind,
Some(chatgpt_base_url),
oauth_config,
)
.await
{
@@ -137,19 +138,19 @@ pub async fn login_with_chatgpt(
forced_chatgpt_workspace_id: Option<Vec<String>>,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
auth_keyring_backend_kind: AuthKeyringBackendKind,
chatgpt_base_url: &str,
oauth_config: ChatgptOAuthConfig,
) -> std::io::Result<()> {
clear_existing_auth_before_login(
&codex_home,
cli_auth_credentials_store_mode,
auth_keyring_backend_kind,
chatgpt_base_url,
&oauth_config,
)
.await;
let opts = ServerOptions::for_chatgpt_base_url(
let opts = ServerOptions::for_oauth_config(
codex_home,
Some(chatgpt_base_url),
oauth_config,
forced_chatgpt_workspace_id,
cli_auth_credentials_store_mode,
auth_keyring_backend_kind,
@@ -178,7 +179,7 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
&config.chatgpt_base_url,
config.chatgpt_oauth.clone(),
)
.await
{
@@ -315,13 +316,13 @@ pub async fn run_login_with_device_code(
&config.codex_home,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
&config.chatgpt_base_url,
&config.chatgpt_oauth,
)
.await;
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
let mut opts = ServerOptions::for_chatgpt_base_url(
let mut opts = ServerOptions::for_oauth_config(
config.codex_home.to_path_buf(),
Some(&config.chatgpt_base_url),
config.chatgpt_oauth.clone(),
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
@@ -364,14 +365,14 @@ pub async fn run_login_with_device_code_fallback_to_browser(
&config.codex_home,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
&config.chatgpt_base_url,
&config.chatgpt_oauth,
)
.await;
let forced_chatgpt_workspace_id = config.forced_chatgpt_workspace_id.clone();
let mut opts = ServerOptions::for_chatgpt_base_url(
let mut opts = ServerOptions::for_oauth_config(
config.codex_home.to_path_buf(),
Some(&config.chatgpt_base_url),
config.chatgpt_oauth.clone(),
forced_chatgpt_workspace_id,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
@@ -472,11 +473,11 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
let config = load_config_or_exit(cli_config_overrides).await;
match logout_with_revoke_for_chatgpt_base_url(
match logout_with_revoke_for_oauth_config(
&config.codex_home,
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
Some(&config.chatgpt_base_url),
&config.chatgpt_oauth,
)
.await
{
@@ -526,6 +527,7 @@ fn safe_format_key(key: &str) -> String {
mod tests {
use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthKeyringBackendKind;
use codex_login::ChatgptOAuthConfig;
use codex_login::load_auth_dot_json;
use codex_login::login_with_api_key;
use pretty_assertions::assert_eq;
@@ -549,7 +551,7 @@ mod tests {
codex_home.path(),
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
"https://chatgpt.com/backend-api",
&ChatgptOAuthConfig::default(),
)
.await;
+11
View File
@@ -58,6 +58,14 @@ use serde::Serialize;
use serde::de::Error as SerdeError;
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct ChatgptOAuthToml {
pub issuer: String,
pub client_id: String,
}
const RESERVED_MODEL_PROVIDER_IDS: [&str; 4] = [
AMAZON_BEDROCK_PROVIDER_ID,
OPENAI_PROVIDER_ID,
@@ -357,6 +365,9 @@ pub struct ConfigToml {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: Option<String>,
/// OAuth issuer and client ID used for ChatGPT authentication.
pub chatgpt_oauth: Option<ChatgptOAuthToml>,
/// Optional product SKU forwarded on host-owned Codex Apps MCP requests.
pub apps_mcp_product_sku: Option<String>,
+1
View File
@@ -62,6 +62,7 @@ const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData";
const PROJECT_LOCAL_CONFIG_DENYLIST: &[&str] = &[
"openai_base_url",
"chatgpt_base_url",
"chatgpt_oauth",
"apps_mcp_product_sku",
"model_provider",
"model_providers",
+2
View File
@@ -3,6 +3,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::config_toml::ChatgptOAuthToml;
use crate::config_toml::ToolsToml;
use crate::types::AnalyticsConfigToml;
use crate::types::ApprovalsReviewer;
@@ -40,6 +41,7 @@ pub struct ConfigProfile {
pub model_catalog_json: Option<AbsolutePathBuf>,
pub personality: Option<Personality>,
pub chatgpt_base_url: Option<String>,
pub chatgpt_oauth: Option<ChatgptOAuthToml>,
/// Optional path to a file containing model instructions.
pub model_instructions_file: Option<AbsolutePathBuf>,
/// Deprecated: ignored.
+28 -1
View File
@@ -351,6 +351,22 @@
},
"type": "object"
},
"ChatgptOAuthToml": {
"additionalProperties": false,
"properties": {
"client_id": {
"type": "string"
},
"issuer": {
"type": "string"
}
},
"required": [
"client_id",
"issuer"
],
"type": "object"
},
"CodeModeConfigToml": {
"additionalProperties": false,
"properties": {
@@ -383,6 +399,9 @@
"chatgpt_base_url": {
"type": "string"
},
"chatgpt_oauth": {
"$ref": "#/definitions/ChatgptOAuthToml"
},
"experimental_compact_prompt_file": {
"$ref": "#/definitions/AbsolutePathBuf"
},
@@ -4458,6 +4477,14 @@
"description": "Base URL for requests to ChatGPT (as opposed to the OpenAI API).",
"type": "string"
},
"chatgpt_oauth": {
"allOf": [
{
"$ref": "#/definitions/ChatgptOAuthToml"
}
],
"description": "OAuth issuer and client ID used for ChatGPT authentication."
},
"check_for_update_on_startup": {
"description": "When `true`, checks for Codex updates on startup and surfaces update prompts. Set to `false` only if your Codex updates are centrally managed. Defaults to `true`.",
"type": "boolean"
@@ -5274,4 +5301,4 @@
},
"title": "ConfigToml",
"type": "object"
}
}
@@ -2925,6 +2925,7 @@ model = "project-model"
model_instructions_file = "instructions.md"
openai_base_url = "https://attacker.example/v1"
chatgpt_base_url = "https://attacker.example/backend-api"
chatgpt_oauth = { issuer = "https://attacker.example", client_id = "attacker" }
apps_mcp_product_sku = "attacker"
model_provider = "attacker"
notify = ["sh", "-c", "echo attacker"]
@@ -2976,6 +2977,7 @@ wire_api = "responses"
let ignored_project_config_keys = vec![
"openai_base_url",
"chatgpt_base_url",
"chatgpt_oauth",
"apps_mcp_product_sku",
"model_provider",
"model_providers",
+27
View File
@@ -1312,6 +1312,33 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s
Ok(())
}
#[tokio::test]
async fn chatgpt_oauth_config_overrides_production_defaults() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
[chatgpt_oauth]
issuer = "https://auth.example.com"
client_id = "custom-client"
"#,
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.build()
.await?;
assert_eq!(
config.chatgpt_oauth,
ChatgptOAuthConfig::new(
"custom-client".to_string(),
"https://auth.example.com".to_string(),
)
);
Ok(())
}
#[tokio::test]
async fn network_proxy_cli_overrides_merge_toggle_with_proxy_config() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
+15
View File
@@ -22,6 +22,7 @@ use codex_config::ResidencyRequirement;
use codex_config::SandboxModeRequirement;
use codex_config::Sourced;
use codex_config::ThreadConfigLoader;
use codex_config::config_toml::ChatgptOAuthToml;
use codex_config::config_toml::ConfigLockfileToml;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::DEFAULT_PROJECT_DOC_MAX_BYTES;
@@ -69,6 +70,7 @@ use codex_features::NetworkProxyConfigToml;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_install_context::InstallContext;
use codex_login::AuthManagerConfig;
use codex_login::ChatgptOAuthConfig;
use codex_mcp::McpConfig;
use codex_mcp::McpServerRegistration;
use codex_mcp::ResolvedMcpCatalog;
@@ -937,6 +939,9 @@ pub struct Config {
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
pub chatgpt_base_url: String,
/// OAuth issuer and client ID used for ChatGPT authentication.
pub chatgpt_oauth: ChatgptOAuthConfig,
/// Optional product SKU forwarded to the host-owned apps MCP server.
pub apps_mcp_product_sku: Option<String>,
@@ -1133,6 +1138,10 @@ impl AuthManagerConfig for Config {
fn chatgpt_base_url(&self) -> String {
self.chatgpt_base_url.clone()
}
fn chatgpt_oauth(&self) -> ChatgptOAuthConfig {
self.chatgpt_oauth.clone()
}
}
#[derive(Clone, Default)]
@@ -3579,6 +3588,12 @@ impl Config {
chatgpt_base_url: cfg
.chatgpt_base_url
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
chatgpt_oauth: cfg.chatgpt_oauth.map_or_else(
ChatgptOAuthConfig::default,
|ChatgptOAuthToml { issuer, client_id }| {
ChatgptOAuthConfig::new(client_id, issuer)
},
),
apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(),
realtime_audio: cfg
.audio
+3
View File
@@ -518,6 +518,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
updated_auth_dot_json,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
&ChatgptOAuthConfig::default(),
AuthKeyringBackendKind::Direct,
)
.await
@@ -1004,6 +1005,7 @@ async fn auth_manager_rejects_env_personal_access_token_workspace_mismatch() {
/*forced_chatgpt_workspace_id*/
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
/*chatgpt_base_url*/ None,
ChatgptOAuthConfig::default(),
AuthKeyringBackendKind::default(),
)
.await;
@@ -1055,6 +1057,7 @@ async fn auth_manager_rejects_stored_personal_access_token_workspace_mismatch()
/*forced_chatgpt_workspace_id*/
Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
/*chatgpt_base_url*/ None,
ChatgptOAuthConfig::default(),
AuthKeyringBackendKind::default(),
)
.await;
+68 -9
View File
@@ -216,6 +216,7 @@ impl CodexAuth {
auth_dot_json: AuthDotJson,
auth_credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: Option<&str>,
oauth_config: &ChatgptOAuthConfig,
keyring_backend_kind: AuthKeyringBackendKind,
) -> std::io::Result<Self> {
let auth_mode = auth_dot_json.resolved_mode();
@@ -255,7 +256,7 @@ impl CodexAuth {
let state = ChatgptAuthState {
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
client,
oauth_config: ChatgptOAuthConfig::for_chatgpt_base_url(chatgpt_base_url),
oauth_config: oauth_config.clone(),
};
match auth_mode {
@@ -605,20 +606,20 @@ pub async fn logout_with_revoke(
auth_credentials_store_mode: AuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyringBackendKind,
) -> std::io::Result<bool> {
logout_with_revoke_for_chatgpt_base_url(
logout_with_revoke_for_oauth_config(
codex_home,
auth_credentials_store_mode,
keyring_backend_kind,
/*chatgpt_base_url*/ None,
&ChatgptOAuthConfig::default(),
)
.await
}
pub async fn logout_with_revoke_for_chatgpt_base_url(
pub async fn logout_with_revoke_for_oauth_config(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
keyring_backend_kind: AuthKeyringBackendKind,
chatgpt_base_url: Option<&str>,
oauth_config: &ChatgptOAuthConfig,
) -> std::io::Result<bool> {
let auth_dot_json = match load_auth_dot_json(
codex_home,
@@ -631,8 +632,7 @@ pub async fn logout_with_revoke_for_chatgpt_base_url(
None
}
};
let oauth_config = ChatgptOAuthConfig::for_chatgpt_base_url(chatgpt_base_url);
if let Err(err) = revoke_auth_tokens(auth_dot_json.as_ref(), &oauth_config).await {
if let Err(err) = revoke_auth_tokens(auth_dot_json.as_ref(), oauth_config).await {
tracing::warn!("failed to revoke auth tokens during logout: {err}");
}
logout_all_stores(
@@ -937,6 +937,27 @@ async fn load_auth(
forced_chatgpt_workspace_id: Option<&[String]>,
chatgpt_base_url: Option<&str>,
keyring_backend_kind: AuthKeyringBackendKind,
) -> std::io::Result<Option<CodexAuth>> {
load_auth_with_oauth_config(
codex_home,
enable_codex_api_key_env,
auth_credentials_store_mode,
forced_chatgpt_workspace_id,
chatgpt_base_url,
&ChatgptOAuthConfig::default(),
keyring_backend_kind,
)
.await
}
async fn load_auth_with_oauth_config(
codex_home: &Path,
enable_codex_api_key_env: bool,
auth_credentials_store_mode: AuthCredentialsStoreMode,
forced_chatgpt_workspace_id: Option<&[String]>,
chatgpt_base_url: Option<&str>,
oauth_config: &ChatgptOAuthConfig,
keyring_backend_kind: AuthKeyringBackendKind,
) -> std::io::Result<Option<CodexAuth>> {
// API key via env var takes precedence over any other auth method.
if enable_codex_api_key_env && let Some(api_key) = read_codex_api_key_from_env() {
@@ -956,6 +977,7 @@ async fn load_auth(
auth_dot_json,
AuthCredentialsStoreMode::Ephemeral,
chatgpt_base_url,
oauth_config,
keyring_backend_kind,
)
.await?;
@@ -1001,6 +1023,7 @@ async fn load_auth(
auth_dot_json,
auth_credentials_store_mode,
chatgpt_base_url,
oauth_config,
keyring_backend_kind,
)
.await?;
@@ -1501,6 +1524,7 @@ pub struct AuthManager {
keyring_backend_kind: AuthKeyringBackendKind,
forced_chatgpt_workspace_id: RwLock<Option<Vec<String>>>,
chatgpt_base_url: Option<String>,
oauth_config: ChatgptOAuthConfig,
refresh_lock: Semaphore,
external_auth: RwLock<Option<Arc<dyn ExternalAuth>>>,
}
@@ -1526,6 +1550,9 @@ pub trait AuthManagerConfig {
/// Returns the ChatGPT backend base URL used for first-party backend authorization.
fn chatgpt_base_url(&self) -> String;
/// Returns the OAuth issuer and client ID used for ChatGPT authentication.
fn chatgpt_oauth(&self) -> ChatgptOAuthConfig;
}
impl Debug for AuthManager {
@@ -1544,6 +1571,7 @@ impl Debug for AuthManager {
&self.forced_chatgpt_workspace_id,
)
.field("chatgpt_base_url", &self.chatgpt_base_url)
.field("oauth_config", &self.oauth_config)
.field("has_external_auth", &self.has_external_auth())
.finish_non_exhaustive()
}
@@ -1567,6 +1595,7 @@ impl AuthManager {
auth_credentials_store_mode,
/*forced_chatgpt_workspace_id*/ None,
chatgpt_base_url,
ChatgptOAuthConfig::default(),
keyring_backend_kind,
)
.await
@@ -1578,14 +1607,16 @@ impl AuthManager {
auth_credentials_store_mode: AuthCredentialsStoreMode,
forced_chatgpt_workspace_id: Option<Vec<String>>,
chatgpt_base_url: Option<String>,
oauth_config: ChatgptOAuthConfig,
keyring_backend_kind: AuthKeyringBackendKind,
) -> Self {
let managed_auth = load_auth(
let managed_auth = load_auth_with_oauth_config(
&codex_home,
enable_codex_api_key_env,
auth_credentials_store_mode,
forced_chatgpt_workspace_id.as_deref(),
chatgpt_base_url.as_deref(),
&oauth_config,
keyring_backend_kind,
)
.await
@@ -1604,6 +1635,7 @@ impl AuthManager {
keyring_backend_kind,
forced_chatgpt_workspace_id: RwLock::new(forced_chatgpt_workspace_id),
chatgpt_base_url,
oauth_config,
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(None),
}
@@ -1626,6 +1658,7 @@ impl AuthManager {
keyring_backend_kind: AuthKeyringBackendKind::default(),
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: None,
oauth_config: ChatgptOAuthConfig::default(),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(None),
})
@@ -1647,6 +1680,7 @@ impl AuthManager {
keyring_backend_kind: AuthKeyringBackendKind::default(),
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: None,
oauth_config: ChatgptOAuthConfig::default(),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(None),
})
@@ -1666,6 +1700,7 @@ impl AuthManager {
keyring_backend_kind: AuthKeyringBackendKind::default(),
forced_chatgpt_workspace_id: RwLock::new(None),
chatgpt_base_url: None,
oauth_config: ChatgptOAuthConfig::default(),
refresh_lock: Semaphore::new(/*permits*/ 1),
external_auth: RwLock::new(Some(
Arc::new(BearerTokenRefresher::new(config)) as Arc<dyn ExternalAuth>
@@ -1806,12 +1841,13 @@ impl AuthManager {
async fn load_auth_from_storage(&self) -> Option<CodexAuth> {
let forced_chatgpt_workspace_id = self.forced_chatgpt_workspace_id();
load_auth(
load_auth_with_oauth_config(
&self.codex_home,
self.enable_codex_api_key_env,
self.auth_credentials_store_mode,
forced_chatgpt_workspace_id.as_deref(),
self.chatgpt_base_url.as_deref(),
&self.oauth_config,
self.keyring_backend_kind,
)
.await
@@ -1900,6 +1936,28 @@ impl AuthManager {
)
}
pub async fn shared_with_oauth_config(
codex_home: PathBuf,
enable_codex_api_key_env: bool,
auth_credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: Option<String>,
oauth_config: ChatgptOAuthConfig,
keyring_backend_kind: AuthKeyringBackendKind,
) -> Arc<Self> {
Arc::new(
Self::new_with_workspace_restriction(
codex_home,
enable_codex_api_key_env,
auth_credentials_store_mode,
/*forced_chatgpt_workspace_id*/ None,
chatgpt_base_url,
oauth_config,
keyring_backend_kind,
)
.await,
)
}
/// Convenience constructor returning an `Arc` wrapper from resolved config.
pub async fn shared_from_config(
config: &impl AuthManagerConfig,
@@ -1912,6 +1970,7 @@ impl AuthManager {
config.cli_auth_credentials_store_mode(),
config.forced_chatgpt_workspace_id(),
Some(config.chatgpt_base_url()),
config.chatgpt_oauth(),
config.auth_keyring_backend_kind(),
)
.await,
+1 -2
View File
@@ -14,7 +14,6 @@ pub use device_code_auth::complete_device_code_login;
pub use device_code_auth::request_device_code;
pub use device_code_auth::run_device_code_login;
pub use oauth::ChatgptOAuthConfig;
pub use oauth::STAGING_CLIENT_ID;
pub use server::LoginServer;
pub use server::ServerOptions;
pub use server::ShutdownHandle;
@@ -48,7 +47,7 @@ pub use auth::login_with_api_key;
pub use auth::login_with_bedrock_api_key;
pub use auth::logout;
pub use auth::logout_with_revoke;
pub use auth::logout_with_revoke_for_chatgpt_base_url;
pub use auth::logout_with_revoke_for_oauth_config;
pub use auth::read_codex_access_token_from_env;
pub use auth::read_openai_api_key_from_env;
pub use auth::save_auth;
+1 -34
View File
@@ -1,11 +1,5 @@
use url::Url;
pub const PROD_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
pub const STAGING_CLIENT_ID: &str = "app_WWpKUzlOnCTqf9WmuzvqovoW";
const PROD_ISSUER: &str = "https://auth.openai.com";
const STAGING_ISSUER: &str = "https://auth.api.openai.org";
const STAGING_CHATGPT_HOST: &str = "chatgpt-staging.com";
pub const PROD_ISSUER: &str = "https://auth.openai.com";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChatgptOAuthConfig {
@@ -24,14 +18,6 @@ impl ChatgptOAuthConfig {
Self { client_id, issuer }
}
pub fn for_chatgpt_base_url(chatgpt_base_url: Option<&str>) -> Self {
if chatgpt_base_url.is_some_and(is_staging_chatgpt_base_url) {
return Self::staging();
}
Self::prod()
}
pub fn token_url(&self) -> String {
self.oauth_endpoint("token")
}
@@ -44,26 +30,7 @@ impl ChatgptOAuthConfig {
Self::new(PROD_CLIENT_ID.to_string(), PROD_ISSUER.to_string())
}
fn staging() -> Self {
Self::new(STAGING_CLIENT_ID.to_string(), STAGING_ISSUER.to_string())
}
fn oauth_endpoint(&self, endpoint: &str) -> String {
format!("{}/oauth/{endpoint}", self.issuer.trim_end_matches('/'))
}
}
fn is_staging_chatgpt_base_url(chatgpt_base_url: &str) -> bool {
let Ok(url) = Url::parse(chatgpt_base_url) else {
return false;
};
let Some(host) = url.host_str() else {
return false;
};
host == STAGING_CHATGPT_HOST || host.ends_with(&format!(".{STAGING_CHATGPT_HOST}"))
}
#[cfg(test)]
#[path = "oauth_tests.rs"]
mod tests;
-33
View File
@@ -1,33 +0,0 @@
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn uses_staging_oauth_for_staging_chatgpt_base_url() {
assert_eq!(
ChatgptOAuthConfig::for_chatgpt_base_url(Some("https://chatgpt-staging.com/backend-api/")),
ChatgptOAuthConfig::staging()
);
}
#[test]
fn uses_staging_oauth_for_staging_chatgpt_subdomain() {
assert_eq!(
ChatgptOAuthConfig::for_chatgpt_base_url(Some(
"https://dev-foo.chatgpt-staging.com/backend-api/"
)),
ChatgptOAuthConfig::staging()
);
}
#[test]
fn leaves_non_staging_chatgpt_base_urls_on_prod_oauth() {
assert_eq!(
ChatgptOAuthConfig::for_chatgpt_base_url(Some("https://chatgpt.com/backend-api/")),
ChatgptOAuthConfig::prod()
);
assert_eq!(
ChatgptOAuthConfig::for_chatgpt_base_url(Some("not a url")),
ChatgptOAuthConfig::prod()
);
}
+2 -3
View File
@@ -96,14 +96,13 @@ impl ServerOptions {
}
}
pub fn for_chatgpt_base_url(
pub fn for_oauth_config(
codex_home: PathBuf,
chatgpt_base_url: Option<&str>,
oauth_config: ChatgptOAuthConfig,
forced_chatgpt_workspace_id: Option<Vec<String>>,
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
auth_keyring_backend_kind: AuthKeyringBackendKind,
) -> Self {
let oauth_config = ChatgptOAuthConfig::for_chatgpt_base_url(chatgpt_base_url);
let mut opts = Self::new(
codex_home,
oauth_config.client_id,
+15 -10
View File
@@ -8,9 +8,9 @@ use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthDotJson;
use codex_login::AuthKeyringBackendKind;
use codex_login::AuthManager;
use codex_login::ChatgptOAuthConfig;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_login::RefreshTokenError;
use codex_login::STAGING_CLIENT_ID;
use codex_login::load_auth_dot_json;
use codex_login::save_auth;
use codex_login::token_data::IdTokenInfo;
@@ -32,6 +32,7 @@ use wiremock::matchers::path;
const INITIAL_ACCESS_TOKEN: &str = "initial-access-token";
const INITIAL_REFRESH_TOKEN: &str = "initial-refresh-token";
const CUSTOM_OAUTH_CLIENT_ID: &str = "custom-oauth-client";
#[serial_test::serial(auth_refresh)]
#[tokio::test]
@@ -101,14 +102,14 @@ async fn refresh_token_succeeds_updates_storage() -> Result<()> {
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn staging_refresh_uses_staging_oauth_client_id() -> Result<()> {
async fn refresh_uses_configured_oauth_client_id() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.and(body_json(json!({
"client_id": STAGING_CLIENT_ID,
"client_id": CUSTOM_OAUTH_CLIENT_ID,
"grant_type": "refresh_token",
"refresh_token": INITIAL_REFRESH_TOKEN,
})))
@@ -120,9 +121,12 @@ async fn staging_refresh_uses_staging_oauth_client_id() -> Result<()> {
.mount(&server)
.await;
let ctx = RefreshTokenTestContext::new_with_chatgpt_base_url(
let ctx = RefreshTokenTestContext::new_with_oauth_config(
&server,
Some("https://chatgpt-staging.com/backend-api/"),
ChatgptOAuthConfig::new(
CUSTOM_OAUTH_CLIENT_ID.to_string(),
"https://auth.example.com".to_string(),
),
)
.await?;
let initial_auth = AuthDotJson {
@@ -1241,23 +1245,24 @@ struct RefreshTokenTestContext {
impl RefreshTokenTestContext {
async fn new(server: &MockServer) -> Result<Self> {
Self::new_with_chatgpt_base_url(server, /*chatgpt_base_url*/ None).await
Self::new_with_oauth_config(server, ChatgptOAuthConfig::default()).await
}
async fn new_with_chatgpt_base_url(
async fn new_with_oauth_config(
server: &MockServer,
chatgpt_base_url: Option<&str>,
oauth_config: ChatgptOAuthConfig,
) -> Result<Self> {
let codex_home = TempDir::new()?;
let endpoint = format!("{}/oauth/token", server.uri());
let env_guard = EnvGuard::set(REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR, endpoint);
let auth_manager = AuthManager::shared(
let auth_manager = AuthManager::shared_with_oauth_config(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
chatgpt_base_url.map(str::to_string),
/*chatgpt_base_url*/ None,
oauth_config,
AuthKeyringBackendKind::default(),
)
.await;