mirror of
https://github.com/openai/codex.git
synced 2026-10-30 00:09:58 +00:00
[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:
@@ -536,6 +536,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
+7
@@ -6951,6 +6951,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
+7
@@ -1077,6 +1077,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* Authentication mode for OpenAI-backed providers.
|
||||
*/
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "headers" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
|
||||
@@ -31,6 +31,11 @@ pub enum AuthMode {
|
||||
#[ts(rename = "chatgptAuthTokens")]
|
||||
#[strum(serialize = "chatgptAuthTokens")]
|
||||
ChatgptAuthTokens,
|
||||
/// Backend auth supplied as request headers.
|
||||
#[serde(rename = "headers")]
|
||||
#[ts(rename = "headers")]
|
||||
#[strum(serialize = "headers")]
|
||||
Headers,
|
||||
/// Programmatic Codex auth backed by a registered Agent Identity.
|
||||
#[serde(rename = "agentIdentity")]
|
||||
#[ts(rename = "agentIdentity")]
|
||||
@@ -53,7 +58,7 @@ impl AuthMode {
|
||||
pub fn has_chatgpt_account(self) -> bool {
|
||||
match self {
|
||||
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
Self::ApiKey | Self::Headers | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +67,7 @@ impl AuthMode {
|
||||
match self {
|
||||
Self::Chatgpt
|
||||
| Self::ChatgptAuthTokens
|
||||
| Self::Headers
|
||||
| Self::AgentIdentity
|
||||
| Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::BedrockApiKey => false,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub(crate) fn auth_mode_to_api(auth_mode: AuthMode) -> ApiAuthMode {
|
||||
AuthMode::ApiKey => ApiAuthMode::ApiKey,
|
||||
AuthMode::Chatgpt => ApiAuthMode::Chatgpt,
|
||||
AuthMode::ChatgptAuthTokens => ApiAuthMode::ChatgptAuthTokens,
|
||||
AuthMode::Headers => ApiAuthMode::Headers,
|
||||
AuthMode::AgentIdentity => ApiAuthMode::AgentIdentity,
|
||||
AuthMode::PersonalAccessToken => ApiAuthMode::PersonalAccessToken,
|
||||
AuthMode::BedrockApiKey => ApiAuthMode::BedrockApiKey,
|
||||
|
||||
@@ -855,7 +855,9 @@ impl AccountRequestProcessor {
|
||||
let auth_mode = auth_mode_to_api(auth.api_auth_mode());
|
||||
let (reported_auth_method, token_opt) = if matches!(
|
||||
auth,
|
||||
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_)
|
||||
CodexAuth::Headers(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_)
|
||||
) || include_token
|
||||
&& permanent_refresh_failure
|
||||
{
|
||||
|
||||
@@ -1327,6 +1327,7 @@ fn stored_auth_mode(auth: &codex_login::AuthDotJson) -> &'static str {
|
||||
AuthMode::ApiKey => "api_key",
|
||||
AuthMode::Chatgpt => "chatgpt",
|
||||
AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
|
||||
AuthMode::Headers => "headers",
|
||||
AuthMode::AgentIdentity => "agent_identity",
|
||||
AuthMode::PersonalAccessToken => "personal_access_token",
|
||||
AuthMode::BedrockApiKey => "bedrock_api_key",
|
||||
@@ -1397,6 +1398,9 @@ fn stored_auth_issues(
|
||||
issues.push("external ChatGPT auth is missing refresh metadata");
|
||||
}
|
||||
}
|
||||
AuthMode::Headers => {
|
||||
issues.push("header auth cannot be loaded from auth storage");
|
||||
}
|
||||
AuthMode::AgentIdentity => {
|
||||
if auth
|
||||
.agent_identity
|
||||
@@ -2466,6 +2470,7 @@ fn auth_mode_name(auth: &CodexAuth) -> &'static str {
|
||||
AuthMode::ApiKey => "api_key",
|
||||
AuthMode::Chatgpt => "chatgpt",
|
||||
AuthMode::ChatgptAuthTokens => "chatgpt_auth_tokens",
|
||||
AuthMode::Headers => "headers",
|
||||
AuthMode::AgentIdentity => "agent_identity",
|
||||
AuthMode::PersonalAccessToken => "personal_access_token",
|
||||
AuthMode::BedrockApiKey => "bedrock_api_key",
|
||||
@@ -2605,6 +2610,7 @@ fn provider_auth_reachability_mode_from_auth(
|
||||
Some(
|
||||
AuthMode::Chatgpt
|
||||
| AuthMode::ChatgptAuthTokens
|
||||
| AuthMode::Headers
|
||||
| AuthMode::AgentIdentity
|
||||
| AuthMode::PersonalAccessToken,
|
||||
)
|
||||
|
||||
@@ -449,6 +449,9 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
eprintln!("Logged in using ChatGPT");
|
||||
std::process::exit(0);
|
||||
}
|
||||
AuthMode::Headers => {
|
||||
unreachable!("header auth cannot be loaded from auth storage")
|
||||
}
|
||||
AuthMode::AgentIdentity => {
|
||||
eprintln!("Logged in using access token");
|
||||
std::process::exit(0);
|
||||
|
||||
@@ -69,7 +69,13 @@ pub use codex_extension_api::empty_extension_registry;
|
||||
pub use codex_features::Feature;
|
||||
pub use codex_features::Features;
|
||||
pub use codex_home::CodexHomeUserInstructionsProvider;
|
||||
pub use codex_login::AuthHeaders;
|
||||
pub use codex_login::AuthManager;
|
||||
pub use codex_login::CodexAuth;
|
||||
pub use codex_login::ExternalAuth;
|
||||
pub use codex_login::ExternalAuthFuture;
|
||||
pub use codex_login::ExternalAuthRefreshContext;
|
||||
pub use codex_login::ExternalAuthRefreshReason;
|
||||
pub use codex_login::default_client::set_default_originator;
|
||||
pub use codex_model_provider_info::OPENAI_PROVIDER_ID;
|
||||
pub use codex_model_provider_info::built_in_model_providers;
|
||||
|
||||
@@ -2141,6 +2141,7 @@ impl AuthRequestTelemetryContext {
|
||||
AuthMode::ApiKey | AuthMode::BedrockApiKey => "ApiKey",
|
||||
AuthMode::Chatgpt
|
||||
| AuthMode::ChatgptAuthTokens
|
||||
| AuthMode::Headers
|
||||
| AuthMode::AgentIdentity
|
||||
| AuthMode::PersonalAccessToken => "Chatgpt",
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use codex_login::AuthHeaders;
|
||||
use codex_login::CodexAuth;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderValue;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn header_auth_is_attached_to_responses_requests() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||
)
|
||||
.await;
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer external"));
|
||||
headers.insert("x-external-auth", HeaderValue::from_static("enabled"));
|
||||
let mut builder = test_codex().with_auth(CodexAuth::Headers(AuthHeaders::new(headers)));
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
let request = response_mock.single_request();
|
||||
assert_eq!(
|
||||
request.header("authorization").as_deref(),
|
||||
Some("Bearer external")
|
||||
);
|
||||
assert_eq!(
|
||||
request.header("x-external-auth").as_deref(),
|
||||
Some("enabled")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -54,6 +54,7 @@ mod exec;
|
||||
mod exec_policy;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod extension_sandbox;
|
||||
mod external_auth;
|
||||
mod fork_thread;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
mod guardian_review;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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(()),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -244,6 +244,7 @@ impl ModelProviderInfo {
|
||||
Some(
|
||||
AuthMode::Chatgpt
|
||||
| AuthMode::ChatgptAuthTokens
|
||||
| AuthMode::Headers
|
||||
| AuthMode::AgentIdentity
|
||||
| AuthMode::PersonalAccessToken
|
||||
)
|
||||
|
||||
@@ -148,6 +148,15 @@ fn test_personal_access_token_uses_chatgpt_codex_base_url() {
|
||||
assert_eq!(api_provider.base_url, CHATGPT_CODEX_BASE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_header_auth_uses_chatgpt_codex_base_url() {
|
||||
let api_provider = ModelProviderInfo::create_openai_provider(/*base_url*/ None)
|
||||
.to_api_provider(Some(AuthMode::Headers))
|
||||
.expect("OpenAI provider should build API provider");
|
||||
|
||||
assert_eq!(api_provider.base_url, CHATGPT_CODEX_BASE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_remote_compaction_for_azure_name() {
|
||||
let provider = ModelProviderInfo {
|
||||
|
||||
@@ -69,6 +69,7 @@ impl AmazonBedrockModelProvider {
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::Chatgpt(_)
|
||||
| CodexAuth::ChatgptAuthTokens(_)
|
||||
| CodexAuth::Headers(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_) => None,
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_agent_identity::authorization_header_for_agent_task;
|
||||
use codex_api::AgentIdentityTelemetry;
|
||||
use codex_api::AuthProvider;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_login::AuthHeaders;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::AgentIdentityAuth;
|
||||
@@ -109,6 +110,17 @@ impl AuthProvider for AgentIdentityAuthProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct HeaderAuthProvider {
|
||||
auth: AuthHeaders,
|
||||
}
|
||||
|
||||
impl AuthProvider for HeaderAuthProvider {
|
||||
fn add_auth_headers(&self, headers: &mut HeaderMap) {
|
||||
headers.extend(self.auth.headers().clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Some providers are meant to send no auth headers. Examples include local OSS
|
||||
// providers and custom test providers with `requires_openai_auth = false`.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -243,6 +255,7 @@ pub fn auth_provider_from_auth(auth: &CodexAuth) -> SharedAuthProvider {
|
||||
CodexAuth::AgentIdentity(auth) => {
|
||||
Arc::new(AgentIdentityAuthProvider { auth: auth.clone() })
|
||||
}
|
||||
CodexAuth::Headers(auth) => Arc::new(HeaderAuthProvider { auth: auth.clone() }),
|
||||
CodexAuth::BedrockApiKey(_) => unreachable!("{BEDROCK_API_KEY_UNSUPPORTED_MESSAGE}"),
|
||||
CodexAuth::ApiKey(_)
|
||||
| CodexAuth::Chatgpt(_)
|
||||
@@ -388,6 +401,21 @@ mod tests {
|
||||
assert!(auth.to_auth_headers().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_auth_adds_predefined_headers() {
|
||||
let mut expected = HeaderMap::new();
|
||||
expected.insert(
|
||||
http::header::AUTHORIZATION,
|
||||
HeaderValue::from_static("Bearer external"),
|
||||
);
|
||||
expected.insert("x-external-auth", HeaderValue::from_static("enabled"));
|
||||
let auth = CodexAuth::Headers(AuthHeaders::new(expected.clone()));
|
||||
|
||||
let actual = auth_provider_from_auth(&auth).to_auth_headers();
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_provider_rejects_bedrock_api_key_auth() {
|
||||
let provider = ModelProviderInfo::create_openai_provider(/*base_url*/ None);
|
||||
|
||||
@@ -277,6 +277,9 @@ impl ModelProvider for ConfiguredModelProvider {
|
||||
if auth_manager.refresh_failure_for_auth(&auth).is_some() {
|
||||
return None;
|
||||
}
|
||||
if matches!(auth, CodexAuth::Headers(_)) {
|
||||
return None;
|
||||
}
|
||||
Some(auth)
|
||||
})
|
||||
.map(|auth| match &auth {
|
||||
@@ -286,6 +289,7 @@ impl ModelProvider for ConfiguredModelProvider {
|
||||
}
|
||||
CodexAuth::Chatgpt(_)
|
||||
| CodexAuth::ChatgptAuthTokens(_)
|
||||
| CodexAuth::Headers(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_) => {
|
||||
let email = auth.get_account_email();
|
||||
|
||||
@@ -59,6 +59,7 @@ impl From<AuthMode> for TelemetryAuthMode {
|
||||
AuthMode::ApiKey | AuthMode::BedrockApiKey => Self::ApiKey,
|
||||
AuthMode::Chatgpt
|
||||
| AuthMode::ChatgptAuthTokens
|
||||
| AuthMode::Headers
|
||||
| AuthMode::AgentIdentity
|
||||
| AuthMode::PersonalAccessToken => Self::Chatgpt,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ pub enum AuthMode {
|
||||
#[serde(rename = "chatgptAuthTokens")]
|
||||
#[strum(serialize = "chatgptAuthTokens")]
|
||||
ChatgptAuthTokens,
|
||||
/// Codex backend auth supplied as request headers.
|
||||
#[serde(rename = "headers")]
|
||||
#[strum(serialize = "headers")]
|
||||
Headers,
|
||||
/// Programmatic Codex auth backed by a registered Agent Identity.
|
||||
#[serde(rename = "agentIdentity")]
|
||||
#[strum(serialize = "agentIdentity")]
|
||||
@@ -34,7 +38,7 @@ impl AuthMode {
|
||||
pub fn has_chatgpt_account(self) -> bool {
|
||||
match self {
|
||||
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
Self::ApiKey | Self::Headers | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +47,7 @@ impl AuthMode {
|
||||
match self {
|
||||
Self::Chatgpt
|
||||
| Self::ChatgptAuthTokens
|
||||
| Self::Headers
|
||||
| Self::AgentIdentity
|
||||
| Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::BedrockApiKey => false,
|
||||
|
||||
@@ -1207,7 +1207,7 @@ pub(crate) fn status_account_display_from_auth_mode(
|
||||
email: None,
|
||||
plan: plan_type.map(plan_type_display_name),
|
||||
}),
|
||||
Some(AuthMode::BedrockApiKey) => None,
|
||||
Some(AuthMode::Headers) | Some(AuthMode::BedrockApiKey) => None,
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,6 +952,7 @@ impl AuthModeWidget {
|
||||
ApiAuthMode::ApiKey => AuthMode::ApiKey,
|
||||
ApiAuthMode::Chatgpt => AuthMode::Chatgpt,
|
||||
ApiAuthMode::ChatgptAuthTokens => AuthMode::ChatgptAuthTokens,
|
||||
ApiAuthMode::Headers => AuthMode::Headers,
|
||||
ApiAuthMode::AgentIdentity => AuthMode::AgentIdentity,
|
||||
ApiAuthMode::PersonalAccessToken => AuthMode::PersonalAccessToken,
|
||||
ApiAuthMode::BedrockApiKey => AuthMode::BedrockApiKey,
|
||||
|
||||
Reference in New Issue
Block a user