Compare commits

...

25 Commits

Author SHA1 Message Date
Ahmed Ibrahim
0461e2a98b unify 2026-03-19 18:03:53 -07:00
Ahmed Ibrahim
112d231f85 unify 2026-03-19 18:01:35 -07:00
Ahmed Ibrahim
178a0dc31c fix 2026-03-19 17:17:02 -07:00
Ahmed Ibrahim
d3c83a408a fix 2026-03-19 17:10:18 -07:00
Ahmed Ibrahim
e57552f3e9 fix 2026-03-19 17:10:01 -07:00
Ahmed Ibrahim
fa2b69dc78 fix 2026-03-19 15:27:47 -07:00
Ahmed Ibrahim
7c9e4b492f fix 2026-03-19 15:19:41 -07:00
Ahmed Ibrahim
7c374c18e0 fix 2026-03-19 15:18:21 -07:00
Ahmed Ibrahim
37e4e55475 fix 2026-03-19 15:14:24 -07:00
Ahmed Ibrahim
b6c3ecfd8b fix 2026-03-19 15:06:27 -07:00
Ahmed Ibrahim
019ab69ee5 fix 2026-03-19 15:04:45 -07:00
Ahmed Ibrahim
c13f83d8de fix 2026-03-19 14:58:47 -07:00
Ahmed Ibrahim
e73bd63352 fix 2026-03-19 14:56:52 -07:00
Ahmed Ibrahim
589fe5ffd9 fix 2026-03-19 14:54:26 -07:00
Ahmed Ibrahim
5c3bdad1a8 fix 2026-03-19 14:53:41 -07:00
Ahmed Ibrahim
e2cd38a787 fix 2026-03-19 14:48:19 -07:00
Ahmed Ibrahim
cfbec84964 Merge branch 'main' into auth-crate-split 2026-03-19 14:08:24 -07:00
Ahmed Ibrahim
499c3190e0 codex: restore login auth API surface on PR #15150
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 14:05:58 -07:00
Ahmed Ibrahim
59c056e43d codex: avoid default_client imports in login on PR #15150
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 13:58:16 -07:00
Ahmed Ibrahim
905255deff codex: fix CI failure on PR #15150
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 13:52:08 -07:00
Ahmed Ibrahim
a7173968c6 codex: clean up auth import style on PR #15150
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 13:46:37 -07:00
Ahmed Ibrahim
4b9dde5ebb Move auth code into login crate
- Move the auth implementation and token data into .
- Keep  re-exporting that surface from  for existing callers.

Co-authored-by: Codex <noreply@openai.com>
2026-03-19 13:45:14 -07:00
Ahmed Ibrahim
9e6c404012 codex: fix CI failure on PR #15216
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 13:39:49 -07:00
Ahmed Ibrahim
3661c085de Rename terminal crate to terminal-detection
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 13:13:26 -07:00
Ahmed Ibrahim
df7a587291 Move terminal module to its own crate
Co-authored-by: Codex <noreply@openai.com>
2026-03-19 12:53:14 -07:00
35 changed files with 262 additions and 209 deletions

20
codex-rs/Cargo.lock generated
View File

