Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ In the codex-rs folder where the rust code lives:
- Do not add these comments for string or char literals unless the comment adds real clarity; those literals are intentionally exempt from the lint.
- If you add one of these comments, the parameter name must exactly match the callee signature.
- When possible, make `match` statements exhaustive and avoid wildcard arms.
- Newly added traits should include doc comments that explain their role and how implementations are expected to use them.
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
- When making a change that adds or changes an API, ensure that the documentation in the `docs/` folder is up to date if applicable.
- If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`.
Expand Down
13 changes: 9 additions & 4 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ use codex_core::models_manager::collaboration_mode_presets::CollaborationModesCo
use codex_exec_server::EnvironmentManager;
use codex_features::Feature;
use codex_feedback::CodexFeedback;
use codex_login::AuthMode as LoginAuthMode;
use codex_login::auth::ExternalAuth;
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;
Expand Down Expand Up @@ -102,7 +103,11 @@ impl ExternalAuthRefreshBridge {
}

#[async_trait]
impl ExternalAuthRefresher for ExternalAuthRefreshBridge {
impl ExternalAuth for ExternalAuthRefreshBridge {
fn auth_mode(&self) -> LoginAuthMode {
LoginAuthMode::Chatgpt
}

async fn refresh(
&self,
context: ExternalAuthRefreshContext,
Expand Down Expand Up @@ -206,7 +211,7 @@ impl MessageProcessor {
session_source,
enable_codex_api_key_env,
} = args;
let auth_manager = AuthManager::shared_with_external_chatgpt_auth_refresher(
let auth_manager = AuthManager::shared_with_external_auth(
config.codex_home.clone(),
enable_codex_api_key_env,
config.cli_auth_credentials_store_mode,
Expand Down Expand Up @@ -282,7 +287,7 @@ impl MessageProcessor {
}

pub(crate) fn clear_runtime_references(&self) {
self.auth_manager.clear_external_chatgpt_auth_refresher();
self.auth_manager.clear_external_auth();
}

pub(crate) async fn process_request(
Expand Down
44 changes: 24 additions & 20 deletions codex-rs/login/src/auth/auth_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ async fn external_bearer_only_auth_manager_uses_cached_provider_token() {

assert_eq!(first.as_deref(), Some("provider-token"));
assert_eq!(second.as_deref(), Some("provider-token"));
assert_eq!(manager.auth_mode(), Some(crate::AuthMode::ApiKey));
assert_eq!(manager.get_api_auth_mode(), Some(ApiAuthMode::ApiKey));
}

#[tokio::test]
Expand Down Expand Up @@ -448,13 +450,29 @@ struct AuthFileParams {
}

fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result<String> {
let fake_jwt = fake_jwt_for_auth_file_params(&params)?;
let auth_file = get_auth_file(codex_home);
// Create a minimal valid JWT for the id_token field.
let auth_json_data = json!({
"OPENAI_API_KEY": params.openai_api_key,
"tokens": {
"id_token": fake_jwt,
"access_token": "test-access-token",
"refresh_token": "test-refresh-token"
},
"last_refresh": Utc::now(),
});
let auth_json = serde_json::to_string_pretty(&auth_json_data)?;
std::fs::write(auth_file, auth_json)?;
Ok(fake_jwt)
}

fn fake_jwt_for_auth_file_params(params: &AuthFileParams) -> std::io::Result<String> {
#[derive(Serialize)]
struct Header {
alg: &'static str,
typ: &'static str,
}

let header = Header {
alg: "none",
typ: "JWT",
Expand All @@ -464,13 +482,12 @@ fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result
"user_id": "user-12345",
});

if let Some(chatgpt_plan_type) = params.chatgpt_plan_type {
auth_payload["chatgpt_plan_type"] = serde_json::Value::String(chatgpt_plan_type);
if let Some(chatgpt_plan_type) = params.chatgpt_plan_type.as_ref() {
auth_payload["chatgpt_plan_type"] = serde_json::Value::String(chatgpt_plan_type.clone());
}

if let Some(chatgpt_account_id) = params.chatgpt_account_id {
let org_value = serde_json::Value::String(chatgpt_account_id);
auth_payload["chatgpt_account_id"] = org_value;
if let Some(chatgpt_account_id) = params.chatgpt_account_id.as_ref() {
auth_payload["chatgpt_account_id"] = serde_json::Value::String(chatgpt_account_id.clone());
}

let payload = serde_json::json!({
Expand All @@ -482,20 +499,7 @@ fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result
let header_b64 = b64(&serde_json::to_vec(&header)?);
let payload_b64 = b64(&serde_json::to_vec(&payload)?);
let signature_b64 = b64(b"sig");
let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}");

let auth_json_data = json!({
"OPENAI_API_KEY": params.openai_api_key,
"tokens": {
"id_token": fake_jwt,
"access_token": "test-access-token",
"refresh_token": "test-refresh-token"
},
"last_refresh": Utc::now(),
});
let auth_json = serde_json::to_string_pretty(&auth_json_data)?;
std::fs::write(auth_file, auth_json)?;
Ok(fake_jwt)
Ok(format!("{header_b64}.{payload_b64}.{signature_b64}"))
}

async fn build_config(
Expand Down
58 changes: 38 additions & 20 deletions codex-rs/login/src/auth/external_bearer.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use super::manager::ExternalAuth;
use super::manager::ExternalAuthRefreshContext;
use super::manager::ExternalAuthTokens;
use async_trait::async_trait;
use codex_protocol::config_types::ModelProviderAuthInfo;
use std::fmt;
use std::io;
Expand All @@ -10,47 +14,61 @@ use tokio::process::Command;
use tokio::sync::Mutex;

#[derive(Clone)]
pub(crate) struct ExternalBearerAuth {
pub(crate) struct BearerTokenRefresher {
state: Arc<ExternalBearerAuthState>,
}

impl ExternalBearerAuth {
impl BearerTokenRefresher {
pub(crate) fn new(config: ModelProviderAuthInfo) -> Self {
Self {
state: Arc::new(ExternalBearerAuthState::new(config)),
}
}
}

pub(crate) async fn resolve_access_token(&self) -> io::Result<String> {
let mut cached = self.state.cached_token.lock().await;
if let Some(cached_token) = cached.as_ref()
&& cached_token.fetched_at.elapsed() < self.state.config.refresh_interval()
{
return Ok(cached_token.access_token.clone());
}
#[async_trait]
impl ExternalAuth for BearerTokenRefresher {
fn auth_mode(&self) -> crate::AuthMode {
crate::AuthMode::ApiKey
}

let access_token = run_provider_auth_command(&self.state.config).await?;
*cached = Some(CachedExternalBearerToken {
access_token: access_token.clone(),
fetched_at: Instant::now(),
});
Ok(access_token)
async fn resolve(&self) -> io::Result<Option<ExternalAuthTokens>> {
let access_token = {
let mut cached = self.state.cached_token.lock().await;
if let Some(cached_token) = cached.as_ref()
&& cached_token.fetched_at.elapsed() < self.state.config.refresh_interval()
{
cached_token.access_token.clone()
} else {
let access_token = run_provider_auth_command(&self.state.config).await?;
*cached = Some(CachedExternalBearerToken {
access_token: access_token.clone(),
fetched_at: Instant::now(),
});
access_token
}
};
Ok(Some(ExternalAuthTokens::access_token_only(access_token)))
}

pub(crate) async fn refresh_after_unauthorized(&self) -> io::Result<()> {
async fn refresh(
&self,
_context: ExternalAuthRefreshContext,
) -> io::Result<ExternalAuthTokens> {
let access_token = run_provider_auth_command(&self.state.config).await?;
let mut cached = self.state.cached_token.lock().await;
*cached = Some(CachedExternalBearerToken {
access_token,
access_token: access_token.clone(),
fetched_at: Instant::now(),
});
Ok(())
Ok(ExternalAuthTokens::access_token_only(access_token))
}
}

impl fmt::Debug for ExternalBearerAuth {
impl fmt::Debug for BearerTokenRefresher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExternalBearerAuth").finish_non_exhaustive()
f.debug_struct("BearerTokenRefresher")
.finish_non_exhaustive()
}
}

Expand Down
Loading
Loading