mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
login: configure ChatGPT OAuth explicitly
This commit is contained in:
+20
-18
@@ -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;
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user