mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
## What changed - Add the experimental `amazonBedrockAccessKeys` app-server login flow, persist its credentials in the configured auth store, and use them for SigV4-signed Bedrock requests. - Report `bedrockAccessKeys` as a distinct auth mode and document access-key login alongside AWS profile and environment credential setup. - Clear the selected Bedrock provider, AWS settings, and Bedrock-specific model on logout while leaving external AWS credentials and `.env` untouched. - Reject managed access-key login when higher-precedence configuration would prevent its region or profile settings from taking effect. ## Testing - Cover access-key login, storage, request signing, configuration conflicts, status reporting, and logout cleanup. GitOrigin-RevId: bbf297887c6503d3651ceb2aad85c4d626e32723
232 lines
7.6 KiB
Rust
232 lines
7.6 KiB
Rust
#![cfg(test)]
|
|
|
|
use std::path::Path;
|
|
|
|
use codex_config::types::AuthCredentialsStoreMode;
|
|
use codex_login::AuthKeyringBackendKind;
|
|
use codex_login::load_auth_dot_json;
|
|
use codex_protocol::auth::AuthMode;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct LocalChatgptAuth {
|
|
pub(crate) access_token: String,
|
|
pub(crate) chatgpt_account_id: String,
|
|
pub(crate) chatgpt_plan_type: Option<String>,
|
|
}
|
|
|
|
pub(crate) fn load_local_chatgpt_auth(
|
|
codex_home: &Path,
|
|
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
|
forced_chatgpt_workspace_id: Option<&[String]>,
|
|
) -> Result<LocalChatgptAuth, String> {
|
|
let auth = load_auth_dot_json(
|
|
codex_home,
|
|
auth_credentials_store_mode,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.map_err(|err| format!("failed to load local auth: {err}"))?
|
|
.ok_or_else(|| "no local auth available".to_string())?;
|
|
if matches!(auth.auth_mode, Some(AuthMode::ApiKey)) || auth.openai_api_key.is_some() {
|
|
return Err("local auth is not a ChatGPT login".to_string());
|
|
}
|
|
|
|
let tokens = auth
|
|
.tokens
|
|
.ok_or_else(|| "local ChatGPT auth is missing token data".to_string())?;
|
|
let access_token = tokens.access_token;
|
|
let chatgpt_account_id = tokens
|
|
.account_id
|
|
.or(tokens.id_token.chatgpt_account_id.clone())
|
|
.ok_or_else(|| "local ChatGPT auth is missing chatgpt account id".to_string())?;
|
|
if let Some(expected_workspaces) = forced_chatgpt_workspace_id
|
|
&& !expected_workspaces.contains(&chatgpt_account_id)
|
|
{
|
|
return Err(format!(
|
|
"local ChatGPT auth must use one of workspace(s) {expected_workspaces:?}, but found {chatgpt_account_id:?}",
|
|
));
|
|
}
|
|
|
|
let chatgpt_plan_type = tokens
|
|
.id_token
|
|
.get_chatgpt_plan_type_raw()
|
|
.map(|plan_type| plan_type.to_ascii_lowercase());
|
|
|
|
Ok(LocalChatgptAuth {
|
|
access_token,
|
|
chatgpt_account_id,
|
|
chatgpt_plan_type,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use base64::Engine;
|
|
use chrono::Utc;
|
|
use codex_login::AuthDotJson;
|
|
use codex_login::auth::login_with_chatgpt_auth_tokens;
|
|
use codex_login::save_auth;
|
|
use codex_login::token_data::TokenData;
|
|
use codex_protocol::auth::AuthMode;
|
|
use pretty_assertions::assert_eq;
|
|
use serde::Serialize;
|
|
use serde_json::json;
|
|
use tempfile::TempDir;
|
|
|
|
fn fake_jwt(email: &str, account_id: &str, plan_type: &str) -> String {
|
|
#[derive(Serialize)]
|
|
struct Header {
|
|
alg: &'static str,
|
|
typ: &'static str,
|
|
}
|
|
|
|
let header = Header {
|
|
alg: "none",
|
|
typ: "JWT",
|
|
};
|
|
let payload = json!({
|
|
"email": email,
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_account_id": account_id,
|
|
"chatgpt_plan_type": plan_type,
|
|
},
|
|
});
|
|
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 write_chatgpt_auth(codex_home: &Path, plan_type: &str) {
|
|
let id_token = fake_jwt("user@example.com", "workspace-1", plan_type);
|
|
let access_token = fake_jwt("user@example.com", "workspace-1", plan_type);
|
|
let auth = AuthDotJson {
|
|
auth_mode: Some(AuthMode::Chatgpt),
|
|
openai_api_key: None,
|
|
tokens: Some(TokenData {
|
|
id_token: codex_login::token_data::parse_chatgpt_jwt_claims(&id_token)
|
|
.expect("id token should parse"),
|
|
access_token,
|
|
refresh_token: "refresh-token".to_string(),
|
|
account_id: Some("workspace-1".to_string()),
|
|
}),
|
|
last_refresh: Some(Utc::now()),
|
|
agent_identity: None,
|
|
personal_access_token: None,
|
|
bedrock_api_key: None,
|
|
bedrock_access_keys: None,
|
|
};
|
|
save_auth(
|
|
codex_home,
|
|
&auth,
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.expect("chatgpt auth should save");
|
|
}
|
|
|
|
#[test]
|
|
fn loads_local_chatgpt_auth_from_managed_auth() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
write_chatgpt_auth(codex_home.path(), "business");
|
|
|
|
let auth = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
Some(&["workspace-1".to_string()]),
|
|
)
|
|
.expect("chatgpt auth should load");
|
|
|
|
assert_eq!(auth.chatgpt_account_id, "workspace-1");
|
|
assert_eq!(auth.chatgpt_plan_type.as_deref(), Some("business"));
|
|
assert!(!auth.access_token.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_local_auth() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
|
|
let err = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
)
|
|
.expect_err("missing auth should fail");
|
|
|
|
assert_eq!(err, "no local auth available");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_api_key_auth() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
save_auth(
|
|
codex_home.path(),
|
|
&AuthDotJson {
|
|
auth_mode: Some(AuthMode::ApiKey),
|
|
openai_api_key: Some("sk-test".to_string()),
|
|
tokens: None,
|
|
last_refresh: None,
|
|
agent_identity: None,
|
|
personal_access_token: None,
|
|
bedrock_api_key: None,
|
|
bedrock_access_keys: None,
|
|
},
|
|
AuthCredentialsStoreMode::File,
|
|
AuthKeyringBackendKind::default(),
|
|
)
|
|
.expect("api key auth should save");
|
|
|
|
let err = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
/*forced_chatgpt_workspace_id*/ None,
|
|
)
|
|
.expect_err("api key auth should fail");
|
|
|
|
assert_eq!(err, "local auth is not a ChatGPT login");
|
|
}
|
|
|
|
#[test]
|
|
fn prefers_managed_auth_over_external_ephemeral_tokens() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
write_chatgpt_auth(codex_home.path(), "business");
|
|
login_with_chatgpt_auth_tokens(
|
|
codex_home.path(),
|
|
&fake_jwt("user@example.com", "workspace-2", "enterprise"),
|
|
"workspace-2",
|
|
Some("enterprise"),
|
|
)
|
|
.expect("external auth should save");
|
|
|
|
let auth = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
Some(&["workspace-1".to_string(), "workspace-2".to_string()]),
|
|
)
|
|
.expect("managed auth should win");
|
|
|
|
assert_eq!(auth.chatgpt_account_id, "workspace-1");
|
|
assert_eq!(auth.chatgpt_plan_type.as_deref(), Some("business"));
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_usage_based_plan_type_wire_name() {
|
|
let codex_home = TempDir::new().expect("tempdir");
|
|
write_chatgpt_auth(codex_home.path(), "self_serve_business_usage_based");
|
|
|
|
let auth = load_local_chatgpt_auth(
|
|
codex_home.path(),
|
|
AuthCredentialsStoreMode::File,
|
|
Some(&["workspace-1".to_string()]),
|
|
)
|
|
.expect("chatgpt auth should load");
|
|
|
|
assert_eq!(
|
|
auth.chatgpt_plan_type.as_deref(),
|
|
Some("self_serve_business_usage_based")
|
|
);
|
|
}
|
|
}
|