[codex] Add externally provided Codex auth (#31274)

## Summary

Add an in-memory externally provided Codex auth snapshot with explicit
runtime capabilities, installed through the existing `ExternalAuth`
provider path.

## Testing

- `just fmt-check`
- `cargo test -p codex-login --lib externally_provided_auth`
- `cargo test -p codex-core --lib
external_auth_snapshot_is_installed_from_runtime_config`
- `cargo test -p codex-model-provider --lib external_auth`
- `cargo test -p codex-mcp-extension --test hosted_apps_mcp
hosted_apps_mcp_accepts_external_provided_codex_auth`
- `cargo check -p codex-app-server -p codex-core-api -p
codex-thread-manager-sample`

---------

Co-authored-by: pakrym-oai <pakrym@openai.com>
This commit is contained in:
lt-oaiandpakrym-oai authored and GitHub committed 2026-07-07 17:43:19 -07:00
1 parent dbd2df27d1
commit 8784de445e
29 files changed
+278 -7

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
use std::fmt;
use reqwest::header::HeaderMap;
/// Request headers returned by an external auth provider.
///
/// The provider owns credential validation, rotation, and persistence. Codex
/// keeps the resolved headers in memory and attaches them to backend requests.
#[derive(Clone, PartialEq, Eq)]
pub struct AuthHeaders {
headers: HeaderMap,
}
impl AuthHeaders {
pub fn new(headers: HeaderMap) -> Self {
Self { headers }
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
}
impl fmt::Debug for AuthHeaders {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthHeaders")
.field("headers", &"<redacted>")
.finish()
}
}
+55
View File
@@ -1093,6 +1093,61 @@ async fn unauthorized_recovery_uses_external_refresh_for_bearer_manager() {
assert_eq!(refreshed_token.as_deref(), Some("refreshed-provider-token"));
}
#[derive(Clone)]
struct StaticExternalAuth(CodexAuth);
impl ExternalAuth for StaticExternalAuth {
fn resolve(&self) -> ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Ok(self.0.clone()) })
}
fn refresh(&self, _context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> {
Box::pin(async { Ok(self.0.clone()) })
}
}
#[tokio::test]
async fn external_auth_provider_can_install_headers() {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_static("Bearer external"),
);
headers.insert(
"x-external-auth",
reqwest::header::HeaderValue::from_static("enabled"),
);
let auth = CodexAuth::Headers(AuthHeaders::new(headers));
let codex_home = tempdir().expect("tempdir");
let manager = AuthManager::new(
codex_home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::Ephemeral,
/*forced_chatgpt_workspace_id*/ None,
/*chatgpt_base_url*/ None,
AuthKeyringBackendKind::default(),
/*auth_route_config*/ None,
)
.await;
manager
.set_external_auth(Arc::new(StaticExternalAuth(auth.clone())))
.await
.expect("external auth should install");
assert_eq!(manager.auth_cached(), Some(auth));
assert!(
manager
.auth_cached()
.is_some_and(|auth| auth.uses_codex_backend())
);
assert!(
!manager
.auth_cached()
.is_some_and(|auth| auth.is_chatgpt_auth())
);
}
struct ProviderAuthScript {
tempdir: TempDir,
command: String,
@@ -85,6 +85,7 @@ async fn login_with_bedrock_api_key_replaces_openai_auth() -> anyhow::Result<()>
CodexAuth::ApiKey(_)
| CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => None,
}),
@@ -148,6 +149,7 @@ async fn bedrock_only_auth_storage_creates_primary_auth() -> anyhow::Result<()>
CodexAuth::ApiKey(_)
| CodexAuth::Chatgpt(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_) => None,
}),
+37 -2
View File
@@ -38,6 +38,7 @@ use super::agent_identity::require_agent_identity_authapi_base_url;
use super::agent_identity::verified_record_from_jwt;
use super::external_bearer::BearerTokenRefresher;
use super::revoke::revoke_auth_tokens;
use crate::auth::AuthHeaders;
pub use crate::auth::agent_identity::AgentIdentityAuth;
pub use crate::auth::agent_identity::AgentIdentityAuthError;
pub use crate::auth::bedrock_api_key::BedrockApiKeyAuth;
@@ -71,6 +72,7 @@ pub enum CodexAuth {
ApiKey(ApiKeyAuth),
Chatgpt(ChatgptAuth),
ChatgptAuthTokens(ChatgptAuthTokens),
Headers(AuthHeaders),
AgentIdentity(AgentIdentityAuth),
PersonalAccessToken(PersonalAccessTokenAuth),
BedrockApiKey(BedrockApiKeyAuth),
@@ -145,6 +147,7 @@ impl AgentIdentityBootstrapCooldown {
impl PartialEq for CodexAuth {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Headers(a), Self::Headers(b)) => a == b,
(Self::PersonalAccessToken(a), Self::PersonalAccessToken(b)) => a == b,
(Self::BedrockApiKey(a), Self::BedrockApiKey(b)) => a == b,
_ => self.api_auth_mode() == other.api_auth_mode(),
@@ -308,6 +311,11 @@ impl CodexAuth {
};
return Ok(Self::BedrockApiKey(auth));
}
if auth_mode == AuthMode::Headers {
return Err(std::io::Error::other(
"externally provided auth cannot be loaded from auth storage.",
));
}
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
let client = create_default_auth_client(&refresh_token_endpoint(), auth_route_config)?;
@@ -327,6 +335,9 @@ impl CodexAuth {
}
AuthMode::ChatgptAuthTokens => Ok(Self::ChatgptAuthTokens(ChatgptAuthTokens { state })),
AuthMode::ApiKey => unreachable!("api key mode is handled above"),
AuthMode::Headers => {
unreachable!("externally provided auth is never loaded from auth storage")
}
AuthMode::AgentIdentity => unreachable!("agent identity mode is handled above"),
AuthMode::PersonalAccessToken => {
unreachable!("personal access token mode is handled above")
@@ -409,6 +420,7 @@ impl CodexAuth {
match self {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => AuthMode::Chatgpt,
Self::Headers(_) => AuthMode::Headers,
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
@@ -421,6 +433,7 @@ impl CodexAuth {
Self::ApiKey(_) => AuthMode::ApiKey,
Self::Chatgpt(_) => AuthMode::Chatgpt,
Self::ChatgptAuthTokens(_) => AuthMode::ChatgptAuthTokens,
Self::Headers(_) => AuthMode::Headers,
Self::AgentIdentity(_) => AuthMode::AgentIdentity,
Self::PersonalAccessToken(_) => AuthMode::PersonalAccessToken,
Self::BedrockApiKey(_) => AuthMode::BedrockApiKey,
@@ -448,7 +461,10 @@ impl CodexAuth {
}
fn supports_unauthorized_recovery(&self) -> bool {
matches!(self, Self::Chatgpt(_) | Self::ChatgptAuthTokens(_))
matches!(
self,
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) | Self::Headers(_)
)
}
/// Returns `None` if `auth_mode() != AuthMode::ApiKey`.
@@ -457,6 +473,7 @@ impl CodexAuth {
Self::ApiKey(auth) => Some(auth.api_key.as_str()),
Self::Chatgpt(_)
| Self::ChatgptAuthTokens(_)
| Self::Headers(_)
| Self::AgentIdentity(_)
| Self::PersonalAccessToken(_)
| Self::BedrockApiKey(_) => None,
@@ -487,6 +504,9 @@ impl CodexAuth {
Self::AgentIdentity(_) => Err(std::io::Error::other(
"agent identity auth does not expose a bearer token",
)),
Self::Headers(_) => Err(std::io::Error::other(
"header auth does not expose a bearer token",
)),
Self::PersonalAccessToken(auth) => Ok(auth.access_token().to_string()),
Self::BedrockApiKey(_) => Err(std::io::Error::other(
"Bedrock API key auth does not expose a Codex bearer token",
@@ -497,6 +517,7 @@ impl CodexAuth {
/// Returns `None` if Codex backend auth does not expose an account id.
pub fn get_account_id(&self) -> Option<String> {
match self {
Self::Headers(_) => None,
Self::AgentIdentity(auth) => Some(auth.account_id().to_string()),
Self::PersonalAccessToken(auth) => Some(auth.account_id().to_string()),
_ => self.get_current_token_data().and_then(|t| t.account_id),
@@ -506,6 +527,7 @@ impl CodexAuth {
/// Returns false if Codex backend auth omits the FedRAMP claim.
pub fn is_fedramp_account(&self) -> bool {
match self {
Self::Headers(_) => false,
Self::AgentIdentity(auth) => auth.is_fedramp_account(),
Self::PersonalAccessToken(auth) => auth.is_fedramp_account(),
_ => self
@@ -517,6 +539,7 @@ impl CodexAuth {
/// Returns `None` if Codex backend auth does not expose an account email.
pub fn get_account_email(&self) -> Option<String> {
match self {
Self::Headers(_) => None,
Self::AgentIdentity(auth) => auth.email().map(str::to_string),
Self::PersonalAccessToken(auth) => auth.email().map(str::to_string),
_ => self.get_current_token_data().and_then(|t| t.id_token.email),
@@ -526,6 +549,7 @@ impl CodexAuth {
/// Returns `None` if Codex backend auth does not expose a ChatGPT user id.
pub fn get_chatgpt_user_id(&self) -> Option<String> {
match self {
Self::Headers(_) => None,
Self::AgentIdentity(auth) => Some(auth.chatgpt_user_id().to_string()),
Self::PersonalAccessToken(auth) => Some(auth.chatgpt_user_id().to_string()),
_ => self
@@ -538,6 +562,9 @@ impl CodexAuth {
/// Returns a high-level `AccountPlanType` (e.g., Free/Plus/Pro/Team/…)
/// for UI or product decisions based on the user's subscription.
pub fn account_plan_type(&self) -> Option<AccountPlanType> {
if matches!(self, Self::Headers(_)) {
return None;
}
if let Self::AgentIdentity(auth) = self {
return Some(auth.plan_type());
}
@@ -564,6 +591,7 @@ impl CodexAuth {
Self::Chatgpt(auth) => &auth.state,
Self::ChatgptAuthTokens(auth) => &auth.state,
Self::ApiKey(_)
| Self::Headers(_)
| Self::AgentIdentity(_)
| Self::PersonalAccessToken(_)
| Self::BedrockApiKey(_) => return None,
@@ -609,6 +637,7 @@ impl CodexAuth {
Self::AgentIdentity(auth) => Ok(Some(auth.clone())),
Self::ApiKey(_)
| Self::ChatgptAuthTokens(_)
| Self::Headers(_)
| Self::PersonalAccessToken(_)
| Self::BedrockApiKey(_) => Ok(None),
Self::Chatgpt(_) => {
@@ -1055,10 +1084,12 @@ async fn enforce_login_restrictions_with_agent_identity_authapi_base_url(
| (ForcedLoginMethod::Api, AuthMode::BedrockApiKey) => None,
(ForcedLoginMethod::Chatgpt, AuthMode::Chatgpt)
| (ForcedLoginMethod::Chatgpt, AuthMode::ChatgptAuthTokens)
| (ForcedLoginMethod::Chatgpt, AuthMode::Headers)
| (ForcedLoginMethod::Chatgpt, AuthMode::AgentIdentity)
| (ForcedLoginMethod::Chatgpt, AuthMode::PersonalAccessToken) => None,
(ForcedLoginMethod::Api, AuthMode::Chatgpt)
| (ForcedLoginMethod::Api, AuthMode::ChatgptAuthTokens)
| (ForcedLoginMethod::Api, AuthMode::Headers)
| (ForcedLoginMethod::Api, AuthMode::AgentIdentity)
| (ForcedLoginMethod::Api, AuthMode::PersonalAccessToken) => Some(
"API key login is required, but ChatGPT is currently being used. Logging out."
@@ -1083,7 +1114,9 @@ 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::BedrockApiKey(_) => return Ok(()),
CodexAuth::ApiKey(_) | CodexAuth::Headers(_) | CodexAuth::BedrockApiKey(_) => {
return Ok(());
}
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_) => {
auth.get_account_id()
}
@@ -2117,6 +2150,7 @@ impl AuthManager {
| (AuthMode::ChatgptAuthTokens, AuthMode::ChatgptAuthTokens) => {
a.get_current_auth_json() == b.get_current_auth_json()
}
(AuthMode::Headers, AuthMode::Headers) => a == b,
(AuthMode::AgentIdentity, AuthMode::AgentIdentity) => match (a, b) {
(CodexAuth::AgentIdentity(a), CodexAuth::AgentIdentity(b)) => {
a.record() == b.record()
@@ -2406,6 +2440,7 @@ impl AuthManager {
}
CodexAuth::ApiKey(_)
| CodexAuth::ChatgptAuthTokens(_)
| CodexAuth::Headers(_)
| CodexAuth::AgentIdentity(_)
| CodexAuth::PersonalAccessToken(_)
| CodexAuth::BedrockApiKey(_) => Ok(()),
+2
View File
@@ -1,5 +1,6 @@
mod access_token;
mod agent_identity;
mod auth_headers;
mod bedrock_api_key;
pub mod default_client;
pub mod error;
@@ -11,6 +12,7 @@ mod external_bearer;
mod manager;
mod revoke;
pub use auth_headers::AuthHeaders;
pub use bedrock_api_key::BedrockApiKeyAuth;
pub use bedrock_api_key::login_with_bedrock_api_key;
pub use error::RefreshTokenFailedError;
+1
View File
@@ -25,6 +25,7 @@ pub use success_page::LoginSuccessPageBrand;
pub use auth::AgentIdentityAuthPolicy;
pub use auth::AuthConfig;
pub use auth::AuthDotJson;
pub use auth::AuthHeaders;
pub use auth::AuthKeyringBackendKind;
pub use auth::AuthManager;
pub use auth::AuthManagerConfig;