Add renewable EMA HTTP authentication and credential versioning (#49987)

## What changed

- Add `EmaAuthenticatedHttpClient` to cache and renew resource bearers, serialize concurrent renewals, and include authentication time in the request deadline. Confine bearers to the configured MCP endpoint and reject redirects.
- Return authentication-required errors for `401` responses without replaying MCP operations. Preserve a newer cached bearer when a delayed response rejects an older one.
- Add revocable request authority that cancels pending requests and ends response streams.
- Pin enterprise credentials to a persistent version advanced on save or deletion. Use short credential locks and invalidate rejected grants only when their record and version are still current, preserving newer logins.
- Hide keyring account identifiers from enterprise credential errors and traces while preserving authentication failure classification.

## Testing

Add tests for token expiry and renewal, endpoint confinement, redirects, request deadlines, revocation during renewal and streaming, delayed `401` responses, credential replacement and logout races, and credential error privacy.

GitOrigin-RevId: ee3e5cf7ecbba3d09e1ab29ed4fbb2a35eacc60d
This commit is contained in:
Nick Steele
2026-10-01 13:57:48 +00:00
committed by copyberry
parent a933dd77db
commit ecc78e4cf5
23 changed files with 2109 additions and 61 deletions
+26
View File
@@ -1,10 +1,13 @@
use anyhow::Error;
use codex_exec_server::ExecServerError;
use rmcp::service::ClientInitializeError;
use rmcp::service::ServiceError;
use rmcp::transport::DynamicTransportError;
use rmcp::transport::auth::AuthError;
use rmcp::transport::streamable_http_client::StreamableHttpError;
use crate::ema_auth_policy::EmaAuthFailure;
use crate::ema_auth_policy::EmaInvalidGrantSource;
use crate::http_client_adapter::StreamableHttpClientAdapterError;
use crate::rmcp_client::ClientOperationError;
@@ -18,6 +21,22 @@ pub fn is_authentication_required_error(error: &Error) -> bool {
source
.downcast_ref::<AuthError>()
.is_some_and(auth_error_requires_authentication)
|| source
.downcast_ref::<EmaAuthFailure>()
.is_some_and(|error| match error {
EmaAuthFailure::InvalidGrant {
grant_source: EmaInvalidGrantSource::EnterpriseIdentity,
}
| EmaAuthFailure::InsufficientUserAuthentication
| EmaAuthFailure::ReauthenticationRequired => true,
// Rejecting an ID-JAG does not invalidate the enterprise login itself.
EmaAuthFailure::InvalidGrant {
grant_source: EmaInvalidGrantSource::ResourceAuthorization,
} => false,
})
|| source
.downcast_ref::<ExecServerError>()
.is_some_and(exec_server_error_requires_authentication)
|| source
.downcast_ref::<ClientInitializeError>()
.is_some_and(|mut error| {
@@ -49,10 +68,17 @@ fn transport_error_requires_authentication(error: &DynamicTransportError) -> boo
.is_some_and(|error| match error {
StreamableHttpError::AuthRequired(_) => true,
StreamableHttpError::Auth(auth_error) => auth_error_requires_authentication(auth_error),
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(error)) => {
exec_server_error_requires_authentication(error)
}
_ => false,
})
}
fn exec_server_error_requires_authentication(error: &ExecServerError) -> bool {
matches!(error, ExecServerError::AuthenticationRequired(_))
}
fn auth_error_requires_authentication(error: &AuthError) -> bool {
matches!(
error,