mirror of
https://github.com/openai/codex.git
synced 2026-04-19 12:14:48 +00:00
Compare commits
5 Commits
xl/plugins
...
jackz/fed-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
029bd9ba19 | ||
|
|
c6740924f4 | ||
|
|
14d5a71418 | ||
|
|
0a1d8c4100 | ||
|
|
8069205b84 |
@@ -161,6 +161,7 @@ pub fn write_chatgpt_auth(
|
||||
let auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(tokens),
|
||||
last_refresh,
|
||||
};
|
||||
|
||||
@@ -115,6 +115,7 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
|
||||
&AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("test-api-key".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
},
|
||||
|
||||
@@ -12,8 +12,11 @@ use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::Config;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::OPENAI_GOV_API_BASE_URL;
|
||||
use codex_login::OpenAiApiKeyLoginCheck;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::check_openai_api_key_for_login;
|
||||
use codex_login::login_with_api_key_and_fedramp_status;
|
||||
use codex_login::logout;
|
||||
use codex_login::run_device_code_login;
|
||||
use codex_login::run_login_server;
|
||||
@@ -35,6 +38,21 @@ const CHATGPT_LOGIN_DISABLED_MESSAGE: &str =
|
||||
const API_KEY_LOGIN_DISABLED_MESSAGE: &str =
|
||||
"API key login is disabled. Use ChatGPT login instead.";
|
||||
const LOGIN_SUCCESS_MESSAGE: &str = "Successfully logged in";
|
||||
const OPENAI_MODEL_PROVIDER_ID: &str = "openai";
|
||||
|
||||
fn should_check_openai_api_key_for_login_provider(
|
||||
model_provider_id: &str,
|
||||
model_provider_base_url: Option<&str>,
|
||||
) -> bool {
|
||||
model_provider_id == OPENAI_MODEL_PROVIDER_ID && model_provider_base_url.is_none()
|
||||
}
|
||||
|
||||
fn should_check_openai_api_key_for_login(config: &Config) -> bool {
|
||||
should_check_openai_api_key_for_login_provider(
|
||||
&config.model_provider_id,
|
||||
config.model_provider.base_url.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Installs a small file-backed tracing layer for direct `codex login` flows.
|
||||
///
|
||||
@@ -171,20 +189,40 @@ pub async fn run_login_with_api_key(
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
match login_with_api_key(
|
||||
let login_check = if should_check_openai_api_key_for_login(&config) {
|
||||
match check_openai_api_key_for_login(&api_key).await {
|
||||
Ok(check) => Some(check),
|
||||
Err(e) => {
|
||||
eprintln!("Error logging in: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let openai_api_key_is_fedramp = login_check.as_ref().map(
|
||||
|OpenAiApiKeyLoginCheck {
|
||||
current_organization_is_fedramp,
|
||||
}| *current_organization_is_fedramp,
|
||||
);
|
||||
|
||||
if let Err(e) = login_with_api_key_and_fedramp_status(
|
||||
&config.codex_home,
|
||||
&api_key,
|
||||
openai_api_key_is_fedramp,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
) {
|
||||
Ok(_) => {
|
||||
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error logging in: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
eprintln!("Error logging in: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if openai_api_key_is_fedramp == Some(true) {
|
||||
eprintln!("FedRAMP API key detected. Codex will use {OPENAI_GOV_API_BASE_URL}.");
|
||||
}
|
||||
|
||||
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
pub fn read_api_key_from_stdin() -> String {
|
||||
@@ -393,6 +431,7 @@ fn safe_format_key(key: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::safe_format_key;
|
||||
use super::should_check_openai_api_key_for_login_provider;
|
||||
|
||||
#[test]
|
||||
fn formats_long_key() {
|
||||
@@ -405,4 +444,27 @@ mod tests {
|
||||
let key = "sk-proj-12345";
|
||||
assert_eq!(safe_format_key(key), "***");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_api_key_only_for_default_openai_provider() {
|
||||
assert!(should_check_openai_api_key_for_login_provider(
|
||||
"openai", None
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_api_key_check_for_custom_openai_base_url() {
|
||||
assert!(!should_check_openai_api_key_for_login_provider(
|
||||
"openai",
|
||||
Some("https://proxy.example/v1")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_api_key_check_for_custom_provider() {
|
||||
assert!(!should_check_openai_api_key_for_login_provider(
|
||||
"custom_provider",
|
||||
Some("https://provider.example/v1")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ use codex_api::response_create_client_metadata;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::OPENAI_GOV_API_BASE_URL;
|
||||
use codex_login::RefreshTokenError;
|
||||
use codex_login::UnauthorizedRecovery;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
@@ -168,6 +169,19 @@ struct CurrentClientSetup {
|
||||
api_auth: CoreAuthProvider,
|
||||
}
|
||||
|
||||
pub(crate) fn apply_fedramp_api_endpoint_if_needed(
|
||||
provider_info: &ModelProviderInfo,
|
||||
auth: Option<&CodexAuth>,
|
||||
api_provider: &mut codex_api::Provider,
|
||||
) {
|
||||
if auth.is_some_and(CodexAuth::openai_api_key_is_fedramp)
|
||||
&& provider_info.is_openai()
|
||||
&& provider_info.base_url.is_none()
|
||||
{
|
||||
api_provider.base_url = OPENAI_GOV_API_BASE_URL.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RequestRouteTelemetry {
|
||||
endpoint: &'static str,
|
||||
@@ -664,10 +678,15 @@ impl ModelClient {
|
||||
Some(manager) => manager.auth().await,
|
||||
None => None,
|
||||
};
|
||||
let api_provider = self
|
||||
let mut api_provider = self
|
||||
.state
|
||||
.provider
|
||||
.to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?;
|
||||
apply_fedramp_api_endpoint_if_needed(
|
||||
&self.state.provider,
|
||||
auth.as_ref(),
|
||||
&mut api_provider,
|
||||
);
|
||||
let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?;
|
||||
Ok(CurrentClientSetup {
|
||||
auth,
|
||||
|
||||
@@ -9,6 +9,9 @@ use super::X_CODEX_WINDOW_ID_HEADER;
|
||||
use super::X_OPENAI_SUBAGENT_HEADER;
|
||||
use codex_api::CoreAuthProvider;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::OPENAI_GOV_API_BASE_URL;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_model_provider_info::create_oss_provider_with_base_url;
|
||||
use codex_otel::SessionTelemetry;
|
||||
@@ -79,6 +82,48 @@ fn test_session_telemetry() -> SessionTelemetry {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fedramp_api_key_uses_gov_base_url_for_default_openai_provider() {
|
||||
let provider_info = ModelProviderInfo::create_openai_provider(None);
|
||||
let mut api_provider = provider_info
|
||||
.to_api_provider(Some(AuthMode::ApiKey))
|
||||
.expect("provider should convert");
|
||||
let auth = CodexAuth::from_api_key_with_fedramp_status("sk-test", true);
|
||||
|
||||
super::apply_fedramp_api_endpoint_if_needed(&provider_info, Some(&auth), &mut api_provider);
|
||||
|
||||
assert_eq!(api_provider.base_url, OPENAI_GOV_API_BASE_URL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fedramp_api_key_preserves_custom_openai_base_url() {
|
||||
let custom_base_url = "https://proxy.example/v1";
|
||||
let provider_info =
|
||||
ModelProviderInfo::create_openai_provider(Some(custom_base_url.to_string()));
|
||||
let mut api_provider = provider_info
|
||||
.to_api_provider(Some(AuthMode::ApiKey))
|
||||
.expect("provider should convert");
|
||||
let auth = CodexAuth::from_api_key_with_fedramp_status("sk-test", true);
|
||||
|
||||
super::apply_fedramp_api_endpoint_if_needed(&provider_info, Some(&auth), &mut api_provider);
|
||||
|
||||
assert_eq!(api_provider.base_url, custom_base_url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fedramp_api_key_preserves_custom_provider_base_url() {
|
||||
let custom_base_url = "https://provider.example/v1";
|
||||
let provider_info = create_oss_provider_with_base_url(custom_base_url, WireApi::Responses);
|
||||
let mut api_provider = provider_info
|
||||
.to_api_provider(Some(AuthMode::ApiKey))
|
||||
.expect("provider should convert");
|
||||
let auth = CodexAuth::from_api_key_with_fedramp_status("sk-test", true);
|
||||
|
||||
super::apply_fedramp_api_endpoint_if_needed(&provider_info, Some(&auth), &mut api_provider);
|
||||
|
||||
assert_eq!(api_provider.base_url, custom_base_url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_subagent_headers_sets_other_subagent_label() {
|
||||
let client = test_model_client(SessionSource::SubAgent(SubAgentSource::Other(
|
||||
|
||||
@@ -515,6 +515,11 @@ async fn prepare_realtime_start(
|
||||
.transport
|
||||
.unwrap_or(ConversationStartTransport::Websocket);
|
||||
let mut api_provider = provider.to_api_provider(Some(AuthMode::ApiKey))?;
|
||||
crate::client::apply_fedramp_api_endpoint_if_needed(
|
||||
&provider,
|
||||
auth.as_ref(),
|
||||
&mut api_provider,
|
||||
);
|
||||
if let Some(realtime_ws_base_url) = &config.experimental_realtime_ws_base_url {
|
||||
api_provider.base_url = realtime_ws_base_url.clone();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use tempfile::tempdir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_without_id_token() {
|
||||
@@ -54,6 +59,7 @@ fn login_with_api_key_overwrites_existing_auth_json() {
|
||||
let auth_path = dir.path().join("auth.json");
|
||||
let stale_auth = json!({
|
||||
"OPENAI_API_KEY": "sk-old",
|
||||
"OPENAI_API_KEY_IS_FEDRAMP": true,
|
||||
"tokens": {
|
||||
"id_token": "stale.header.payload",
|
||||
"access_token": "stale-access",
|
||||
@@ -75,9 +81,93 @@ fn login_with_api_key_overwrites_existing_auth_json() {
|
||||
.try_read_auth_json(&auth_path)
|
||||
.expect("auth.json should parse");
|
||||
assert_eq!(auth.openai_api_key.as_deref(), Some("sk-new"));
|
||||
assert_eq!(auth.openai_api_key_is_fedramp, None);
|
||||
assert!(auth.tokens.is_none(), "tokens should be cleared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_with_api_key_and_fedramp_status_persists_status_with_key() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
super::login_with_api_key_and_fedramp_status(
|
||||
dir.path(),
|
||||
"sk-fed",
|
||||
Some(true),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("login_with_api_key_and_fedramp_status should succeed");
|
||||
|
||||
let storage = FileAuthStorage::new(dir.path().to_path_buf());
|
||||
let auth_file = dir.path().join("auth.json");
|
||||
let auth_dot_json = storage
|
||||
.try_read_auth_json(&auth_file)
|
||||
.expect("auth.json should parse");
|
||||
assert_eq!(auth_dot_json.openai_api_key.as_deref(), Some("sk-fed"));
|
||||
assert_eq!(auth_dot_json.openai_api_key_is_fedramp, Some(true));
|
||||
|
||||
let auth = super::load_auth(
|
||||
dir.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(auth.openai_api_key_is_fedramp());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_key_login_check_reads_fedramp_status_from_current_org() {
|
||||
let server = MockServer::start().await;
|
||||
mount_me_response(
|
||||
&server, /*status*/ 200, /*current_organization_is_fedramp*/ true,
|
||||
)
|
||||
.await;
|
||||
|
||||
let result =
|
||||
check_openai_api_key_for_login_with_base_url("sk-test", &format!("{}/v1", server.uri()))
|
||||
.await
|
||||
.expect("auth check should succeed");
|
||||
|
||||
assert!(result.current_organization_is_fedramp);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_key_login_check_ignores_fedramp_membership_if_current_org_is_commercial() {
|
||||
let server = MockServer::start().await;
|
||||
let response = ResponseTemplate::new(200).set_body_json(json!({
|
||||
"current_organization_is_fedramp": false,
|
||||
"orgs": {
|
||||
"data": [
|
||||
{
|
||||
"id": "org-fed-membership",
|
||||
"is_fedramp": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
mount_me_response_template(&server, response).await;
|
||||
|
||||
let result =
|
||||
check_openai_api_key_for_login_with_base_url("sk-test", &format!("{}/v1", server.uri()))
|
||||
.await
|
||||
.expect("auth check should succeed");
|
||||
|
||||
assert!(!result.current_organization_is_fedramp);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_key_login_check_rejects_invalid_api_key() {
|
||||
let server = MockServer::start().await;
|
||||
mount_me_response_template(&server, ResponseTemplate::new(401)).await;
|
||||
|
||||
let error =
|
||||
check_openai_api_key_for_login_with_base_url("sk-test", &format!("{}/v1", server.uri()))
|
||||
.await
|
||||
.expect_err("auth check should fail");
|
||||
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_auth_json_returns_none() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -86,6 +176,25 @@ fn missing_auth_json_returns_none() {
|
||||
assert_eq!(auth, None);
|
||||
}
|
||||
|
||||
async fn mount_me_response(
|
||||
server: &MockServer,
|
||||
status: u16,
|
||||
current_organization_is_fedramp: bool,
|
||||
) {
|
||||
let response = ResponseTemplate::new(status).set_body_json(json!({
|
||||
"current_organization_is_fedramp": current_organization_is_fedramp,
|
||||
}));
|
||||
mount_me_response_template(server, response).await;
|
||||
}
|
||||
|
||||
async fn mount_me_response_template(server: &MockServer, response: ResponseTemplate) {
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/v1/me"))
|
||||
.respond_with(response)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_api_key)]
|
||||
async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
@@ -122,6 +231,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
AuthDotJson {
|
||||
auth_mode: None,
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(TokenData {
|
||||
id_token: IdTokenInfo {
|
||||
email: Some("user@example.com".to_string()),
|
||||
@@ -160,16 +270,41 @@ async fn loads_api_key_from_auth_json() {
|
||||
.unwrap();
|
||||
assert_eq!(auth.auth_mode(), AuthMode::ApiKey);
|
||||
assert_eq!(auth.api_key(), Some("sk-test-key"));
|
||||
assert!(!auth.openai_api_key_is_fedramp());
|
||||
|
||||
assert!(auth.get_token_data().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial(codex_api_key)]
|
||||
async fn loads_api_key_fedramp_status_from_auth_json() {
|
||||
let dir = tempdir().unwrap();
|
||||
let auth_file = dir.path().join("auth.json");
|
||||
std::fs::write(
|
||||
auth_file,
|
||||
r#"{"OPENAI_API_KEY":"sk-test-key","OPENAI_API_KEY_IS_FEDRAMP":true}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let auth = super::load_auth(
|
||||
dir.path(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(auth.auth_mode(), AuthMode::ApiKey);
|
||||
assert_eq!(auth.api_key(), Some("sk-test-key"));
|
||||
assert!(auth.openai_api_key_is_fedramp());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logout_removes_auth_file() -> Result<(), std::io::Error> {
|
||||
let dir = tempdir()?;
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::ApiKey),
|
||||
openai_api_key: Some("sk-test-key".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use crate::auth::storage::AuthDotJson;
|
||||
use crate::auth::storage::AuthStorageBackend;
|
||||
use crate::auth::storage::create_auth_storage;
|
||||
use crate::auth::util::try_parse_error_message;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
use crate::default_client::create_client;
|
||||
use crate::token_data::TokenData;
|
||||
use crate::token_data::parse_chatgpt_jwt_claims;
|
||||
@@ -49,6 +50,7 @@ pub enum CodexAuth {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApiKeyAuth {
|
||||
api_key: String,
|
||||
openai_api_key_is_fedramp: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -84,6 +86,19 @@ const REFRESH_TOKEN_UNKNOWN_MESSAGE: &str =
|
||||
const REFRESH_TOKEN_ACCOUNT_MISMATCH_MESSAGE: &str = "Your access token could not be refreshed because you have since logged out or signed in to another account. Please sign in again.";
|
||||
const REFRESH_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
|
||||
pub const REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR: &str = "CODEX_REFRESH_TOKEN_URL_OVERRIDE";
|
||||
pub const OPENAI_API_BASE_URL: &str = "https://api.openai.com/v1";
|
||||
pub const OPENAI_GOV_API_BASE_URL: &str = "https://gov.api.openai.com/v1";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OpenAiApiKeyLoginCheck {
|
||||
pub current_organization_is_fedramp: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MeAuthCheckResponse {
|
||||
#[serde(default)]
|
||||
current_organization_is_fedramp: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RefreshTokenError {
|
||||
@@ -195,7 +210,10 @@ impl CodexAuth {
|
||||
let Some(api_key) = auth_dot_json.openai_api_key.as_deref() else {
|
||||
return Err(std::io::Error::other("API key auth is missing a key."));
|
||||
};
|
||||
return Ok(Self::from_api_key(api_key));
|
||||
return Ok(Self::from_api_key_with_fedramp_status(
|
||||
api_key,
|
||||
auth_dot_json.openai_api_key_is_fedramp.unwrap_or(false),
|
||||
));
|
||||
}
|
||||
|
||||
let storage_mode = auth_dot_json.storage_mode(auth_credentials_store_mode);
|
||||
@@ -356,6 +374,7 @@ impl CodexAuth {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(TokenData {
|
||||
id_token: Default::default(),
|
||||
access_token: "Access Token".to_string(),
|
||||
@@ -375,10 +394,25 @@ impl CodexAuth {
|
||||
}
|
||||
|
||||
pub fn from_api_key(api_key: &str) -> Self {
|
||||
Self::from_api_key_with_fedramp_status(api_key, /*openai_api_key_is_fedramp*/ false)
|
||||
}
|
||||
|
||||
pub fn from_api_key_with_fedramp_status(
|
||||
api_key: &str,
|
||||
openai_api_key_is_fedramp: bool,
|
||||
) -> Self {
|
||||
Self::ApiKey(ApiKeyAuth {
|
||||
api_key: api_key.to_owned(),
|
||||
openai_api_key_is_fedramp,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn openai_api_key_is_fedramp(&self) -> bool {
|
||||
match self {
|
||||
Self::ApiKey(auth) => auth.openai_api_key_is_fedramp,
|
||||
Self::Chatgpt(_) | Self::ChatgptAuthTokens(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatgptAuth {
|
||||
@@ -417,6 +451,52 @@ pub fn read_codex_api_key_from_env() -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub async fn check_openai_api_key_for_login(
|
||||
api_key: &str,
|
||||
) -> std::io::Result<OpenAiApiKeyLoginCheck> {
|
||||
check_openai_api_key_for_login_with_base_url(api_key, OPENAI_API_BASE_URL).await
|
||||
}
|
||||
|
||||
pub async fn check_openai_api_key_for_login_with_base_url(
|
||||
api_key: &str,
|
||||
api_base_url: &str,
|
||||
) -> std::io::Result<OpenAiApiKeyLoginCheck> {
|
||||
let client = build_reqwest_client();
|
||||
let url = format!("{}/me", api_base_url.trim_end_matches('/'));
|
||||
let response = client
|
||||
.get(url)
|
||||
.bearer_auth(api_key)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| std::io::Error::other(format!("/v1/me auth check failed: {err}")))?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"API key was rejected by /v1/me.",
|
||||
));
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"/v1/me auth check failed with {status}"
|
||||
)));
|
||||
}
|
||||
|
||||
let payload = response
|
||||
.json::<MeAuthCheckResponse>()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"/v1/me auth check returned an invalid response: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(OpenAiApiKeyLoginCheck {
|
||||
current_organization_is_fedramp: payload.current_organization_is_fedramp,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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(
|
||||
@@ -432,10 +512,26 @@ pub fn login_with_api_key(
|
||||
codex_home: &Path,
|
||||
api_key: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<()> {
|
||||
login_with_api_key_and_fedramp_status(
|
||||
codex_home,
|
||||
api_key,
|
||||
/*openai_api_key_is_fedramp*/ None,
|
||||
auth_credentials_store_mode,
|
||||
)
|
||||
}
|
||||
|
||||
/// Writes an `auth.json` that contains the API key and optional FedRAMP routing metadata.
|
||||
pub fn login_with_api_key_and_fedramp_status(
|
||||
codex_home: &Path,
|
||||
api_key: &str,
|
||||
openai_api_key_is_fedramp: Option<bool>,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::ApiKey),
|
||||
openai_api_key: Some(api_key.to_string()),
|
||||
openai_api_key_is_fedramp,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
};
|
||||
@@ -809,6 +905,7 @@ impl AuthDotJson {
|
||||
Ok(Self {
|
||||
auth_mode: Some(ApiAuthMode::ChatgptAuthTokens),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(tokens),
|
||||
last_refresh: Some(Utc::now()),
|
||||
})
|
||||
|
||||
@@ -34,6 +34,13 @@ pub struct AuthDotJson {
|
||||
#[serde(rename = "OPENAI_API_KEY")]
|
||||
pub openai_api_key: Option<String>,
|
||||
|
||||
#[serde(
|
||||
rename = "OPENAI_API_KEY_IS_FEDRAMP",
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub openai_api_key_is_fedramp: Option<bool>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tokens: Option<TokenData>,
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ async fn file_storage_load_returns_auth_dot_json() -> anyhow::Result<()> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("test-key".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: Some(Utc::now()),
|
||||
};
|
||||
@@ -36,6 +37,7 @@ async fn file_storage_save_persists_auth_dot_json() -> anyhow::Result<()> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("test-key".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: Some(Utc::now()),
|
||||
};
|
||||
@@ -58,6 +60,7 @@ fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("sk-test-key".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
};
|
||||
@@ -81,6 +84,7 @@ fn ephemeral_storage_save_load_delete_is_in_memory_only() -> anyhow::Result<()>
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("sk-ephemeral".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: Some(Utc::now()),
|
||||
};
|
||||
@@ -174,6 +178,7 @@ fn auth_with_prefix(prefix: &str) -> AuthDotJson {
|
||||
AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some(format!("{prefix}-api-key")),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(TokenData {
|
||||
id_token: id_token_with_prefix(prefix),
|
||||
access_token: format!("{prefix}-access"),
|
||||
@@ -195,6 +200,7 @@ fn keyring_auth_storage_load_returns_deserialized_auth() -> anyhow::Result<()> {
|
||||
let expected = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("sk-test".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
};
|
||||
@@ -232,6 +238,7 @@ fn keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Res
|
||||
let auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(TokenData {
|
||||
id_token: Default::default(),
|
||||
access_token: "access".to_string(),
|
||||
|
||||
@@ -32,14 +32,19 @@ pub use auth::ExternalAuthChatgptMetadata;
|
||||
pub use auth::ExternalAuthRefreshContext;
|
||||
pub use auth::ExternalAuthRefreshReason;
|
||||
pub use auth::ExternalAuthTokens;
|
||||
pub use auth::OPENAI_API_BASE_URL;
|
||||
pub use auth::OPENAI_API_KEY_ENV_VAR;
|
||||
pub use auth::OPENAI_GOV_API_BASE_URL;
|
||||
pub use auth::OpenAiApiKeyLoginCheck;
|
||||
pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
pub use auth::RefreshTokenError;
|
||||
pub use auth::UnauthorizedRecovery;
|
||||
pub use auth::check_openai_api_key_for_login;
|
||||
pub use auth::default_client;
|
||||
pub use auth::enforce_login_restrictions;
|
||||
pub use auth::load_auth_dot_json;
|
||||
pub use auth::login_with_api_key;
|
||||
pub use auth::login_with_api_key_and_fedramp_status;
|
||||
pub use auth::logout;
|
||||
pub use auth::read_openai_api_key_from_env;
|
||||
pub use auth::save_auth;
|
||||
|
||||
@@ -779,6 +779,7 @@ pub(crate) async fn persist_tokens_async(
|
||||
let auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: api_key,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(tokens),
|
||||
last_refresh: Some(Utc::now()),
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@ async fn refresh_token_succeeds_updates_storage() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -115,6 +116,7 @@ async fn refresh_token_refreshes_when_auth_is_unchanged() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -169,6 +171,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -178,6 +181,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -232,6 +236,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -242,6 +247,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -300,6 +306,7 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(stale_refresh),
|
||||
};
|
||||
@@ -347,6 +354,7 @@ async fn refreshes_token_when_access_token_is_expired() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(fresh_refresh),
|
||||
};
|
||||
@@ -396,6 +404,7 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens),
|
||||
last_refresh: Some(stale_refresh),
|
||||
};
|
||||
@@ -406,6 +415,7 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens.clone()),
|
||||
last_refresh: Some(fresh_refresh),
|
||||
};
|
||||
@@ -457,6 +467,7 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens),
|
||||
last_refresh: Some(stale_refresh),
|
||||
};
|
||||
@@ -467,6 +478,7 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens.clone()),
|
||||
last_refresh: Some(fresh_refresh),
|
||||
};
|
||||
@@ -516,6 +528,7 @@ async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Re
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -568,6 +581,7 @@ async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -634,6 +648,7 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -655,6 +670,7 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens.clone()),
|
||||
last_refresh: Some(fresh_refresh),
|
||||
};
|
||||
@@ -713,6 +729,7 @@ async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()>
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -765,6 +782,7 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -774,6 +792,7 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -857,6 +876,7 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -867,6 +887,7 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
|
||||
let disk_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: Some(disk_tokens),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
};
|
||||
@@ -924,6 +945,7 @@ async fn unauthorized_recovery_requires_chatgpt_auth() -> Result<()> {
|
||||
let auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
openai_api_key: Some("sk-test".to_string()),
|
||||
openai_api_key_is_fedramp: None,
|
||||
tokens: None,
|
||||
last_refresh: None,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user