@@ -1841,7 +1841,6 @@ dependencies = [
"codex-arg0",
"codex-artifacts",
"codex-async-utils",
"codex-client",
"codex-config",
"codex-connectors",
"codex-exec-server",
@@ -1849,7 +1848,7 @@ dependencies = [
"codex-file-search",
"codex-git",
"codex-hooks",
"codex-keyring-store",
"codex-login",
"codex-network-proxy",
"codex-otel",
"codex-protocol",
@@ -1886,7 +1885,6 @@ dependencies = [
"image",
"indexmap 2.13.0",
"insta",
"keyring",
"landlock",
"libc",
"maplit",
@@ -1895,7 +1893,6 @@ dependencies = [
"openssl-sys",
"opentelemetry",
"opentelemetry_sdk",
"os_info",
"predicates",
"pretty_assertions",
"rand 0.9.2",
@@ -1909,7 +1906,6 @@ dependencies = [
"serde_yaml",
"serial_test",
"sha1",
"sha2",
"shlex",
"similar",
"tempfile",
@@ -2172,19 +2168,30 @@ name = "codex-login"
version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-client",
"codex-core",
"codex-config",
"codex-keyring-store",
"codex-protocol",
"codex-terminal-detection",
"core_test_support",
"keyring",
"once_cell",
"os_info",
"pretty_assertions",
"rand 0.9.2",
"regex-lite",
"reqwest",
"schemars 0.8.22",
"serde",
"serde_json",
"serial_test",
"sha2",
"tempfile",
"thiserror 2.0.18",
"tiny_http",
"tokio",
"tracing",
@@ -2276,6 +2283,7 @@ version = "0.0.0"
dependencies = [
"chrono",
"codex-api",
"codex-app-server-protocol",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-string",

View File

@@ -191,7 +191,6 @@ use codex_core::ThreadSortKey as CoreThreadSortKey;
use codex_core::auth::AuthMode as CoreAuthMode;
use codex_core::auth::CLIENT_ID;
use codex_core::auth::login_with_api_key;
use codex_core::auth::login_with_chatgpt_auth_tokens;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata;
@@ -242,6 +241,7 @@ use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
use codex_feedback::CodexFeedback;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
use codex_login::auth::login_with_chatgpt_auth_tokens;
use codex_login::run_login_server;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationMode;
@@ -1411,7 +1411,7 @@ impl CodexMessageProcessor {
let account = match self.auth_manager.auth_cached() {
Some(auth) => match auth.auth_mode() {
CoreAuthMode::ApiKey => Some(Account::ApiKey {}),
CoreAuthMode::Chatgpt => {
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
let email = auth.get_account_email();
let plan_type = auth.account_plan_type();

View File

@@ -50,10 +50,6 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::AnalyticsEventsClient;
use codex_core::AuthManager;
use codex_core::ThreadManager;
use codex_core::auth::ExternalAuthRefreshContext;
use codex_core::auth::ExternalAuthRefreshReason;
use codex_core::auth::ExternalAuthRefresher;
use codex_core::auth::ExternalAuthTokens;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
@@ -64,6 +60,10 @@ use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::default_client::set_default_originator;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_feedback::CodexFeedback;
use codex_login::auth::ExternalAuthRefreshContext;
use codex_login::auth::ExternalAuthRefreshReason;
use codex_login::auth::ExternalAuthRefresher;
use codex_login::auth::ExternalAuthTokens;
use codex_protocol::ThreadId;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::W3cTraceContext;

View File

@@ -328,7 +328,7 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
std::process::exit(1);
}
},
AuthMode::Chatgpt => {
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => {
eprintln!("Logged in using ChatGPT");
std::process::exit(0);
}

View File

@@ -31,17 +31,16 @@ codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-client = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }
codex-exec-server = { workspace = true }
codex-login = { workspace = true }
codex-shell-command = { workspace = true }
codex-skills = { workspace = true }
codex-execpolicy = { workspace = true }
codex-file-search = { workspace = true }
codex-git = { workspace = true }
codex-hooks = { workspace = true }
codex-keyring-store = { workspace = true }
codex-network-proxy = { workspace = true }
codex-otel = { workspace = true }
codex-artifacts = { workspace = true }
@@ -70,11 +69,9 @@ http = { workspace = true }
iana-time-zone = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "webp"] }
indexmap = { workspace = true }
keyring = { workspace = true, features = ["crypto-rust"] }
libc = { workspace = true }
notify = { workspace = true }
once_cell = { workspace = true }
os_info = { workspace = true }
rand = { workspace = true }
regex-lite = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
@@ -89,7 +86,6 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
shlex = { workspace = true }
similar = { workspace = true }
tempfile = { workspace = true }
@@ -120,13 +116,11 @@ wildmatch = { workspace = true }
zip = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
landlock = { workspace = true }
seccompiler = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
keyring = { workspace = true, features = ["apple-native"] }
# Build OpenSSL from source for musl builds.
[target.x86_64-unknown-linux-musl.dependencies]
@@ -137,16 +131,12 @@ openssl-sys = { workspace = true, features = ["vendored"] }
openssl-sys = { workspace = true, features = ["vendored"] }
[target.'cfg(target_os = "windows")'.dependencies]
keyring = { workspace = true, features = ["windows-native"] }
windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_UI_Shell",
] }
[target.'cfg(any(target_os = "freebsd", target_os = "openbsd"))'.dependencies]
keyring = { workspace = true, features = ["sync-secret-service"] }
[target.'cfg(unix)'.dependencies]
codex-shell-escalation = { workspace = true }

View File

@@ -1530,7 +1530,7 @@ impl AuthRequestTelemetryContext {
Self {
auth_mode: auth_mode.map(|mode| match mode {
AuthMode::ApiKey => "ApiKey",
AuthMode::Chatgpt => "Chatgpt",
AuthMode::Chatgpt | AuthMode::ChatgptAuthTokens => "Chatgpt",
}),
auth_header_attached: api_auth.auth_header_attached(),
auth_header_name: api_auth.auth_header_name(),

View File

@@ -0,0 +1,2 @@
// Re-exported as `crate::default_client` from `lib.rs`.
pub use codex_login::default_client::*;

View File

@@ -9,6 +9,8 @@ use chrono::Datelike;
use chrono::Local;
use chrono::Utc;
use codex_async_utils::CancelErr;
pub use codex_login::auth::RefreshTokenFailedError;
pub use codex_login::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_login 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_login::token_data;
mod truncate;
mod unified_exec;
pub mod windows_sandbox;
@@ -110,7 +110,15 @@ pub type CodexConversation = CodexThread;
pub use analytics_client::AnalyticsEventsClient;
pub use auth::AuthManager;
pub use auth::CodexAuth;
pub mod default_client;
mod default_client_forwarding;
/// Default Codex HTTP client headers and reqwest construction.
///
/// Implemented in [`codex_login::default_client`]; this module re-exports that API for crates
/// that import `codex_core::default_client`.
pub mod default_client {
pub use super::default_client_forwarding::*;
}
pub mod project_doc;
mod rollout;
pub(crate) mod safety;

View File

@@ -4,7 +4,6 @@ use std::time::Duration;
use codex_protocol::ThreadId;
use rand::Rng;
use tracing::debug;
use tracing::error;
use crate::auth_env_telemetry::AuthEnvTelemetry;
@@ -217,21 +216,6 @@ pub(crate) fn error_or_panic(message: impl std::string::ToString) {
}
}
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()
}
pub fn resolve_path(base: &Path, path: &PathBuf) -> PathBuf {
if path.is_absolute() {
path.clone()

View File

@@ -12,30 +12,6 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::util::SubscriberInitExt;
#[test]
fn test_try_parse_error_message() {
let text = r#"{
"error": {
"message": "Your refresh token has already been used to generate a new access token. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "refresh_token_reused"
}
}"#;
let message = try_parse_error_message(text);
assert_eq!(
message,
"Your refresh token has already been used to generate a new access token. Please try signing in again."
);
}
#[test]
fn test_try_parse_error_message_no_error() {
let text = r#"{"message": "test"}"#;
let message = try_parse_error_message(text);
assert_eq!(message, r#"{"message": "test"}"#);
}
#[test]
fn feedback_tags_macro_compiles() {
#[derive(Debug)]

View File

@@ -789,8 +789,10 @@ fn minimal_jwt() -> String {
}
fn build_tokens(access_token: &str, refresh_token: &str) -> TokenData {
let mut id_token = IdTokenInfo::default();
id_token.raw_jwt = minimal_jwt();
let id_token = IdTokenInfo {
raw_jwt: minimal_jwt(),
..Default::default()
};
TokenData {
id_token,
access_token: access_token.to_string(),

View File

@@ -45,6 +45,7 @@ use codex_cloud_requirements::cloud_requirements_loader;
use codex_core::AuthManager;
use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
use codex_core::OLLAMA_OSS_PROVIDER_ID;
use codex_core::auth::AuthConfig;
use codex_core::auth::enforce_login_restrictions;
use codex_core::check_execpolicy_for_warnings;
use codex_core::config::Config;
@@ -381,7 +382,12 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
set_default_client_residency_requirement(config.enforce_residency.value());
if let Err(err) = enforce_login_restrictions(&config) {
if let Err(err) = enforce_login_restrictions(&AuthConfig {
codex_home: config.codex_home.clone(),
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
forced_login_method: config.forced_login_method,
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
}) {
eprintln!("{err}");
std::process::exit(1);
}

View File

@@ -8,16 +8,24 @@ license.workspace = true
workspace = true
[dependencies]
async-trait = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-client = { workspace = true }
codex-core = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-client = { workspace = true }
codex-config = { workspace = true }
codex-keyring-store = { workspace = true }
codex-protocol = { workspace = true }
codex-terminal-detection = { workspace = true }
once_cell = { workspace = true }
os_info = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["json", "blocking"] }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tiny_http = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
@@ -34,6 +42,9 @@ webbrowser = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
core_test_support = { workspace = true }
keyring = { workspace = true }
pretty_assertions = { workspace = true }
regex-lite = { workspace = true }
serial_test = { workspace = true }
tempfile = { workspace = true }
wiremock = { workspace = true }

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::token_data::IdTokenInfo;
use crate::token_data::KnownPlan as InternalKnownPlan;
use crate::token_data::PlanType as InternalPlanType;
@@ -103,7 +101,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
.unwrap()
.unwrap();
assert_eq!(None, auth.api_key());
assert_eq!(AuthMode::Chatgpt, auth.auth_mode());
assert_eq!(crate::AuthMode::Chatgpt, auth.auth_mode());
assert_eq!(auth.get_chatgpt_user_id().as_deref(), Some("user-12345"));
let auth_dot_json = auth
@@ -149,7 +147,7 @@ async fn loads_api_key_from_auth_json() {
let auth = super::load_auth(dir.path(), false, AuthCredentialsStoreMode::File)
.unwrap()
.unwrap();
assert_eq!(auth.auth_mode(), AuthMode::ApiKey);
assert_eq!(auth.auth_mode(), crate::AuthMode::ApiKey);
assert_eq!(auth.api_key(), Some("sk-test-key"));
assert!(auth.get_token_data().is_err());
@@ -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

@@ -1,5 +1,9 @@
use crate::config_loader::ResidencyRequirement;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
//! Default Codex HTTP client: shared `User-Agent`, `originator`, optional residency header, and
//! reqwest/`CodexHttpClient` construction.
//!
//! Use [`crate::default_client`] or [`codex_login::default_client`] from other crates in this
//! workspace.
use codex_client::BuildCustomCaTransportError;
use codex_client::CodexHttpClient;
pub use codex_client::CodexRequestBuilder;
@@ -31,6 +35,8 @@ pub const DEFAULT_ORIGINATOR: &str = "codex_cli_rs";
pub const CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR: &str = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE";
pub const RESIDENCY_HEADER_NAME: &str = "x-openai-internal-codex-residency";
pub use codex_config::ResidencyRequirement;
#[derive(Debug, Clone)]
pub struct Originator {
pub value: String,
@@ -232,7 +238,7 @@ pub fn default_headers() -> HeaderMap {
}
fn is_sandboxed() -> bool {
std::env::var(CODEX_SANDBOX_ENV_VAR).as_deref() == Ok("seatbelt")
std::env::var("CODEX_SANDBOX").as_deref() == Ok("seatbelt")
}
#[cfg(test)]

View File

@@ -1,3 +1,4 @@
use super::sanitize_user_agent;
use super::*;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;

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

@@ -1,5 +1,3 @@
mod storage;
use async_trait::async_trait;
use chrono::Utc;
use reqwest::StatusCode;
@@ -16,46 +14,25 @@ use std::sync::Mutex;
use std::sync::RwLock;
use codex_app_server_protocol::AuthMode as ApiAuthMode;
use codex_otel::TelemetryAuthMode;
use codex_protocol::config_types::ForcedLoginMethod;
use crate::auth::error::RefreshTokenFailedError;
use crate::auth::error::RefreshTokenFailedReason;
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;
use crate::auth::util::try_parse_error_message;
use crate::default_client::create_client;
use crate::token_data::KnownPlan as InternalKnownPlan;
use crate::token_data::PlanType as InternalPlanType;
use crate::token_data::TokenData;
use crate::token_data::parse_chatgpt_jwt_claims;
use crate::util::try_parse_error_message;
use codex_client::CodexHttpClient;
use codex_protocol::account::PlanType as AccountPlanType;
use serde_json::Value;
use thiserror::Error;
/// Account type for the current user.
///
/// This is used internally to determine the base URL for generating responses,
/// and to gate ChatGPT-only behaviors like rate limits and available models (as
/// opposed to API key-based auth).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AuthMode {
ApiKey,
Chatgpt,
}
impl From<AuthMode> for TelemetryAuthMode {
fn from(mode: AuthMode) -> Self {
match mode {
AuthMode::ApiKey => TelemetryAuthMode::ApiKey,
AuthMode::Chatgpt => TelemetryAuthMode::Chatgpt,
}
}
}
/// Authentication mechanism used by the current user.
#[derive(Debug, Clone)]
pub enum CodexAuth {
@@ -161,14 +138,14 @@ impl CodexAuth {
codex_home: &Path,
auth_dot_json: AuthDotJson,
auth_credentials_store_mode: AuthCredentialsStoreMode,
client: CodexHttpClient,
) -> std::io::Result<Self> {
let auth_mode = auth_dot_json.resolved_mode();
let client = create_client();
if auth_mode == ApiAuthMode::ApiKey {
let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else {
return Err(std::io::Error::other("API key auth is missing a key."));
};
return Ok(CodexAuth::from_api_key_with_client(api_key, client));
return Ok(Self::from_api_key(api_key));
}
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
@@ -189,7 +166,6 @@ impl CodexAuth {
}
}
/// Loads the available auth information from auth storage.
pub fn from_auth_storage(
codex_home: &Path,
auth_credentials_store_mode: AuthCredentialsStoreMode,
@@ -201,10 +177,10 @@ impl CodexAuth {
)
}
pub fn auth_mode(&self) -> AuthMode {
pub fn auth_mode(&self) -> crate::AuthMode {
match self {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => AuthMode::Chatgpt,
Self::ApiKey(_) => crate::AuthMode::ApiKey,
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => crate::AuthMode::Chatgpt,
}
}
@@ -217,11 +193,11 @@ impl CodexAuth {
}
pub fn is_api_key_auth(&self) -> bool {
self.auth_mode() == AuthMode::ApiKey
self.auth_mode() == crate::AuthMode::ApiKey
}
pub fn is_chatgpt_auth(&self) -> bool {
self.auth_mode() == AuthMode::Chatgpt
self.auth_mode() == crate::AuthMode::Chatgpt
}
pub fn is_external_chatgpt_tokens(&self) -> bool {
@@ -335,7 +311,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,
@@ -344,15 +320,11 @@ impl CodexAuth {
Self::Chatgpt(ChatgptAuth { state, storage })
}
fn from_api_key_with_client(api_key: &str, _client: CodexHttpClient) -> Self {
pub fn from_api_key(api_key: &str) -> Self {
Self::ApiKey(ApiKeyAuth {
api_key: api_key.to_owned(),
})
}
pub fn from_api_key(api_key: &str) -> Self {
Self::from_api_key_with_client(api_key, crate::default_client::create_client())
}
}
impl ChatgptAuth {
@@ -458,11 +430,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(());
@@ -470,13 +450,15 @@ pub fn enforce_login_restrictions(config: &Config) -> std::io::Result<()> {
if let Some(required_method) = config.forced_login_method {
let method_violation = match (required_method, auth.auth_mode()) {
(ForcedLoginMethod::Api, AuthMode::ApiKey) => None,
(ForcedLoginMethod::Chatgpt, AuthMode::Chatgpt) => None,
(ForcedLoginMethod::Api, AuthMode::Chatgpt) => Some(
(ForcedLoginMethod::Api, crate::AuthMode::ApiKey) => None,
(ForcedLoginMethod::Chatgpt, crate::AuthMode::Chatgpt)
| (ForcedLoginMethod::Chatgpt, crate::AuthMode::ChatgptAuthTokens) => None,
(ForcedLoginMethod::Api, crate::AuthMode::Chatgpt)
| (ForcedLoginMethod::Api, crate::AuthMode::ChatgptAuthTokens) => Some(
"API key login is required, but ChatGPT is currently being used. Logging out."
.to_string(),
),
(ForcedLoginMethod::Chatgpt, AuthMode::ApiKey) => Some(
(ForcedLoginMethod::Chatgpt, crate::AuthMode::ApiKey) => Some(
"ChatGPT login is required, but an API key is currently being used. Logging out."
.to_string(),
),
@@ -486,7 +468,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 +486,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 +505,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,17 +546,12 @@ 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();
CodexAuth::from_auth_dot_json(codex_home, auth_dot_json, storage_mode, client)
CodexAuth::from_auth_dot_json(codex_home, auth_dot_json, storage_mode)
};
// 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();
return Ok(Some(CodexAuth::from_api_key_with_client(
api_key.as_str(),
client,
)));
return Ok(Some(CodexAuth::from_api_key(api_key.as_str())));
}
// External ChatGPT auth tokens live in the in-memory (ephemeral) store. Always check this
@@ -1077,7 +1054,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 +1070,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,
@@ -1342,7 +1316,7 @@ impl AuthManager {
self.auth_cached().as_ref().map(CodexAuth::api_auth_mode)
}
pub fn auth_mode(&self) -> Option<AuthMode> {
pub fn auth_mode(&self) -> Option<crate::AuthMode> {
self.auth_cached().as_ref().map(CodexAuth::auth_mode)
}

View File

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

View File

@@ -0,0 +1,45 @@
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()
}
#[cfg(test)]
mod tests {
use super::try_parse_error_message;
#[test]
fn try_parse_error_message_extracts_openai_error_message() {
let text = r#"{
"error": {
"message": "Your refresh token has already been used to generate a new access token. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "refresh_token_reused"
}
}"#;
let message = try_parse_error_message(text);
assert_eq!(
message,
"Your refresh token has already been used to generate a new access token. Please try signing in again."
);
}
#[test]
fn try_parse_error_message_falls_back_to_raw_text() {
let text = r#"{"message": "test"}"#;
let message = try_parse_error_message(text);
assert_eq!(message, r#"{"message": "test"}"#);
}
}

View File

@@ -1,3 +1,6 @@
pub mod auth;
pub mod token_data;
mod device_code_auth;
mod pkce;
mod server;
@@ -12,15 +15,23 @@ pub use server::ServerOptions;
pub use server::ShutdownHandle;
pub use server::run_login_server;
// Re-export commonly used auth types and helpers from codex-core for compatibility
pub use auth::AuthConfig;
pub use auth::AuthCredentialsStoreMode;
pub use auth::AuthDotJson;
pub use auth::AuthManager;
pub use auth::CLIENT_ID;
pub use auth::CODEX_API_KEY_ENV_VAR;
pub use auth::CodexAuth;
pub use auth::OPENAI_API_KEY_ENV_VAR;
pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
pub use auth::RefreshTokenError;
pub use auth::UnauthorizedRecovery;
pub use auth::default_client;
pub use auth::enforce_login_restrictions;
pub use auth::load_auth_dot_json;
pub use auth::login_with_api_key;
pub use auth::logout;
pub use auth::read_openai_api_key_from_env;
pub use auth::save_auth;
pub use codex_app_server_protocol::AuthMode;
pub use codex_core::AuthManager;
pub use codex_core::CodexAuth;
pub use codex_core::auth::AuthDotJson;
pub use codex_core::auth::CLIENT_ID;
pub use codex_core::auth::CODEX_API_KEY_ENV_VAR;
pub use codex_core::auth::OPENAI_API_KEY_ENV_VAR;
pub use codex_core::auth::login_with_api_key;
pub use codex_core::auth::logout;
pub use codex_core::auth::save_auth;
pub use codex_core::token_data::TokenData;
pub use token_data::TokenData;

View File

@@ -23,18 +23,18 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::auth::AuthCredentialsStoreMode;
use crate::auth::AuthDotJson;
use crate::auth::save_auth;
use crate::default_client::originator;
use crate::pkce::PkceCodes;
use crate::pkce::generate_pkce;
use crate::token_data::TokenData;
use crate::token_data::parse_chatgpt_jwt_claims;
use base64::Engine;
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_client::build_reqwest_client_with_custom_ca;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::AuthDotJson;
use codex_core::auth::save_auth;
use codex_core::default_client::originator;
use codex_core::token_data::TokenData;
use codex_core::token_data::parse_chatgpt_jwt_claims;
use rand::RngCore;
use serde_json::Value as JsonValue;
use tiny_http::Header;
@@ -484,10 +484,7 @@ fn build_authorize_url(
("id_token_add_organizations".to_string(), "true".to_string()),
("codex_cli_simplified_flow".to_string(), "true".to_string()),
("state".to_string(), state.to_string()),
(
"originator".to_string(),
originator().value.as_str().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()));

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

@@ -3,9 +3,9 @@
use anyhow::Context;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::load_auth_dot_json;
use codex_login::ServerOptions;
use codex_login::auth::AuthCredentialsStoreMode;
use codex_login::auth::load_auth_dot_json;
use codex_login::run_device_code_login;
use serde_json::json;
use std::sync::Arc;

View File

@@ -7,8 +7,8 @@ use std::time::Duration;
use anyhow::Result;
use base64::Engine;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_login::ServerOptions;
use codex_login::auth::AuthCredentialsStoreMode;
use codex_login::run_login_server;
use core_test_support::skip_if_no_network;
use tempfile::tempdir;

View File

@@ -24,6 +24,7 @@ chrono = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-string = { workspace = true }
codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-protocol = { workspace = true }
eventsource-stream = { workspace = true }
gethostname = { workspace = true }

View File

@@ -36,13 +36,23 @@ pub enum ToolDecisionSource {
User,
}
/// Maps to core AuthMode to avoid a circular dependency on codex-core.
/// Maps to API/auth `AuthMode` to avoid a circular dependency on codex-core.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum TelemetryAuthMode {
ApiKey,
Chatgpt,
}
impl From<codex_app_server_protocol::AuthMode> for TelemetryAuthMode {
fn from(mode: codex_app_server_protocol::AuthMode) -> Self {
match mode {
codex_app_server_protocol::AuthMode::ApiKey => Self::ApiKey,
codex_app_server_protocol::AuthMode::Chatgpt
| codex_app_server_protocol::AuthMode::ChatgptAuthTokens => Self::Chatgpt,
}
}
}
/// Start a metrics timer using the globally installed metrics client.
pub fn start_global_timer(name: &str, tags: &[(&str, &str)]) -> MetricsResult<Timer> {
let Some(metrics) = crate::metrics::global() else {

View File

@@ -13,6 +13,7 @@ use codex_core::CodexAuth;
use codex_core::INTERACTIVE_SESSION_SOURCES;
use codex_core::RolloutRecorder;
use codex_core::ThreadSortKey;
use codex_core::auth::AuthConfig;
use codex_core::auth::AuthMode;
use codex_core::auth::enforce_login_restrictions;
use codex_core::check_execpolicy_for_warnings;
@@ -454,7 +455,12 @@ pub async fn run_main(
}
#[allow(clippy::print_stderr)]
if let Err(err) = enforce_login_restrictions(&config) {
if let Err(err) = enforce_login_restrictions(&AuthConfig {
codex_home: config.codex_home.clone(),
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
forced_login_method: config.forced_login_method,
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
}) {
eprintln!("{err}");
std::process::exit(1);
}

View File

@@ -92,7 +92,7 @@ pub(crate) fn compose_account_display(
match auth.auth_mode() {
CoreAuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey),
CoreAuthMode::Chatgpt => {
CoreAuthMode::Chatgpt | CoreAuthMode::ChatgptAuthTokens => {
let email = auth.get_account_email();
let plan = plan
.map(|plan_type| title_case(format!("{plan_type:?}").as_str()))

View File

@@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
use codex_app_server_protocol::ThreadSourceKind;
use codex_cloud_requirements::cloud_requirements_loader_for_storage;
use codex_core::auth::AuthConfig;
use codex_core::auth::enforce_login_restrictions;
use codex_core::check_execpolicy_for_warnings;
use codex_core::config::Config;
@@ -777,7 +778,12 @@ pub async fn run_main(
if matches!(app_server_target, AppServerTarget::Embedded) {
#[allow(clippy::print_stderr)]
if let Err(err) = enforce_login_restrictions(&config) {
if let Err(err) = enforce_login_restrictions(&AuthConfig {
codex_home: config.codex_home.clone(),
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
forced_login_method: config.forced_login_method,
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
}) {
eprintln!("{err}");
std::process::exit(1);
}

View File

@@ -70,9 +70,9 @@ mod tests {
use chrono::Utc;
use codex_app_server_protocol::AuthMode;
use codex_core::auth::AuthDotJson;
use codex_core::auth::login_with_chatgpt_auth_tokens;
use codex_core::auth::save_auth;
use codex_core::token_data::TokenData;
use codex_login::auth::login_with_chatgpt_auth_tokens;
use pretty_assertions::assert_eq;
use serde::Serialize;
use serde_json::json;