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
2 changes: 2 additions & 0 deletions codex-rs/login/src/auth/auth_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use http::HeaderMap;
///
/// The provider owns credential validation, rotation, and persistence. Codex
/// keeps the resolved headers in memory and attaches them to backend requests.
/// Identity headers must be derived from and bound to the validated credentials;
/// Codex may use them to enforce managed authentication policy.
#[derive(Clone, PartialEq, Eq)]
pub struct AuthHeaders {
headers: HeaderMap,
Expand Down
73 changes: 73 additions & 0 deletions codex-rs/login/src/auth/auth_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,36 @@ impl ExternalAuth for StaticExternalAuth {
}
}

struct RefreshingExternalAuth {
initial: CodexAuth,
refreshed: CodexAuth,
}

impl ExternalAuth for RefreshingExternalAuth {
fn resolve(&self) -> ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Ok(self.initial.clone()) })
}

fn refresh(&self, _context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Ok(self.refreshed.clone()) })
}
}

fn external_header_auth(account_id: Option<&'static str>) -> CodexAuth {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::AUTHORIZATION,
http::HeaderValue::from_static("Bearer external"),
);
if let Some(account_id) = account_id {
headers.insert(
"chatgpt-account-id",
http::HeaderValue::from_static(account_id),
);
}
CodexAuth::Headers(AuthHeaders::new(headers))
}

struct FailingExternalAuth {
auth: CodexAuth,
resolve_count: AtomicUsize,
Expand Down Expand Up @@ -1460,6 +1490,49 @@ async fn external_auth_provider_can_install_headers() {
);
}

#[tokio::test]
async fn external_header_auth_obeys_workspace_policy() {
for (account_id, should_succeed) in [
(Some(WORKSPACE_ID_ALLOWED), true),
(Some(WORKSPACE_ID_DISALLOWED), false),
(None, false),
] {
let auth = external_header_auth(account_id);
let expected_auth = should_succeed.then_some(auth.clone());
let manager = AuthManager::from_optional_auth_for_testing(/*auth*/ None);
manager.set_forced_chatgpt_workspace_id(Some(vec![WORKSPACE_ID_ALLOWED.to_string()]));

let result = manager
.set_external_auth(Arc::new(StaticExternalAuth(auth)))
.await;

assert_eq!(result.is_ok(), should_succeed, "account ID: {account_id:?}");
assert_eq!(manager.auth_cached(), expected_auth);
}
}

#[tokio::test]
async fn external_header_auth_rejects_a_disallowed_workspace_on_refresh() {
let allowed_auth = external_header_auth(Some(WORKSPACE_ID_ALLOWED));
let disallowed_auth = external_header_auth(Some(WORKSPACE_ID_DISALLOWED));
let manager = AuthManager::from_optional_auth_for_testing(/*auth*/ None);
manager.set_forced_chatgpt_workspace_id(Some(vec![WORKSPACE_ID_ALLOWED.to_string()]));
manager
.set_external_auth(Arc::new(RefreshingExternalAuth {
initial: allowed_auth.clone(),
refreshed: disallowed_auth,
}))
.await
.expect("initial external header auth should install");

manager
.refresh_token_from_authority()
.await
.expect_err("external header auth from a disallowed workspace must not replace the cache");

assert_eq!(manager.auth_cached(), Some(allowed_auth));
}

#[tokio::test]
async fn workload_identity_auth_is_immutable_and_process_local() {
let codex_home = tempdir().expect("tempdir");
Expand Down
13 changes: 5 additions & 8 deletions codex-rs/login/src/auth/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,10 +1219,7 @@ fn validate_auth_restrictions(
let Some(expected_workspaces) = expected_workspaces else {
return Ok(());
};
if matches!(
auth,
CodexAuth::ApiKey(_) | CodexAuth::Headers(_) | CodexAuth::BedrockApiKey(_)
) {
if matches!(auth, CodexAuth::ApiKey(_) | CodexAuth::BedrockApiKey(_)) {
return Ok(());
}

Expand Down Expand Up @@ -1317,12 +1314,12 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(

if let Some(expected_account_ids) = config.forced_chatgpt_workspace_id.as_deref() {
let chatgpt_account_id = match &auth {
CodexAuth::ApiKey(_) | CodexAuth::Headers(_) | CodexAuth::BedrockApiKey(_) => {
CodexAuth::ApiKey(_) | CodexAuth::BedrockApiKey(_) => {
return Ok(());
}
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => {
auth.get_account_id()
}
CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => auth.get_account_id(),
CodexAuth::Chatgpt(_) | CodexAuth::ChatgptAuthTokens(_) => {
let token_data = match auth.get_token_data() {
Ok(data) => data,
Expand Down
Loading