mirror of
https://github.com/openai/codex.git
synced 2026-05-06 12:26:38 +00:00
Compare commits
3 Commits
pr20369
...
efrazer/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae616594c8 | ||
|
|
5512954961 | ||
|
|
2e0af353bc |
@@ -91,7 +91,7 @@ struct AgentIdentityBinding {
|
||||
binding_id: String,
|
||||
chatgpt_account_id: String,
|
||||
chatgpt_user_id: Option<String>,
|
||||
access_token: String,
|
||||
access_token: Option<String>,
|
||||
}
|
||||
|
||||
struct GeneratedAgentKeyMaterial {
|
||||
@@ -116,10 +116,15 @@ impl AgentIdentityManager {
|
||||
|
||||
pub(crate) fn is_enabled(&self) -> bool {
|
||||
self.feature_enabled
|
||||
|| self
|
||||
.auth_manager
|
||||
.auth_cached()
|
||||
.as_ref()
|
||||
.is_some_and(CodexAuth::is_agent_identity_only)
|
||||
}
|
||||
|
||||
pub(crate) async fn ensure_registered_identity(&self) -> Result<Option<StoredAgentIdentity>> {
|
||||
if !self.feature_enabled {
|
||||
if !self.is_enabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -154,7 +159,7 @@ impl AgentIdentityManager {
|
||||
}
|
||||
|
||||
pub(crate) async fn task_matches_current_identity(&self, task: &RegisteredAgentTask) -> bool {
|
||||
if !self.feature_enabled {
|
||||
if !self.is_enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -244,11 +249,15 @@ impl AgentIdentityManager {
|
||||
target_url: &str,
|
||||
) -> Result<String> {
|
||||
let url = agent_identity_biscuit_url(&self.chatgpt_base_url);
|
||||
let access_token = binding
|
||||
.access_token
|
||||
.as_deref()
|
||||
.context("ChatGPT access token is unavailable")?;
|
||||
let request_id = agent_identity_request_id()?;
|
||||
let client = create_client();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.bearer_auth(&binding.access_token)
|
||||
.bearer_auth(access_token)
|
||||
.header("X-Request-Id", request_id.clone())
|
||||
.header("X-Original-Method", target_method)
|
||||
.header("X-Original-Url", target_url)
|
||||
@@ -455,6 +464,16 @@ impl AgentIdentityBinding {
|
||||
return None;
|
||||
}
|
||||
|
||||
if auth.is_agent_identity_only() {
|
||||
let record = auth.agent_identity_record()?;
|
||||
return Some(Self {
|
||||
binding_id: format!("chatgpt-account-{}", record.workspace_id),
|
||||
chatgpt_account_id: record.workspace_id,
|
||||
chatgpt_user_id: record.chatgpt_user_id,
|
||||
access_token: None,
|
||||
});
|
||||
}
|
||||
|
||||
let token_data = auth.get_token_data().ok()?;
|
||||
let resolved_account_id =
|
||||
forced_workspace_id
|
||||
@@ -471,7 +490,7 @@ impl AgentIdentityBinding {
|
||||
.id_token
|
||||
.chatgpt_user_id
|
||||
.filter(|value| !value.is_empty()),
|
||||
access_token: token_data.access_token,
|
||||
access_token: Some(token_data.access_token),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -582,6 +601,28 @@ mod tests {
|
||||
assert_eq!(manager.ensure_registered_identity().await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_registered_identity_uses_agent_identity_only_auth_when_feature_is_disabled() {
|
||||
let auth = make_agent_identity_only_auth("account-123", Some("user-123"), "agent-123");
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth);
|
||||
let manager = AgentIdentityManager::new_for_tests(
|
||||
auth_manager,
|
||||
/*feature_enabled*/ false,
|
||||
"https://chatgpt.com/backend-api/".to_string(),
|
||||
SessionSource::Cli,
|
||||
);
|
||||
|
||||
let stored = manager
|
||||
.ensure_registered_identity()
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("agent identity-only auth should load the stored identity");
|
||||
|
||||
assert_eq!(stored.agent_runtime_id, "agent-123");
|
||||
assert_eq!(stored.chatgpt_account_id, "account-123");
|
||||
assert_eq!(stored.chatgpt_user_id.as_deref(), Some("user-123"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_registered_identity_skips_for_api_key_auth() {
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test-key"));
|
||||
@@ -830,6 +871,33 @@ mod tests {
|
||||
.expect("auth")
|
||||
}
|
||||
|
||||
fn make_agent_identity_only_auth(
|
||||
account_id: &str,
|
||||
user_id: Option<&str>,
|
||||
agent_runtime_id: &str,
|
||||
) -> CodexAuth {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let key_material = generate_agent_key_material().expect("key material");
|
||||
let auth_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(AgentIdentityAuthRecord {
|
||||
workspace_id: account_id.to_string(),
|
||||
chatgpt_user_id: user_id.map(ToOwned::to_owned),
|
||||
agent_runtime_id: agent_runtime_id.to_string(),
|
||||
agent_private_key: key_material.private_key_pkcs8_base64,
|
||||
registered_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
background_task_id: None,
|
||||
}),
|
||||
};
|
||||
save_auth(tempdir.path(), &auth_json, AuthCredentialsStoreMode::File).expect("save auth");
|
||||
CodexAuth::from_auth_storage(tempdir.path(), AuthCredentialsStoreMode::File)
|
||||
.expect("load auth")
|
||||
.expect("auth")
|
||||
}
|
||||
|
||||
fn fake_id_token(account_id: &str, user_id: Option<&str>) -> String {
|
||||
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
|
||||
let payload = serde_json::json!({
|
||||
|
||||
@@ -36,7 +36,7 @@ struct RegisterTaskResponse {
|
||||
|
||||
impl AgentIdentityManager {
|
||||
pub(crate) async fn register_task(&self) -> Result<Option<RegisteredAgentTask>> {
|
||||
if !self.feature_enabled {
|
||||
if !self.is_enabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -65,12 +65,15 @@ impl AgentIdentityManager {
|
||||
let client = create_client();
|
||||
let url =
|
||||
agent_task_registration_url(&self.chatgpt_base_url, &stored_identity.agent_runtime_id);
|
||||
let human_biscuit = self.mint_human_biscuit(&binding, "POST", &url).await?;
|
||||
let response = client
|
||||
let mut request = client
|
||||
.post(&url)
|
||||
.header("X-OpenAI-Authorization", human_biscuit)
|
||||
.json(&request_body)
|
||||
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT)
|
||||
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT);
|
||||
if binding.access_token.is_some() {
|
||||
let human_biscuit = self.mint_human_biscuit(&binding, "POST", &url).await?;
|
||||
request = request.header("X-OpenAI-Authorization", human_biscuit);
|
||||
}
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("failed to send agent task registration request to {url}"))?;
|
||||
@@ -198,6 +201,50 @@ mod tests {
|
||||
assert_eq!(manager.register_task().await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_task_uses_agent_identity_only_auth_when_feature_is_disabled() {
|
||||
let server = MockServer::start().await;
|
||||
let chatgpt_base_url = server.uri();
|
||||
let auth = make_agent_identity_only_auth("account-123", Some("user-123"), "agent-123");
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth.clone());
|
||||
let manager = AgentIdentityManager::new_for_tests(
|
||||
auth_manager,
|
||||
/*feature_enabled*/ false,
|
||||
chatgpt_base_url,
|
||||
SessionSource::Cli,
|
||||
);
|
||||
let stored_identity = manager
|
||||
.current_stored_identity()
|
||||
.await
|
||||
.expect("stored identity");
|
||||
let encrypted_task_id =
|
||||
encrypt_task_id_for_identity(&stored_identity, "task_123").expect("task ciphertext");
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/agent/agent-123/task/register"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"encrypted_task_id": encrypted_task_id,
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let task = manager
|
||||
.register_task()
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("task should be registered");
|
||||
|
||||
assert_eq!(
|
||||
task,
|
||||
RegisteredAgentTask {
|
||||
agent_runtime_id: "agent-123".to_string(),
|
||||
task_id: "task_123".to_string(),
|
||||
registered_at: task.registered_at.clone(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_task_skips_for_api_key_auth() {
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test-key"));
|
||||
@@ -452,6 +499,33 @@ mod tests {
|
||||
.expect("auth")
|
||||
}
|
||||
|
||||
fn make_agent_identity_only_auth(
|
||||
account_id: &str,
|
||||
user_id: Option<&str>,
|
||||
agent_runtime_id: &str,
|
||||
) -> CodexAuth {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let key_material = generate_agent_key_material().expect("key material");
|
||||
let auth_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(AgentIdentityAuthRecord {
|
||||
workspace_id: account_id.to_string(),
|
||||
chatgpt_user_id: user_id.map(ToOwned::to_owned),
|
||||
agent_runtime_id: agent_runtime_id.to_string(),
|
||||
agent_private_key: key_material.private_key_pkcs8_base64,
|
||||
registered_at: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
|
||||
background_task_id: None,
|
||||
}),
|
||||
};
|
||||
save_auth(tempdir.path(), &auth_json, AuthCredentialsStoreMode::File).expect("save auth");
|
||||
CodexAuth::from_auth_storage(tempdir.path(), AuthCredentialsStoreMode::File)
|
||||
.expect("load auth")
|
||||
.expect("auth")
|
||||
}
|
||||
|
||||
fn fake_id_token(account_id: &str, user_id: Option<&str>) -> String {
|
||||
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
|
||||
let payload = serde_json::json!({
|
||||
|
||||
@@ -74,6 +74,10 @@ impl BackgroundAgentTaskAuthMode {
|
||||
fn is_enabled(self) -> bool {
|
||||
matches!(self, Self::Enabled)
|
||||
}
|
||||
|
||||
fn is_enabled_for_auth(self, auth: &CodexAuth) -> bool {
|
||||
self.is_enabled() || auth.is_agent_identity_only()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -124,7 +128,7 @@ struct AgentIdentityBinding {
|
||||
binding_id: String,
|
||||
chatgpt_account_id: String,
|
||||
chatgpt_user_id: Option<String>,
|
||||
access_token: String,
|
||||
access_token: Option<String>,
|
||||
}
|
||||
|
||||
struct GeneratedAgentKeyMaterial {
|
||||
@@ -166,7 +170,7 @@ impl BackgroundAgentTaskManager {
|
||||
&self,
|
||||
auth: &CodexAuth,
|
||||
) -> Result<Option<String>> {
|
||||
if !self.auth_mode.is_enabled() {
|
||||
if !self.auth_mode.is_enabled_for_auth(auth) {
|
||||
debug!("skipping background agent task auth because agent identity is disabled");
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -314,12 +318,15 @@ impl BackgroundAgentTaskManager {
|
||||
let client = create_client();
|
||||
let url =
|
||||
agent_task_registration_url(&self.chatgpt_base_url, &stored_identity.agent_runtime_id);
|
||||
let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?;
|
||||
let response = client
|
||||
let mut request = client
|
||||
.post(&url)
|
||||
.header("X-OpenAI-Authorization", human_biscuit)
|
||||
.json(&request_body)
|
||||
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT)
|
||||
.timeout(AGENT_TASK_REGISTRATION_TIMEOUT);
|
||||
if binding.access_token.is_some() {
|
||||
let human_biscuit = self.mint_human_biscuit(binding, "POST", &url).await?;
|
||||
request = request.header("X-OpenAI-Authorization", human_biscuit);
|
||||
}
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("failed to send background agent task request to {url}"))?;
|
||||
@@ -353,11 +360,15 @@ impl BackgroundAgentTaskManager {
|
||||
target_url: &str,
|
||||
) -> Result<String> {
|
||||
let url = agent_identity_biscuit_url(&self.chatgpt_base_url);
|
||||
let access_token = binding
|
||||
.access_token
|
||||
.as_deref()
|
||||
.context("ChatGPT access token is unavailable")?;
|
||||
let request_id = agent_identity_request_id()?;
|
||||
let client = create_client();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.bearer_auth(&binding.access_token)
|
||||
.bearer_auth(access_token)
|
||||
.header("X-Request-Id", request_id.clone())
|
||||
.header("X-Original-Method", target_method)
|
||||
.header("X-Original-Url", target_url)
|
||||
@@ -448,7 +459,7 @@ pub fn cached_background_agent_task_authorization_header_value(
|
||||
auth: &CodexAuth,
|
||||
auth_mode: BackgroundAgentTaskAuthMode,
|
||||
) -> Result<Option<String>> {
|
||||
if !auth_mode.is_enabled() {
|
||||
if !auth_mode.is_enabled_for_auth(auth) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -551,6 +562,16 @@ impl AgentIdentityBinding {
|
||||
return None;
|
||||
}
|
||||
|
||||
if auth.is_agent_identity_only() {
|
||||
let record = auth.agent_identity_record()?;
|
||||
return Some(Self {
|
||||
binding_id: format!("chatgpt-account-{}", record.workspace_id),
|
||||
chatgpt_account_id: record.workspace_id,
|
||||
chatgpt_user_id: record.chatgpt_user_id,
|
||||
access_token: None,
|
||||
});
|
||||
}
|
||||
|
||||
let token_data = auth.get_token_data().ok()?;
|
||||
let resolved_account_id =
|
||||
forced_workspace_id
|
||||
@@ -567,7 +588,7 @@ impl AgentIdentityBinding {
|
||||
.id_token
|
||||
.chatgpt_user_id
|
||||
.filter(|value| !value.is_empty()),
|
||||
access_token: token_data.access_token,
|
||||
access_token: Some(token_data.access_token),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -797,6 +818,31 @@ mod tests {
|
||||
assert_eq!(None, authorization_header_value);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_background_agent_task_auth_uses_agent_identity_only_auth() {
|
||||
let auth = make_agent_identity_only_auth(
|
||||
"account_id",
|
||||
/*user_id*/ None,
|
||||
"agent_123",
|
||||
Some("task_123"),
|
||||
);
|
||||
let auth_manager = AuthManager::from_auth_for_testing(auth.clone());
|
||||
let manager = BackgroundAgentTaskManager::new_with_auth_mode(
|
||||
auth_manager,
|
||||
"https://chatgpt.com/backend-api".to_string(),
|
||||
SessionSource::Cli,
|
||||
BackgroundAgentTaskAuthMode::Disabled,
|
||||
);
|
||||
|
||||
let authorization_header_value = manager
|
||||
.authorization_header_value_for_auth(&auth)
|
||||
.await
|
||||
.expect("identity-only auth should not fail")
|
||||
.expect("identity-only auth should return an agent assertion");
|
||||
|
||||
assert!(authorization_header_value.starts_with("AgentAssertion "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_background_agent_task_auth_honors_disabled_mode() {
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
@@ -827,4 +873,36 @@ mod tests {
|
||||
assert_eq!(None, disabled_authorization_header_value);
|
||||
assert!(enabled_authorization_header_value.is_some());
|
||||
}
|
||||
|
||||
fn make_agent_identity_only_auth(
|
||||
account_id: &str,
|
||||
user_id: Option<&str>,
|
||||
agent_runtime_id: &str,
|
||||
background_task_id: Option<&str>,
|
||||
) -> CodexAuth {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let key_material = generate_agent_key_material().expect("generate key material");
|
||||
crate::save_auth(
|
||||
tempdir.path(),
|
||||
&crate::AuthDotJson {
|
||||
auth_mode: Some(codex_app_server_protocol::AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(AgentIdentityAuthRecord {
|
||||
workspace_id: account_id.to_string(),
|
||||
chatgpt_user_id: user_id.map(ToOwned::to_owned),
|
||||
agent_runtime_id: agent_runtime_id.to_string(),
|
||||
agent_private_key: key_material.private_key_pkcs8_base64,
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: background_task_id.map(ToOwned::to_owned),
|
||||
}),
|
||||
},
|
||||
crate::AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("save auth");
|
||||
CodexAuth::from_auth_storage(tempdir.path(), crate::AuthCredentialsStoreMode::File)
|
||||
.expect("load auth")
|
||||
.expect("auth")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@ fn dummy_chatgpt_auth_does_not_create_cwd_auth_json_when_identity_is_set() {
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
};
|
||||
|
||||
auth.set_agent_identity(record.clone())
|
||||
@@ -257,6 +258,121 @@ fn dummy_chatgpt_auth_does_not_create_cwd_auth_json_when_identity_is_set() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_auth_detects_agent_identity_only() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let record = AgentIdentityAuthRecord {
|
||||
workspace_id: "account-123".to_string(),
|
||||
chatgpt_user_id: Some("user-123".to_string()),
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
};
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::ChatgptAuthTokens),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(record.clone()),
|
||||
};
|
||||
super::save_auth(
|
||||
codex_home.path(),
|
||||
&auth_dot_json,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("save auth file");
|
||||
let auth = super::load_auth(
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("load auth")
|
||||
.expect("auth available");
|
||||
|
||||
assert!(auth.is_agent_identity_only());
|
||||
assert_eq!(auth.agent_identity_record(), Some(record));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_auth_with_tokens_is_not_agent_identity_only() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
write_auth_file(
|
||||
AuthFileParams {
|
||||
openai_api_key: None,
|
||||
chatgpt_plan_type: Some("pro".to_string()),
|
||||
chatgpt_account_id: Some("account-123".to_string()),
|
||||
},
|
||||
codex_home.path(),
|
||||
)
|
||||
.expect("failed to write auth file");
|
||||
let auth = super::load_auth(
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("load auth")
|
||||
.expect("auth available");
|
||||
auth.set_agent_identity(AgentIdentityAuthRecord {
|
||||
workspace_id: "account-123".to_string(),
|
||||
chatgpt_user_id: Some("user-123".to_string()),
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
})
|
||||
.expect("set agent identity");
|
||||
|
||||
assert!(!auth.is_agent_identity_only());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
fn load_auth_reads_agent_identity_from_env() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let expected_record = agent_identity_record("account-123");
|
||||
let agent_identity = fake_agent_identity_jwt(&expected_record).expect("fake agent identity");
|
||||
let _agent_guard = EnvVarGuard::set(CODEX_AGENT_IDENTITY_ENV_VAR, &agent_identity);
|
||||
let _api_key_guard = EnvVarGuard::remove(CODEX_API_KEY_ENV_VAR);
|
||||
|
||||
let auth = super::load_auth(
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("load auth")
|
||||
.expect("auth available");
|
||||
|
||||
assert!(auth.is_agent_identity_only());
|
||||
assert_eq!(auth.agent_identity_record(), Some(expected_record));
|
||||
assert!(
|
||||
!get_auth_file(codex_home.path()).exists(),
|
||||
"env auth should not write auth.json"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
fn load_auth_keeps_codex_api_key_env_precedence() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let record = agent_identity_record("account-123");
|
||||
let agent_identity = fake_agent_identity_jwt(&record).expect("fake agent identity");
|
||||
let _agent_guard = EnvVarGuard::set(CODEX_AGENT_IDENTITY_ENV_VAR, &agent_identity);
|
||||
let _api_key_guard = EnvVarGuard::set(CODEX_API_KEY_ENV_VAR, "sk-env");
|
||||
|
||||
let auth = super::load_auth(
|
||||
codex_home.path(),
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("load auth")
|
||||
.expect("auth available");
|
||||
|
||||
assert_eq!(auth.auth_mode(), AuthMode::ApiKey);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_recovery_reports_mode_and_step_names() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -670,6 +786,42 @@ fn fake_jwt_for_auth_file_params(params: &AuthFileParams) -> std::io::Result<Str
|
||||
Ok(format!("{header_b64}.{payload_b64}.{signature_b64}"))
|
||||
}
|
||||
|
||||
fn agent_identity_record(workspace_id: &str) -> AgentIdentityAuthRecord {
|
||||
AgentIdentityAuthRecord {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
chatgpt_user_id: Some("user-123".to_string()),
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fake_agent_identity_jwt(record: &AgentIdentityAuthRecord) -> std::io::Result<String> {
|
||||
#[derive(Serialize)]
|
||||
struct Header {
|
||||
alg: &'static str,
|
||||
typ: &'static str,
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
};
|
||||
let payload = serde_json::json!({
|
||||
"workspace_id": record.workspace_id.clone(),
|
||||
"chatgpt_user_id": record.chatgpt_user_id.clone(),
|
||||
"agent_runtime_id": record.agent_runtime_id.clone(),
|
||||
"agent_private_key": record.agent_private_key.clone(),
|
||||
"registered_at": record.registered_at.clone(),
|
||||
});
|
||||
let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b);
|
||||
let header_b64 = b64(&serde_json::to_vec(&header)?);
|
||||
let payload_b64 = b64(&serde_json::to_vec(&payload)?);
|
||||
let signature_b64 = b64(b"sig");
|
||||
Ok(format!("{header_b64}.{payload_b64}.{signature_b64}"))
|
||||
}
|
||||
|
||||
async fn build_config(
|
||||
codex_home: &Path,
|
||||
forced_login_method: Option<ForcedLoginMethod>,
|
||||
@@ -683,6 +835,30 @@ async fn build_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn write_agent_identity_only_auth_file(
|
||||
codex_home: &Path,
|
||||
workspace_id: &str,
|
||||
) -> std::io::Result<()> {
|
||||
save_auth(
|
||||
codex_home,
|
||||
&AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(AgentIdentityAuthRecord {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
chatgpt_user_id: None,
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "private-key".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
}),
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
}
|
||||
|
||||
/// Use sparingly.
|
||||
/// TODO (gpeal): replace this with an injectable env var provider.
|
||||
#[cfg(test)]
|
||||
@@ -700,6 +876,14 @@ impl EnvVarGuard {
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
|
||||
fn remove(key: &'static str) -> Self {
|
||||
let original = env::var_os(key);
|
||||
unsafe {
|
||||
env::remove_var(key);
|
||||
}
|
||||
Self { key, original }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -715,6 +899,8 @@ impl Drop for EnvVarGuard {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_logs_out_for_method_mismatch() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
login_with_api_key(codex_home.path(), "sk-test", AuthCredentialsStoreMode::File)
|
||||
@@ -737,6 +923,7 @@ async fn enforce_login_restrictions_logs_out_for_method_mismatch() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_logs_out_for_workspace_mismatch() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
@@ -767,6 +954,7 @@ async fn enforce_login_restrictions_logs_out_for_workspace_mismatch() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_allows_matching_workspace() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
@@ -795,6 +983,97 @@ async fn enforce_login_restrictions_allows_matching_workspace() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_allows_matching_agent_identity_only_workspace() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
write_agent_identity_only_auth_file(codex_home.path(), "org_mine")
|
||||
.expect("failed to write auth file");
|
||||
|
||||
let config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
Some("org_mine".to_string()),
|
||||
)
|
||||
.await;
|
||||
|
||||
super::enforce_login_restrictions(&config).expect("matching workspace should succeed");
|
||||
assert!(
|
||||
codex_home.path().join("auth.json").exists(),
|
||||
"auth.json should remain when restrictions pass"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_logs_out_for_agent_identity_only_workspace_mismatch() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
write_agent_identity_only_auth_file(codex_home.path(), "org_another_org")
|
||||
.expect("failed to write auth file");
|
||||
|
||||
let config = build_config(
|
||||
codex_home.path(),
|
||||
/*forced_login_method*/ None,
|
||||
Some("org_mine".to_string()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let err = super::enforce_login_restrictions(&config)
|
||||
.expect_err("expected workspace mismatch to error");
|
||||
assert!(err.to_string().contains("workspace org_mine"));
|
||||
assert!(
|
||||
!codex_home.path().join("auth.json").exists(),
|
||||
"auth.json should be removed on mismatch"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_allows_matching_agent_identity_workspace_from_env() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let record = agent_identity_record("org_mine");
|
||||
let agent_identity = fake_agent_identity_jwt(&record).expect("fake agent identity");
|
||||
let _agent_guard = EnvVarGuard::set(CODEX_AGENT_IDENTITY_ENV_VAR, &agent_identity);
|
||||
let _api_key_guard = EnvVarGuard::remove(CODEX_API_KEY_ENV_VAR);
|
||||
|
||||
let config = build_config(
|
||||
codex_home.path(),
|
||||
Some(ForcedLoginMethod::Chatgpt),
|
||||
Some("org_mine".to_string()),
|
||||
)
|
||||
.await;
|
||||
|
||||
super::enforce_login_restrictions(&config).expect("matching workspace should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_blocks_agent_identity_workspace_mismatch_from_env() {
|
||||
let codex_home = tempdir().unwrap();
|
||||
let record = agent_identity_record("org_another_org");
|
||||
let agent_identity = fake_agent_identity_jwt(&record).expect("fake agent identity");
|
||||
let _agent_guard = EnvVarGuard::set(CODEX_AGENT_IDENTITY_ENV_VAR, &agent_identity);
|
||||
let _api_key_guard = EnvVarGuard::remove(CODEX_API_KEY_ENV_VAR);
|
||||
|
||||
let config = build_config(
|
||||
codex_home.path(),
|
||||
Some(ForcedLoginMethod::Chatgpt),
|
||||
Some("org_mine".to_string()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let err =
|
||||
super::enforce_login_restrictions(&config).expect_err("workspace mismatch should fail");
|
||||
|
||||
assert!(err.to_string().contains("workspace org_mine"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_forced_chatgpt_workspace_id_is_set()
|
||||
{
|
||||
let codex_home = tempdir().unwrap();
|
||||
@@ -816,6 +1095,7 @@ async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_f
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_agent_identity)]
|
||||
#[serial(codex_api_key)]
|
||||
async fn enforce_login_restrictions_blocks_env_api_key_when_chatgpt_required() {
|
||||
let _guard = EnvVarGuard::set(CODEX_API_KEY_ENV_VAR, "sk-env");
|
||||
|
||||
@@ -241,6 +241,21 @@ impl CodexAuth {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_agent_identity_jwt(codex_home: &Path, jwt: &str) -> std::io::Result<Self> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::ChatgptAuthTokens),
|
||||
openai_api_key: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
agent_identity: Some(AgentIdentityAuthRecord::from_agent_identity_jwt(jwt)?),
|
||||
};
|
||||
Self::from_auth_dot_json(
|
||||
codex_home,
|
||||
auth_dot_json,
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn auth_mode(&self) -> AuthMode {
|
||||
match self {
|
||||
Self::ApiKey(_) => AuthMode::ApiKey,
|
||||
@@ -311,6 +326,17 @@ impl CodexAuth {
|
||||
.is_some_and(|t| t.id_token.is_fedramp_account())
|
||||
}
|
||||
|
||||
pub fn is_agent_identity_only(&self) -> bool {
|
||||
self.get_current_auth_json().is_some_and(|auth| {
|
||||
auth.tokens.is_none() && auth.agent_identity.is_some() && self.is_chatgpt_auth()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn agent_identity_record(&self) -> Option<AgentIdentityAuthRecord> {
|
||||
self.get_current_auth_json()
|
||||
.and_then(|auth| auth.agent_identity)
|
||||
}
|
||||
|
||||
/// Returns `None` if `is_chatgpt_auth()` is false.
|
||||
pub fn get_account_email(&self) -> Option<String> {
|
||||
self.get_current_token_data().and_then(|t| t.id_token.email)
|
||||
@@ -481,6 +507,7 @@ impl ChatgptAuth {
|
||||
|
||||
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
||||
pub const CODEX_API_KEY_ENV_VAR: &str = "CODEX_API_KEY";
|
||||
pub const CODEX_AGENT_IDENTITY_ENV_VAR: &str = "CODEX_AGENT_IDENTITY";
|
||||
|
||||
pub fn read_openai_api_key_from_env() -> Option<String> {
|
||||
env::var(OPENAI_API_KEY_ENV_VAR)
|
||||
@@ -496,6 +523,13 @@ pub fn read_codex_api_key_from_env() -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub fn read_codex_agent_identity_from_env() -> Option<String> {
|
||||
env::var(CODEX_AGENT_IDENTITY_ENV_VAR)
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)`
|
||||
/// if a file was removed, `Ok(false)` if no auth file was present.
|
||||
pub fn logout(
|
||||
@@ -625,6 +659,23 @@ pub fn enforce_login_restrictions(config: &AuthConfig) -> std::io::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if auth.is_agent_identity_only() {
|
||||
let actual_account_id = auth
|
||||
.agent_identity_record()
|
||||
.map(|identity| identity.workspace_id);
|
||||
if actual_account_id.as_deref() != Some(expected_account_id) {
|
||||
return logout_with_message(
|
||||
&config.codex_home,
|
||||
format!(
|
||||
"Login is restricted to workspace {expected_account_id}, but current agent identity belongs to {actual_account_id:?}. Logging out."
|
||||
),
|
||||
config.auth_credentials_store_mode,
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let token_data = match auth.get_token_data() {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
@@ -701,6 +752,10 @@ fn load_auth(
|
||||
return Ok(Some(CodexAuth::from_api_key(api_key.as_str())));
|
||||
}
|
||||
|
||||
if enable_codex_api_key_env && let Some(agent_identity) = read_codex_agent_identity_from_env() {
|
||||
return CodexAuth::from_agent_identity_jwt(codex_home, &agent_identity).map(Some);
|
||||
}
|
||||
|
||||
// External ChatGPT auth tokens live in the in-memory (ephemeral) store. Always check this
|
||||
// first so external auth takes precedence over any persisted credentials.
|
||||
let ephemeral_storage = create_auth_storage(
|
||||
@@ -1547,6 +1602,10 @@ impl AuthManager {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if auth.is_agent_identity_only() {
|
||||
return Ok(auth.agent_identity_record());
|
||||
}
|
||||
|
||||
let token_data = auth
|
||||
.get_token_data()
|
||||
.context("ChatGPT token data is not available")?;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -19,6 +20,7 @@ use std::sync::Mutex;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::token_data::TokenData;
|
||||
use crate::token_data::decode_jwt_payload;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_keyring_store::DefaultKeyringStore;
|
||||
@@ -40,7 +42,11 @@ pub struct AuthDotJson {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_refresh: Option<DateTime<Utc>>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
deserialize_with = "deserialize_agent_identity"
|
||||
)]
|
||||
pub agent_identity: Option<AgentIdentityAuthRecord>,
|
||||
}
|
||||
|
||||
@@ -56,6 +62,68 @@ pub struct AgentIdentityAuthRecord {
|
||||
pub background_task_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum AgentIdentityAuthInput {
|
||||
Record(AgentIdentityAuthRecord),
|
||||
Jwt(String),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AgentIdentityJwtClaims {
|
||||
workspace_id: String,
|
||||
#[serde(default)]
|
||||
chatgpt_user_id: Option<String>,
|
||||
agent_runtime_id: String,
|
||||
agent_private_key: String,
|
||||
#[serde(default)]
|
||||
registered_at: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<AgentIdentityAuthInput> for AgentIdentityAuthRecord {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(input: AgentIdentityAuthInput) -> std::io::Result<Self> {
|
||||
match input {
|
||||
AgentIdentityAuthInput::Record(record) => Ok(record),
|
||||
AgentIdentityAuthInput::Jwt(jwt) => {
|
||||
AgentIdentityAuthRecord::from_agent_identity_jwt(&jwt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentIdentityAuthRecord {
|
||||
pub(crate) fn from_agent_identity_jwt(jwt: &str) -> std::io::Result<Self> {
|
||||
let claims: AgentIdentityJwtClaims =
|
||||
decode_jwt_payload(jwt).map_err(std::io::Error::other)?;
|
||||
|
||||
Ok(Self {
|
||||
workspace_id: claims.workspace_id,
|
||||
chatgpt_user_id: claims.chatgpt_user_id,
|
||||
agent_runtime_id: claims.agent_runtime_id,
|
||||
agent_private_key: claims.agent_private_key,
|
||||
registered_at: claims
|
||||
.registered_at
|
||||
.unwrap_or_else(|| Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)),
|
||||
background_task_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_agent_identity<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<AgentIdentityAuthRecord>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let input = Option::<AgentIdentityAuthInput>::deserialize(deserializer)?;
|
||||
input
|
||||
.map(AgentIdentityAuthRecord::try_from)
|
||||
.transpose()
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub(super) fn get_auth_file(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join("auth.json")
|
||||
}
|
||||
|
||||
@@ -79,6 +79,64 @@ async fn file_storage_persists_agent_identity() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_storage_loads_agent_identity_from_jwt() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
|
||||
let expected_record = AgentIdentityAuthRecord {
|
||||
workspace_id: "account-123".to_string(),
|
||||
chatgpt_user_id: None,
|
||||
agent_runtime_id: "agent_123".to_string(),
|
||||
agent_private_key: "pkcs8-base64".to_string(),
|
||||
registered_at: "2026-04-13T12:00:00Z".to_string(),
|
||||
background_task_id: None,
|
||||
};
|
||||
let agent_identity_jwt = jwt_with_payload(json!({
|
||||
"workspace_id": expected_record.workspace_id,
|
||||
"agent_runtime_id": expected_record.agent_runtime_id,
|
||||
"agent_private_key": expected_record.agent_private_key,
|
||||
"registered_at": expected_record.registered_at,
|
||||
}));
|
||||
let auth_file = get_auth_file(codex_home.path());
|
||||
std::fs::write(
|
||||
&auth_file,
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"auth_mode": "chatgpt",
|
||||
"agent_identity": agent_identity_jwt,
|
||||
}))?,
|
||||
)?;
|
||||
|
||||
let loaded = storage.load()?;
|
||||
|
||||
assert_eq!(
|
||||
loaded.expect("auth should load").agent_identity,
|
||||
Some(expected_record)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_storage_rejects_invalid_agent_identity_jwt() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
let storage = FileAuthStorage::new(codex_home.path().to_path_buf());
|
||||
let auth_file = get_auth_file(codex_home.path());
|
||||
std::fs::write(
|
||||
&auth_file,
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"auth_mode": "chatgpt",
|
||||
"agent_identity": "not-a-jwt",
|
||||
}))?,
|
||||
)?;
|
||||
|
||||
let err = storage.load().expect_err("invalid JWT should fail load");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains("invalid ID token format"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {
|
||||
let dir = tempdir()?;
|
||||
@@ -199,6 +257,24 @@ fn id_token_with_prefix(prefix: &str) -> IdTokenInfo {
|
||||
crate::token_data::parse_chatgpt_jwt_claims(&fake_jwt).expect("fake JWT should parse")
|
||||
}
|
||||
|
||||
fn jwt_with_payload(payload: serde_json::Value) -> String {
|
||||
#[derive(Serialize)]
|
||||
struct Header {
|
||||
alg: &'static str,
|
||||
typ: &'static str,
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
alg: "none",
|
||||
typ: "JWT",
|
||||
};
|
||||
let encode = |bytes: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
|
||||
let header_b64 = encode(&serde_json::to_vec(&header).expect("serialize header"));
|
||||
let payload_b64 = encode(&serde_json::to_vec(&payload).expect("serialize payload"));
|
||||
let signature_b64 = encode(b"sig");
|
||||
format!("{header_b64}.{payload_b64}.{signature_b64}")
|
||||
}
|
||||
|
||||
fn auth_with_prefix(prefix: &str) -> AuthDotJson {
|
||||
AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
|
||||
@@ -27,6 +27,7 @@ pub use auth::AuthDotJson;
|
||||
pub use auth::AuthManager;
|
||||
pub use auth::AuthManagerConfig;
|
||||
pub use auth::CLIENT_ID;
|
||||
pub use auth::CODEX_AGENT_IDENTITY_ENV_VAR;
|
||||
pub use auth::CODEX_API_KEY_ENV_VAR;
|
||||
pub use auth::CodexAuth;
|
||||
pub use auth::ExternalAuth;
|
||||
@@ -45,6 +46,7 @@ pub use auth::load_auth_dot_json;
|
||||
pub use auth::login_with_api_key;
|
||||
pub use auth::logout;
|
||||
pub use auth::logout_with_revoke;
|
||||
pub use auth::read_codex_agent_identity_from_env;
|
||||
pub use auth::read_openai_api_key_from_env;
|
||||
pub use auth::save_auth;
|
||||
pub use auth_env_telemetry::AuthEnvTelemetry;
|
||||
|
||||
@@ -114,7 +114,7 @@ pub enum IdTokenInfoError {
|
||||
Json(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
fn decode_jwt_payload<T: DeserializeOwned>(jwt: &str) -> Result<T, IdTokenInfoError> {
|
||||
pub(crate) fn decode_jwt_payload<T: DeserializeOwned>(jwt: &str) -> Result<T, IdTokenInfoError> {
|
||||
// JWT format: header.payload.signature
|
||||
let mut parts = jwt.split('.');
|
||||
let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) {
|
||||
|
||||
@@ -41,6 +41,16 @@ fn bearer_auth_provider_from_auth(
|
||||
}
|
||||
|
||||
if let Some(auth) = auth {
|
||||
if auth.is_agent_identity_only() {
|
||||
return Ok(BearerAuthProvider {
|
||||
token: None,
|
||||
account_id: auth
|
||||
.agent_identity_record()
|
||||
.map(|record| record.workspace_id),
|
||||
is_fedramp_account: false,
|
||||
});
|
||||
}
|
||||
|
||||
let token = auth.get_token()?;
|
||||
Ok(BearerAuthProvider {
|
||||
token: Some(token),
|
||||
|
||||
Reference in New Issue
Block a user