Skip to content

Commit 4a42882

Browse files
committed
auth: let AuthManager own external bearer auth
1 parent 4e7d464 commit 4a42882

File tree

7 files changed

+544
-45
lines changed

7 files changed

+544
-45
lines changed

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ impl CodexMessageProcessor {
10151015
&mut self,
10161016
params: &LoginApiKeyParams,
10171017
) -> std::result::Result<(), JSONRPCErrorError> {
1018-
if self.auth_manager.is_external_auth_active() {
1018+
if self.auth_manager.is_external_chatgpt_auth_active() {
10191019
return Err(self.external_auth_active_error());
10201020
}
10211021

@@ -1094,7 +1094,7 @@ impl CodexMessageProcessor {
10941094
) -> std::result::Result<LoginServerOptions, JSONRPCErrorError> {
10951095
let config = self.config.as_ref();
10961096

1097-
if self.auth_manager.is_external_auth_active() {
1097+
if self.auth_manager.is_external_chatgpt_auth_active() {
10981098
return Err(self.external_auth_active_error());
10991099
}
11001100

@@ -1531,7 +1531,7 @@ impl CodexMessageProcessor {
15311531
}
15321532

15331533
async fn refresh_token_if_requested(&self, do_refresh: bool) -> RefreshTokenRequestOutcome {
1534-
if self.auth_manager.is_external_auth_active() {
1534+
if self.auth_manager.is_external_chatgpt_auth_active() {
15351535
return RefreshTokenRequestOutcome::NotAttemptedOrSucceeded;
15361536
}
15371537
if do_refresh && let Err(err) = self.auth_manager.refresh_token().await {

codex-rs/app-server/src/message_processor.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,13 @@ impl MessageProcessor {
206206
session_source,
207207
enable_codex_api_key_env,
208208
} = args;
209-
let auth_manager = AuthManager::shared(
209+
let auth_manager = AuthManager::shared_with_external_chatgpt_auth_refresher(
210210
config.codex_home.clone(),
211211
enable_codex_api_key_env,
212212
config.cli_auth_credentials_store_mode,
213+
Arc::new(ExternalAuthRefreshBridge {
214+
outgoing: outgoing.clone(),
215+
}),
213216
);
214217
let thread_manager = Arc::new(ThreadManager::new(
215218
config.as_ref(),
@@ -223,9 +226,6 @@ impl MessageProcessor {
223226
environment_manager,
224227
));
225228
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id.clone());
226-
auth_manager.set_external_auth_refresher(Arc::new(ExternalAuthRefreshBridge {
227-
outgoing: outgoing.clone(),
228-
}));
229229
let analytics_events_client = AnalyticsEventsClient::new(
230230
Arc::clone(&auth_manager),
231231
config.chatgpt_base_url.trim_end_matches('/').to_string(),
@@ -282,7 +282,7 @@ impl MessageProcessor {
282282
}
283283

284284
pub(crate) fn clear_runtime_references(&self) {
285-
self.auth_manager.clear_external_auth_refresher();
285+
self.auth_manager.clear_external_chatgpt_auth_refresher();
286286
}
287287

288288
pub(crate) async fn process_request(

codex-rs/login/src/auth/auth_tests.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use codex_protocol::account::PlanType as AccountPlanType;
88

99
use base64::Engine;
1010
use codex_protocol::config_types::ForcedLoginMethod;
11+
use codex_protocol::config_types::ModelProviderAuthInfo;
1112
use pretty_assertions::assert_eq;
1213
use serde::Serialize;
1314
use serde_json::json;
1415
use std::sync::Arc;
16+
use tempfile::TempDir;
1517
use tempfile::tempdir;
1618

1719
#[tokio::test]
@@ -265,6 +267,180 @@ fn external_auth_tokens_without_chatgpt_metadata_cannot_seed_chatgpt_auth() {
265267
);
266268
}
267269

270+
#[tokio::test]
271+
async fn external_bearer_only_auth_manager_uses_cached_provider_token() {
272+
let script = ProviderAuthScript::new(&["provider-token", "next-token"]).unwrap();
273+
let manager = AuthManager::external_bearer_only(script.auth_config());
274+
275+
let first = manager
276+
.auth()
277+
.await
278+
.and_then(|auth| auth.api_key().map(str::to_string));
279+
let second = manager
280+
.auth()
281+
.await
282+
.and_then(|auth| auth.api_key().map(str::to_string));
283+
284+
assert_eq!(first.as_deref(), Some("provider-token"));
285+
assert_eq!(second.as_deref(), Some("provider-token"));
286+
}
287+
288+
#[tokio::test]
289+
async fn external_bearer_only_auth_manager_returns_none_when_command_fails() {
290+
let script = ProviderAuthScript::new_failing().unwrap();
291+
let manager = AuthManager::external_bearer_only(script.auth_config());
292+
293+
assert_eq!(manager.auth().await, None);
294+
}
295+
296+
#[tokio::test]
297+
async fn unauthorized_recovery_uses_external_refresh_for_bearer_manager() {
298+
let script = ProviderAuthScript::new(&["provider-token", "refreshed-provider-token"]).unwrap();
299+
let manager = AuthManager::external_bearer_only(script.auth_config());
300+
let initial_token = manager
301+
.auth()
302+
.await
303+
.and_then(|auth| auth.api_key().map(str::to_string));
304+
let mut recovery = manager.unauthorized_recovery();
305+
306+
assert!(recovery.has_next());
307+
assert_eq!(recovery.mode_name(), "external");
308+
assert_eq!(recovery.step_name(), "external_refresh");
309+
310+
let result = recovery
311+
.next()
312+
.await
313+
.expect("external refresh should succeed");
314+
315+
assert_eq!(result.auth_state_changed(), Some(true));
316+
let refreshed_token = manager
317+
.auth()
318+
.await
319+
.and_then(|auth| auth.api_key().map(str::to_string));
320+
assert_eq!(initial_token.as_deref(), Some("provider-token"));
321+
assert_eq!(refreshed_token.as_deref(), Some("refreshed-provider-token"));
322+
}
323+
324+
struct ProviderAuthScript {
325+
tempdir: TempDir,
326+
command: String,
327+
args: Vec<String>,
328+
}
329+
330+
impl ProviderAuthScript {
331+
fn new(tokens: &[&str]) -> std::io::Result<Self> {
332+
let tempdir = tempfile::tempdir()?;
333+
let token_file = tempdir.path().join("tokens.txt");
334+
let mut token_file_contents = String::new();
335+
for token in tokens {
336+
token_file_contents.push_str(token);
337+
token_file_contents.push('\n');
338+
}
339+
std::fs::write(&token_file, token_file_contents)?;
340+
341+
#[cfg(unix)]
342+
let (command, args) = {
343+
let script_path = tempdir.path().join("print-token.sh");
344+
std::fs::write(
345+
&script_path,
346+
r#"#!/bin/sh
347+
first_line=$(sed -n '1p' tokens.txt)
348+
printf '%s\n' "$first_line"
349+
tail -n +2 tokens.txt > tokens.next
350+
mv tokens.next tokens.txt
351+
"#,
352+
)?;
353+
let mut permissions = std::fs::metadata(&script_path)?.permissions();
354+
{
355+
use std::os::unix::fs::PermissionsExt;
356+
permissions.set_mode(0o755);
357+
}
358+
std::fs::set_permissions(&script_path, permissions)?;
359+
("./print-token.sh".to_string(), Vec::new())
360+
};
361+
362+
#[cfg(windows)]
363+
let (command, args) = {
364+
let script_path = tempdir.path().join("print-token.ps1");
365+
std::fs::write(
366+
&script_path,
367+
r#"$lines = Get-Content -Path tokens.txt
368+
if ($lines.Count -eq 0) { exit 1 }
369+
Write-Output $lines[0]
370+
$lines | Select-Object -Skip 1 | Set-Content -Path tokens.txt
371+
"#,
372+
)?;
373+
(
374+
"powershell".to_string(),
375+
vec![
376+
"-NoProfile".to_string(),
377+
"-ExecutionPolicy".to_string(),
378+
"Bypass".to_string(),
379+
"-File".to_string(),
380+
".\\print-token.ps1".to_string(),
381+
],
382+
)
383+
};
384+
385+
Ok(Self {
386+
tempdir,
387+
command,
388+
args,
389+
})
390+
}
391+
392+
fn new_failing() -> std::io::Result<Self> {
393+
let tempdir = tempfile::tempdir()?;
394+
395+
#[cfg(unix)]
396+
let (command, args) = {
397+
let script_path = tempdir.path().join("fail.sh");
398+
std::fs::write(
399+
&script_path,
400+
r#"#!/bin/sh
401+
exit 1
402+
"#,
403+
)?;
404+
let mut permissions = std::fs::metadata(&script_path)?.permissions();
405+
{
406+
use std::os::unix::fs::PermissionsExt;
407+
permissions.set_mode(0o755);
408+
}
409+
std::fs::set_permissions(&script_path, permissions)?;
410+
("./fail.sh".to_string(), Vec::new())
411+
};
412+
413+
#[cfg(windows)]
414+
let (command, args) = (
415+
"powershell".to_string(),
416+
vec![
417+
"-NoProfile".to_string(),
418+
"-ExecutionPolicy".to_string(),
419+
"Bypass".to_string(),
420+
"-Command".to_string(),
421+
"exit 1".to_string(),
422+
],
423+
);
424+
425+
Ok(Self {
426+
tempdir,
427+
command,
428+
args,
429+
})
430+
}
431+
432+
fn auth_config(&self) -> ModelProviderAuthInfo {
433+
serde_json::from_value(json!({
434+
"command": self.command,
435+
"args": self.args,
436+
"timeout_ms": 1000,
437+
"refresh_interval_ms": 60000,
438+
"cwd": self.tempdir.path(),
439+
}))
440+
.expect("provider auth config should deserialize")
441+
}
442+
}
443+
268444
struct AuthFileParams {
269445
openai_api_key: Option<String>,
270446
chatgpt_plan_type: Option<String>,

0 commit comments

Comments
 (0)