validate api key before login success

This commit is contained in:
Rahul Thathoo
2026-05-09 16:38:42 -07:00
parent 789b7e39dc
commit 6661caf131
5 changed files with 100 additions and 1 deletions

View File

@@ -343,6 +343,7 @@ use codex_mcp::resolve_oauth_scopes;
use codex_memories_write::clear_memory_roots_contents;
use codex_model_provider::ProviderAccountError;
use codex_model_provider::create_model_provider;
use codex_model_provider::validate_api_key_with_models_endpoint;
use codex_models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationMode;

View File

@@ -271,6 +271,8 @@ impl AccountRequestProcessor {
}
}
self.validate_api_key(&params.api_key).await?;
match login_with_api_key(
&self.config.codex_home,
&params.api_key,
@@ -284,6 +286,21 @@ impl AccountRequestProcessor {
}
}
async fn validate_api_key(&self, api_key: &str) -> Result<(), JSONRPCErrorError> {
if !self.config.model_provider.requires_openai_auth {
return Ok(());
}
validate_api_key_with_models_endpoint(self.config.model_provider.clone(), api_key)
.await
.map_err(|err| match err {
CodexErr::UnexpectedStatus(err) if matches!(err.status.as_u16(), 401 | 403) => {
invalid_request("API key is invalid or unusable.")
}
err => internal_error(format!("failed to validate api key: {err}")),
})
}
async fn login_api_key_v2(&self, request_id: ConnectionRequestId, params: LoginApiKeyParams) {
let result = self
.login_api_key_common(&params)

View File

@@ -883,7 +883,23 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
#[tokio::test]
async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
let mock_server = MockServer::start().await;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
requires_openai_auth: Some(true),
base_url: Some(format!("{}/v1", mock_server.uri())),
..Default::default()
},
)?;
Mock::given(method("GET"))
.and(path("/v1/models"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"models": []
})))
.expect(1)
.mount(&mock_server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
@@ -928,6 +944,45 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn login_account_api_key_rejects_unusable_key_before_persisting() -> Result<()> {
let codex_home = TempDir::new()?;
let mock_server = MockServer::start().await;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
requires_openai_auth: Some(true),
base_url: Some(format!("{}/v1", mock_server.uri())),
..Default::default()
},
)?;
Mock::given(method("GET"))
.and(path("/v1/models"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": { "message": "Invalid API key" }
})))
.expect(1)
.mount(&mock_server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_login_account_api_key_request("sk-invalid-key")
.await?;
let err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(err.error.message, "API key is invalid or unusable.");
assert!(!codex_home.path().join("auth.json").exists());
Ok(())
}
#[tokio::test]
async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> {
let codex_home = TempDir::new()?;

View File

@@ -9,6 +9,7 @@ pub use auth::unauthenticated_auth_provider;
pub use bearer_auth_provider::BearerAuthProvider;
pub use bearer_auth_provider::BearerAuthProvider as CoreAuthProvider;
pub use codex_protocol::account::ProviderAccount;
pub use models_endpoint::validate_api_key_with_models_endpoint;
pub use provider::ModelProvider;
pub use provider::ProviderAccountError;
pub use provider::ProviderAccountResult;

View File

@@ -16,6 +16,7 @@ use codex_login::CodexAuth;
use codex_login::collect_auth_env_telemetry;
use codex_login::default_client::build_reqwest_client;
use codex_model_provider_info::ModelProviderInfo;
use codex_models_manager::client_version_to_whole;
use codex_models_manager::manager::ModelsEndpointClient;
use codex_otel::TelemetryAuthMode;
use codex_protocol::error::CodexErr;
@@ -36,6 +37,7 @@ const MODELS_ENDPOINT: &str = "/models";
pub(crate) struct OpenAiModelsEndpoint {
provider_info: ModelProviderInfo,
auth_manager: Option<Arc<AuthManager>>,
auth_override: Option<CodexAuth>,
}
impl OpenAiModelsEndpoint {
@@ -46,10 +48,23 @@ impl OpenAiModelsEndpoint {
Self {
provider_info,
auth_manager,
auth_override: None,
}
}
fn with_auth(provider_info: ModelProviderInfo, auth: CodexAuth) -> Self {
Self {
provider_info,
auth_manager: None,
auth_override: Some(auth),
}
}
async fn auth(&self) -> Option<CodexAuth> {
if let Some(auth) = self.auth_override.as_ref() {
return Some(auth.clone());
}
match self.auth_manager.as_ref() {
Some(auth_manager) => auth_manager.auth().await,
None => None,
@@ -65,6 +80,16 @@ impl OpenAiModelsEndpoint {
}
}
pub async fn validate_api_key_with_models_endpoint(
provider_info: ModelProviderInfo,
api_key: &str,
) -> CoreResult<()> {
OpenAiModelsEndpoint::with_auth(provider_info, CodexAuth::from_api_key(api_key))
.list_models(&client_version_to_whole())
.await
.map(|_| ())
}
#[async_trait]
impl ModelsEndpointClient for OpenAiModelsEndpoint {
fn has_command_auth(&self) -> bool {