Compare commits

...

4 Commits

Author SHA1 Message Date
Ahmed Ibrahim
144a0fb312 fix 2026-03-19 00:04:27 -07:00
Ahmed Ibrahim
2c1d6d4e6a codex: fix CI failure on PR #15150
Make auth testing constructors public for codex-core callers across the new crate boundary.

Co-authored-by: Codex <noreply@openai.com>
2026-03-18 23:59:11 -07:00
Ahmed Ibrahim
1412ccd128 codex: fix CI failure on PR #15150
Fix moved auth test imports, add auth-crate test deps, and expose testing constructors used by codex-core.

Co-authored-by: Codex <noreply@openai.com>
2026-03-18 23:56:40 -07:00
Ahmed Ibrahim
87d9fe453a Make core auth a separate crate
Move auth-owned source and tests into core/src/auth as a standalone workspace crate, and keep codex-core consuming it via re-exports.

Co-authored-by: Codex <noreply@openai.com>
2026-03-18 23:46:01 -07:00
13 changed files with 166 additions and 64 deletions

29
codex-rs/Cargo.lock generated
View File

@@ -1596,6 +1596,34 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "codex-auth"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-client",
"codex-keyring-store",
"codex-otel",
"codex-protocol",
"keyring",
"once_cell",
"pretty_assertions",
"reqwest",
"schemars 0.8.22",
"serde",
"serde_json",
"serial_test",
"sha2",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]]
name = "codex-backend-client"
version = "0.0.0"
@@ -1840,6 +1868,7 @@ dependencies = [
"codex-arg0",
"codex-artifacts",
"codex-async-utils",
"codex-auth",
"codex-client",
"codex-config",
"codex-connectors",

View File

@@ -10,6 +10,7 @@ members = [
"debug-client",
"apply-patch",
"arg0",
"core/src/auth",
"feedback",
"codex-backend-openapi-models",
"cloud-requirements",
@@ -97,6 +98,7 @@ codex-app-server-test-client = { path = "app-server-test-client" }
codex-apply-patch = { path = "apply-patch" }
codex-arg0 = { path = "arg0" }
codex-async-utils = { path = "async-utils" }
codex-auth = { path = "core/src/auth" }
codex-backend-client = { path = "backend-client" }
codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli" }

View File

@@ -31,6 +31,7 @@ codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-auth = { workspace = true }
codex-client = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }

View File

@@ -0,0 +1,39 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-auth"
version.workspace = true
[lib]
doctest = false
name = "codex_auth"
path = "lib.rs"
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-app-server-protocol = { workspace = true }
codex-client = { workspace = true }
codex-keyring-store = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
once_cell = { workspace = true }
reqwest = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
keyring = { workspace = true }
pretty_assertions = { workspace = true }
serial_test = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

View File

@@ -1,5 +1,3 @@
mod storage;
use async_trait::async_trait;
use chrono::Utc;
use reqwest::StatusCode;
@@ -19,13 +17,12 @@ use codex_app_server_protocol::AuthMode as ApiAuthMode;
use codex_otel::TelemetryAuthMode;
use codex_protocol::config_types::ForcedLoginMethod;
pub use crate::auth::storage::AuthCredentialsStoreMode;
pub use crate::auth::storage::AuthDotJson;
use crate::auth::storage::AuthStorageBackend;
use crate::auth::storage::create_auth_storage;
use crate::config::Config;
use crate::error::RefreshTokenFailedError;
use crate::error::RefreshTokenFailedReason;
pub use crate::storage::AuthCredentialsStoreMode;
pub use crate::storage::AuthDotJson;
use crate::storage::AuthStorageBackend;
use crate::storage::create_auth_storage;
use crate::token_data::KnownPlan as InternalKnownPlan;
use crate::token_data::PlanType as InternalPlanType;
use crate::token_data::TokenData;
@@ -335,7 +332,7 @@ impl CodexAuth {
last_refresh: Some(Utc::now()),
};
let client = crate::default_client::create_client();
let client = create_client();
let state = ChatgptAuthState {
auth_dot_json: Arc::new(Mutex::new(Some(auth_dot_json))),
client,
@@ -351,7 +348,7 @@ impl CodexAuth {
}
pub fn from_api_key(api_key: &str) -> Self {
Self::from_api_key_with_client(api_key, crate::default_client::create_client())
Self::from_api_key_with_client(api_key, create_client())
}
}
@@ -458,11 +455,19 @@ pub fn load_auth_dot_json(
storage.load()
}
pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> {
#[derive(Debug, Clone, PartialEq, Eq)]
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 fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
let Some(auth) = load_auth(
&config.codex_home,
/*enable_codex_api_key_env*/ true,
config.cli_auth_credentials_store_mode,
config.auth_credentials_store_mode,
)?
else {
return Ok(());
@@ -486,7 +491,7 @@ pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> {
return logout_with_message(
&config.codex_home,
message,
config.cli_auth_credentials_store_mode,
config.auth_credentials_store_mode,
);
}
}
@@ -504,7 +509,7 @@ pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> {
format!(
"Failed to load ChatGPT credentials while enforcing workspace restrictions: {err}. Logging out."
),
config.cli_auth_credentials_store_mode,
config.auth_credentials_store_mode,
);
}
};
@@ -523,7 +528,7 @@ pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> {
return logout_with_message(
&config.codex_home,
message,
config.cli_auth_credentials_store_mode,
config.auth_credentials_store_mode,
);
}
}
@@ -564,13 +569,13 @@ fn load_auth(
auth_credentials_store_mode: AuthCredentialsStoreMode,
) -> std::io::Result<Option<CodexAuth>> {
let build_auth = |auth_dot_json: AuthDotJson, storage_mode| {
let client = crate::default_client::create_client();
let client = create_client();
CodexAuth::from_auth_dot_json(codex_home, auth_dot_json, storage_mode, client)
};
// 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() {
let client = crate::default_client::create_client();
let client = create_client();
return Ok(Some(CodexAuth::from_api_key_with_client(
api_key.as_str(),
client,
@@ -1077,7 +1082,7 @@ impl AuthManager {
}
/// Create an AuthManager with a specific CodexAuth, for testing only.
pub(crate) fn from_auth_for_testing(auth: CodexAuth) -> Arc<Self> {
pub fn from_auth_for_testing(auth: CodexAuth) -> Arc<Self> {
let cached = CachedAuth {
auth: Some(auth),
external_refresher: None,
@@ -1093,10 +1098,7 @@ impl AuthManager {
}
/// Create an AuthManager with a specific CodexAuth and codex home, for testing only.
pub(crate) fn from_auth_for_testing_with_home(
auth: CodexAuth,
codex_home: PathBuf,
) -> Arc<Self> {
pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc<Self> {
let cached = CachedAuth {
auth: Some(auth),
external_refresher: None,
@@ -1449,3 +1451,7 @@ impl AuthManager {
#[cfg(test)]
#[path = "auth_tests.rs"]
mod tests;
fn create_client() -> CodexHttpClient {
CodexHttpClient::new(reqwest::Client::new())
}

View File

@@ -1,8 +1,6 @@
use super::*;
use crate::auth::storage::FileAuthStorage;
use crate::auth::storage::get_auth_file;
use crate::config::Config;
use crate::config::ConfigBuilder;
use crate::storage::FileAuthStorage;
use crate::storage::get_auth_file;
use crate::token_data::IdTokenInfo;
use crate::token_data::KnownPlan as InternalKnownPlan;
use crate::token_data::PlanType as InternalPlanType;
@@ -260,15 +258,13 @@ async fn build_config(
codex_home: &Path,
forced_login_method: Option<ForcedLoginMethod>,
forced_chatgpt_workspace_id: Option<String>,
) -> Config {
let mut config = ConfigBuilder::default()
.codex_home(codex_home.to_path_buf())
.build()
.await
.expect("config should load");
config.forced_login_method = forced_login_method;
config.forced_chatgpt_workspace_id = forced_chatgpt_workspace_id;
config
) -> AuthConfig {
AuthConfig {
codex_home: codex_home.to_path_buf(),
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
forced_login_method,
forced_chatgpt_workspace_id,
}
}
/// Use sparingly.

View File

@@ -0,0 +1,25 @@
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{message}")]
pub struct RefreshTokenFailedError {
pub reason: RefreshTokenFailedReason,
pub message: String,
}
impl RefreshTokenFailedError {
pub fn new(reason: RefreshTokenFailedReason, message: impl Into<String>) -> Self {
Self {
reason,
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshTokenFailedReason {
Expired,
Exhausted,
Revoked,
Other,
}

View File

@@ -0,0 +1,10 @@
pub mod error;
mod storage;
pub mod token_data;
mod util;
mod auth;
pub use auth::*;
pub use error::RefreshTokenFailedError;
pub use error::RefreshTokenFailedReason;

View File

@@ -27,7 +27,7 @@ pub struct IdTokenInfo {
/// The ChatGPT subscription plan type
/// (e.g., "free", "plus", "pro", "business", "enterprise", "edu").
/// (Note: values may vary by backend.)
pub(crate) chatgpt_plan_type: Option<PlanType>,
pub chatgpt_plan_type: Option<PlanType>,
/// ChatGPT user identifier associated with the token, if present.
pub chatgpt_user_id: Option<String>,
/// Organization/workspace identifier associated with the token, if present.
@@ -55,13 +55,13 @@ impl IdTokenInfo {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum PlanType {
pub enum PlanType {
Known(KnownPlan),
Unknown(String),
}
impl PlanType {
pub(crate) fn from_raw_value(raw: &str) -> Self {
pub fn from_raw_value(raw: &str) -> Self {
match raw.to_ascii_lowercase().as_str() {
"free" => Self::Known(KnownPlan::Free),
"go" => Self::Known(KnownPlan::Go),
@@ -78,7 +78,7 @@ impl PlanType {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum KnownPlan {
pub enum KnownPlan {
Free,
Go,
Plus,

View File

@@ -0,0 +1,16 @@
use tracing::debug;
pub(crate) fn try_parse_error_message(text: &str) -> String {
debug!("Parsing server error response: {}", text);
let json = serde_json::from_str::<serde_json::Value>(text).unwrap_or_default();
if let Some(error) = json.get("error")
&& let Some(message) = error.get("message")
&& let Some(message_str) = message.as_str()
{
return message_str.to_string();
}
if text.is_empty() {
return "Unknown error".to_string();
}
text.to_string()
}

View File

@@ -9,6 +9,8 @@ use chrono::Datelike;
use chrono::Local;
use chrono::Utc;
use codex_async_utils::CancelErr;
pub use codex_auth::RefreshTokenFailedError;
pub use codex_auth::RefreshTokenFailedReason;
use codex_protocol::ThreadId;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::ErrorEvent;
@@ -261,30 +263,6 @@ impl std::fmt::Display for ResponseStreamFailed {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{message}")]
pub struct RefreshTokenFailedError {
pub reason: RefreshTokenFailedReason,
pub message: String,
}
impl RefreshTokenFailedError {
pub fn new(reason: RefreshTokenFailedReason, message: impl Into<String>) -> Self {
Self {
reason,
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshTokenFailedReason {
Expired,
Exhausted,
Revoked,
Other,
}
#[derive(Debug)]
pub struct UnexpectedResponseError {
pub status: StatusCode,

View File

@@ -10,7 +10,7 @@ pub mod api_bridge;
mod apply_patch;
mod apps;
mod arc_monitor;
pub mod auth;
pub use codex_auth as auth;
mod auth_env_telemetry;
mod client;
mod client_common;
@@ -76,7 +76,7 @@ mod shell_detect;
mod stream_events_utils;
pub mod test_support;
mod text_encoding;
pub mod token_data;
pub use codex_auth::token_data;
mod truncate;
mod unified_exec;
pub mod windows_sandbox;