Protect workload identity auth in app-server account RPCs (#38426)

## Why

Workload identity credentials are owned by the app-server host and must not be replaced, removed, or exported through client account operations.

## What changed

- Reject account login and logout RPCs while workload identity is selected.
- Continue reporting the active authentication method from `getAuthStatus`, but omit the host-owned token even when `includeToken` is requested.

GitOrigin-RevId: 750c9d07ed2f4ba7007bbe75f188573e79749647
This commit is contained in:
cooper-oai
2026-08-13 20:04:57 +00:00
committed by copyberry
parent 990218bbbd
commit 5ed321ce00
2 changed files with 40 additions and 23 deletions
@@ -284,6 +284,9 @@ impl AccountRequestProcessor {
request_id: ConnectionRequestId,
params: LoginAccountParams,
) -> Result<(), JSONRPCErrorError> {
if self.auth_manager.is_workload_identity_selected() {
return Err(self.configured_auth_owned_by_host_error());
}
match params {
LoginAccountParams::ApiKey { api_key } => {
self.login_api_key_v2(request_id, LoginApiKeyParams { api_key })
@@ -341,6 +344,12 @@ impl AccountRequestProcessor {
)
}
fn configured_auth_owned_by_host_error(&self) -> JSONRPCErrorError {
invalid_request(
"Configured external authentication is owned by the app-server host and cannot be changed through account RPCs.",
)
}
async fn login_api_key_common(
&self,
params: &LoginApiKeyParams,
@@ -868,6 +877,9 @@ impl AccountRequestProcessor {
}
async fn logout_common(&self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError> {
if self.auth_manager.is_workload_identity_selected() {
return Err(self.configured_auth_owned_by_host_error());
}
let managed_bedrock_auth = matches!(
self.auth_manager.auth_cached(),
Some(CodexAuth::BedrockApiKey(_))
@@ -986,30 +998,31 @@ impl AccountRequestProcessor {
let permanent_refresh_failure =
self.auth_manager.refresh_failure_for_auth(&auth).is_some();
let auth_mode = auth_mode_to_api(auth.api_auth_mode());
let (reported_auth_method, token_opt) = if matches!(
auth,
CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_)
) || include_token
&& permanent_refresh_failure
{
// This response cannot represent the metadata needed to reuse these
// credentials.
(Some(auth_mode), None)
} else {
match auth.get_token() {
Ok(token) if !token.is_empty() => {
let tok = if include_token { Some(token) } else { None };
(Some(auth_mode), tok)
let (reported_auth_method, token_opt) =
if self.auth_manager.is_workload_identity_selected()
|| matches!(
auth,
CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_)
)
|| include_token && permanent_refresh_failure
{
// Host-owned and metadata-bearing credentials are never exported.
(Some(auth_mode), None)
} else {
match auth.get_token() {
Ok(token) if !token.is_empty() => {
let tok = if include_token { Some(token) } else { None };
(Some(auth_mode), tok)
}
Ok(_) => (None, None),
Err(err) => {
tracing::warn!("failed to get token for auth status: {err}");
(None, None)
}
}
Ok(_) => (None, None),
Err(err) => {
tracing::warn!("failed to get token for auth status: {err}");
(None, None)
}
}
};
};
GetAuthStatusResponse {
auth_method: reported_auth_method,
auth_token: token_opt,
+4
View File
@@ -2612,6 +2612,10 @@ impl AuthManager {
self.external_auth_provider().is_some()
}
pub fn is_workload_identity_selected(&self) -> bool {
self.workload_identity_selected
}
pub fn is_external_chatgpt_auth_active(&self) -> bool {
self.auth_cached()
.as_ref()