mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
changes
This commit is contained in:
Generated
+1
@@ -3649,6 +3649,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-keyring-store",
|
||||
"codex-protocol",
|
||||
"codex-secrets",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-home-dir",
|
||||
"codex-utils-pty",
|
||||
|
||||
@@ -286,7 +286,7 @@ impl AccountRequestProcessor {
|
||||
&self.config.codex_home,
|
||||
¶ms.api_key,
|
||||
self.config.cli_auth_credentials_store_mode,
|
||||
self.config.cli_auth_keyring_backend_kind(),
|
||||
self.config.auth_keyring_backend_kind(),
|
||||
) {
|
||||
Ok(()) => {
|
||||
self.auth_manager.reload().await;
|
||||
@@ -335,7 +335,7 @@ impl AccountRequestProcessor {
|
||||
CLIENT_ID.to_string(),
|
||||
config.forced_chatgpt_workspace_id.clone(),
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
@@ -161,6 +161,7 @@ impl McpRequestProcessor {
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
&resolved_scopes.scopes,
|
||||
|
||||
@@ -1677,6 +1677,7 @@ impl PluginRequestProcessor {
|
||||
);
|
||||
|
||||
let store_mode = config.mcp_oauth_credentials_store_mode;
|
||||
let keyring_backend_kind = config.auth_keyring_backend_kind();
|
||||
let callback_port = config.mcp_oauth_callback_port;
|
||||
let callback_url = config.mcp_oauth_callback_url.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
@@ -1688,6 +1689,7 @@ impl PluginRequestProcessor {
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_config.http_headers.clone(),
|
||||
oauth_config.env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
@@ -1704,6 +1706,7 @@ impl PluginRequestProcessor {
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
|
||||
@@ -9,7 +9,7 @@ use chrono::Utc;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthDotJson;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::save_auth;
|
||||
use codex_login::token_data::TokenData;
|
||||
use codex_login::token_data::parse_chatgpt_jwt_claims;
|
||||
@@ -172,7 +172,7 @@ pub fn write_chatgpt_auth(
|
||||
codex_home,
|
||||
&auth,
|
||||
cli_auth_credentials_store_mode,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("write auth.json")
|
||||
}
|
||||
|
||||
@@ -1199,7 +1199,7 @@ fn auth_check(config: &Config) -> DoctorCheck {
|
||||
match load_auth_dot_json(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
) {
|
||||
Ok(Some(auth)) => {
|
||||
details.push(format!("stored auth mode: {}", stored_auth_mode(&auth)));
|
||||
@@ -2526,7 +2526,7 @@ fn provider_reachability_plan(config: &Config) -> ReachabilityPlan {
|
||||
let stored_auth = load_auth_dot_json(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
+10
-10
@@ -10,8 +10,8 @@
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::Config;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::login_with_access_token;
|
||||
@@ -118,14 +118,14 @@ pub async fn login_with_chatgpt(
|
||||
codex_home: PathBuf,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<()> {
|
||||
let opts = ServerOptions::new(
|
||||
codex_home,
|
||||
CLIENT_ID.to_string(),
|
||||
forced_chatgpt_workspace_id,
|
||||
cli_auth_credentials_store_mode,
|
||||
cli_auth_keyring_backend_kind,
|
||||
auth_keyring_backend_kind,
|
||||
);
|
||||
let server = run_login_server(opts)?;
|
||||
|
||||
@@ -150,7 +150,7 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
|
||||
config.codex_home.to_path_buf(),
|
||||
forced_chatgpt_workspace_id,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -182,7 +182,7 @@ pub async fn run_login_with_api_key(
|
||||
&config.codex_home,
|
||||
&api_key,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
) {
|
||||
Ok(_) => {
|
||||
eprintln!("{LOGIN_SUCCESS_MESSAGE}");
|
||||
@@ -213,7 +213,7 @@ pub async fn run_login_with_access_token(
|
||||
&access_token,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(&config.chatgpt_base_url),
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -288,7 +288,7 @@ pub async fn run_login_with_device_code(
|
||||
client_id.unwrap_or(CLIENT_ID.to_string()),
|
||||
forced_chatgpt_workspace_id,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
);
|
||||
if let Some(iss) = issuer_base_url {
|
||||
opts.issuer = iss;
|
||||
@@ -328,7 +328,7 @@ pub async fn run_login_with_device_code_fallback_to_browser(
|
||||
client_id.unwrap_or(CLIENT_ID.to_string()),
|
||||
forced_chatgpt_workspace_id,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
);
|
||||
if let Some(iss) = issuer_base_url {
|
||||
opts.issuer = iss;
|
||||
@@ -377,7 +377,7 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
Some(&config.chatgpt_base_url),
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -422,7 +422,7 @@ pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||
match logout_with_revoke(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -211,6 +211,7 @@ async fn perform_oauth_login_retry_without_scopes(
|
||||
name: &str,
|
||||
url: &str,
|
||||
store_mode: codex_config::types::OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: codex_config::types::AuthKeyringBackendKind,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
resolved_scopes: &ResolvedMcpOAuthScopes,
|
||||
@@ -223,6 +224,7 @@ async fn perform_oauth_login_retry_without_scopes(
|
||||
name,
|
||||
url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_headers.clone(),
|
||||
env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
@@ -240,6 +242,7 @@ async fn perform_oauth_login_retry_without_scopes(
|
||||
name,
|
||||
url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
&[],
|
||||
@@ -384,6 +387,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&resolved_scopes,
|
||||
@@ -478,6 +482,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
&resolved_scopes,
|
||||
@@ -514,7 +519,12 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
|
||||
_ => bail!("OAuth logout is only supported for streamable_http transports."),
|
||||
};
|
||||
|
||||
match delete_oauth_tokens(&name, &url, config.mcp_oauth_credentials_store_mode) {
|
||||
match delete_oauth_tokens(
|
||||
&name,
|
||||
&url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
) {
|
||||
Ok(true) => println!("Removed OAuth credentials for '{name}'."),
|
||||
Ok(false) => println!("No OAuth credentials stored for '{name}'."),
|
||||
Err(err) => return Err(anyhow!("failed to delete OAuth credentials: {err}")),
|
||||
@@ -541,6 +551,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
effective_mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
/*auth*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -5,8 +5,8 @@ use codex_config::CloudConfigBundleLoadError;
|
||||
use codex_config::CloudConfigBundleLoadErrorCode;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
@@ -63,7 +63,7 @@ pub async fn cloud_config_bundle_loader_for_storage(
|
||||
enable_codex_api_key_env,
|
||||
credentials_store_mode,
|
||||
Some(chatgpt_base_url.clone()),
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await;
|
||||
cloud_config_bundle_loader(auth_manager, chatgpt_base_url, codex_home)
|
||||
|
||||
@@ -50,7 +50,7 @@ pub async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthM
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
chatgpt_base_url.or(Some(config.chatgpt_base_url.clone())),
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
@@ -119,6 +120,7 @@ impl McpConnectionManager {
|
||||
pub async fn new(
|
||||
mcp_servers: &HashMap<String, EffectiveMcpServer>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth_entries: HashMap<String, McpAuthStatusEntry>,
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
submit_id: String,
|
||||
@@ -198,6 +200,7 @@ impl McpConnectionManager {
|
||||
server_name.clone(),
|
||||
server,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
cancel_token.clone(),
|
||||
tx_event.clone(),
|
||||
elicitation_requests.clone(),
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::tools::normalize_tools_for_model_with_prefix;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
@@ -1213,6 +1214,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
let manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
OAuthCredentialsStoreMode::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
HashMap::new(),
|
||||
&approval_policy,
|
||||
String::new(),
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::HashMap;
|
||||
use anyhow::Result;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
@@ -130,6 +131,7 @@ pub fn should_retry_without_scopes(scopes: &ResolvedMcpOAuthScopes, error: &anyh
|
||||
pub async fn compute_auth_statuses<'a, I>(
|
||||
servers: I,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> HashMap<String, McpAuthStatusEntry>
|
||||
where
|
||||
@@ -152,7 +154,15 @@ where
|
||||
async move {
|
||||
let auth_status = match config.as_ref() {
|
||||
Some(config) => {
|
||||
match compute_auth_status(&name, config, store_mode, has_runtime_auth).await {
|
||||
match compute_auth_status(
|
||||
&name,
|
||||
config,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
has_runtime_auth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(status) => status,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
@@ -179,6 +189,7 @@ async fn compute_auth_status(
|
||||
server_name: &str,
|
||||
config: &McpServerConfig,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
has_runtime_auth: bool,
|
||||
) -> Result<McpAuthStatus> {
|
||||
if !config.enabled {
|
||||
@@ -204,6 +215,7 @@ async fn compute_auth_status(
|
||||
http_headers.clone(),
|
||||
env_http_headers.clone(),
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
@@ -113,6 +115,8 @@ pub struct McpConfig {
|
||||
pub codex_home: PathBuf,
|
||||
/// Preferred credential store for MCP OAuth tokens.
|
||||
pub mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode,
|
||||
/// Backend used when MCP OAuth storage is configured for keyring-backed persistence.
|
||||
pub auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
/// Optional fixed localhost callback port for MCP OAuth login.
|
||||
pub mcp_oauth_callback_port: Option<u16>,
|
||||
/// Optional OAuth redirect URI override for MCP login.
|
||||
@@ -264,6 +268,7 @@ pub async fn read_mcp_resource(
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
@@ -273,6 +278,7 @@ pub async fn read_mcp_resource(
|
||||
let manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind,
|
||||
auth_statuses,
|
||||
&config.approval_policy,
|
||||
String::new(),
|
||||
@@ -332,6 +338,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
let auth_status_entries = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind,
|
||||
auth,
|
||||
)
|
||||
.await;
|
||||
@@ -345,6 +352,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
let mcp_connection_manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind,
|
||||
auth_status_entries.clone(),
|
||||
&config.approval_policy,
|
||||
submit_id,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
@@ -19,6 +21,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
apps_mcp_product_sku: None,
|
||||
codex_home,
|
||||
mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode::default(),
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
mcp_oauth_callback_port: None,
|
||||
mcp_oauth_callback_url: None,
|
||||
skill_mcp_dependency_install_enabled: true,
|
||||
|
||||
@@ -45,6 +45,7 @@ use codex_async_utils::CancelErr;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_exec_server::HttpClient;
|
||||
use codex_exec_server::ReqwestHttpClient;
|
||||
@@ -141,6 +142,7 @@ impl AsyncManagedClient {
|
||||
server_name: String,
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
cancel_token: CancellationToken,
|
||||
tx_event: Sender<Event>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
@@ -179,6 +181,7 @@ impl AsyncManagedClient {
|
||||
&server_name,
|
||||
server.clone(),
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
runtime_context,
|
||||
runtime_auth_provider,
|
||||
)
|
||||
@@ -577,6 +580,7 @@ async fn make_rmcp_client(
|
||||
server_name: &str,
|
||||
server: EffectiveMcpServer,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
runtime_context: McpRuntimeContext,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Result<RmcpClient, StartupOutcomeError> {
|
||||
@@ -648,6 +652,7 @@ async fn make_rmcp_client(
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_client,
|
||||
runtime_auth_provider,
|
||||
)
|
||||
|
||||
@@ -113,6 +113,26 @@ pub enum OAuthCredentialsStoreMode {
|
||||
Keyring,
|
||||
}
|
||||
|
||||
/// Determine how auth credentials should use keyring-backed storage.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthKeyringBackendKind {
|
||||
/// Store the serialized auth payload directly in the OS keyring.
|
||||
Direct,
|
||||
/// Store auth payloads in the local encrypted secrets file, with the file key in the OS keyring.
|
||||
Secrets,
|
||||
}
|
||||
|
||||
impl Default for AuthKeyringBackendKind {
|
||||
fn default() -> Self {
|
||||
if cfg!(windows) {
|
||||
Self::Secrets
|
||||
} else {
|
||||
Self::Direct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WindowsSandboxModeToml {
|
||||
|
||||
@@ -12,6 +12,7 @@ pub use codex_config::config_toml::ProjectConfig;
|
||||
pub use codex_config::config_toml::RealtimeAudioConfig;
|
||||
pub use codex_config::config_toml::RealtimeConfig;
|
||||
pub use codex_config::types::AuthCredentialsStoreMode;
|
||||
pub use codex_config::types::AuthKeyringBackendKind;
|
||||
pub use codex_config::types::History;
|
||||
pub use codex_config::types::MemoriesConfig;
|
||||
pub use codex_config::types::ModelAvailabilityNuxConfig;
|
||||
|
||||
@@ -38,6 +38,7 @@ use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::sandbox_mode_requirement_for_permission_profile;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::History;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerDisabledReason;
|
||||
@@ -70,7 +71,6 @@ use codex_features::NetworkProxyConfigToml;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_install_context::InstallContext;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_mcp::McpConfig;
|
||||
use codex_memories_read::memory_root;
|
||||
use codex_model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
|
||||
@@ -1104,8 +1104,8 @@ impl AuthManagerConfig for Config {
|
||||
self.cli_auth_credentials_store_mode
|
||||
}
|
||||
|
||||
fn cli_auth_keyring_backend_kind(&self) -> CliAuthKeyringBackendKind {
|
||||
Config::cli_auth_keyring_backend_kind(self)
|
||||
fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind {
|
||||
Config::auth_keyring_backend_kind(self)
|
||||
}
|
||||
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>> {
|
||||
@@ -1356,13 +1356,11 @@ impl Config {
|
||||
workspace_roots
|
||||
}
|
||||
|
||||
pub fn cli_auth_keyring_backend_kind(&self) -> CliAuthKeyringBackendKind {
|
||||
// TODO: Migrate all keyring-backed CLI auth storage to the secrets
|
||||
// backend after validating this path on Windows.
|
||||
pub fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind {
|
||||
if self.features.enabled(Feature::SecretAuthStorage) {
|
||||
CliAuthKeyringBackendKind::Secrets
|
||||
AuthKeyringBackendKind::Secrets
|
||||
} else {
|
||||
CliAuthKeyringBackendKind::Direct
|
||||
AuthKeyringBackendKind::Direct
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1429,6 +1427,7 @@ impl Config {
|
||||
apps_mcp_product_sku: self.apps_mcp_product_sku.clone(),
|
||||
codex_home: self.codex_home.to_path_buf(),
|
||||
mcp_oauth_credentials_store_mode: self.mcp_oauth_credentials_store_mode,
|
||||
auth_keyring_backend_kind: self.auth_keyring_backend_kind(),
|
||||
mcp_oauth_callback_port: self.mcp_oauth_callback_port,
|
||||
mcp_oauth_callback_url: self.mcp_oauth_callback_url.clone(),
|
||||
skill_mcp_dependency_install_enabled: self
|
||||
|
||||
@@ -279,6 +279,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
let auth_status_entries = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
@@ -290,6 +291,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_mcp_manager(
|
||||
let mcp_connection_manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
auth_status_entries,
|
||||
&config.permissions.approval_policy,
|
||||
INITIAL_SUBMIT_ID.to_owned(),
|
||||
|
||||
@@ -156,6 +156,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
oauth_config.http_headers.clone(),
|
||||
oauth_config.env_http_headers.clone(),
|
||||
&resolved_scopes.scopes,
|
||||
@@ -172,6 +173,7 @@ pub(crate) async fn maybe_install_mcp_dependencies(
|
||||
&name,
|
||||
&oauth_config.url,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
oauth_config.http_headers,
|
||||
oauth_config.env_http_headers,
|
||||
&[],
|
||||
|
||||
@@ -1260,6 +1260,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
|
||||
let manager = codex_mcp::McpConnectionManager::new(
|
||||
&HashMap::new(),
|
||||
turn_context.config.mcp_oauth_credentials_store_mode,
|
||||
turn_context.config.auth_keyring_backend_kind(),
|
||||
HashMap::new(),
|
||||
&turn_context.approval_policy,
|
||||
turn_context.sub_id.clone(),
|
||||
|
||||
@@ -303,8 +303,14 @@ impl Session {
|
||||
effective_mcp_servers_from_configured(mcp_servers, &mcp_config, auth.as_ref());
|
||||
let host_owned_codex_apps_enabled =
|
||||
host_owned_codex_apps_enabled(&mcp_config, auth.as_ref());
|
||||
let auth_statuses =
|
||||
compute_auth_statuses(mcp_servers.iter(), store_mode, auth.as_ref()).await;
|
||||
let keyring_backend_kind = config.auth_keyring_backend_kind();
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let mcp_runtime_context = match turn_context.environments.primary() {
|
||||
Some(turn_environment) => McpRuntimeContext::new(
|
||||
Arc::clone(&self.services.environment_manager),
|
||||
@@ -326,6 +332,7 @@ impl Session {
|
||||
let refreshed_manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
auth_statuses,
|
||||
&turn_context.approval_policy,
|
||||
turn_context.sub_id.clone(),
|
||||
|
||||
@@ -611,6 +611,7 @@ impl Session {
|
||||
let auth_statuses = compute_auth_statuses(
|
||||
mcp_servers.iter(),
|
||||
config_for_mcp.mcp_oauth_credentials_store_mode,
|
||||
config_for_mcp.auth_keyring_backend_kind(),
|
||||
auth.as_ref(),
|
||||
)
|
||||
.await;
|
||||
@@ -1142,6 +1143,7 @@ impl Session {
|
||||
let mcp_connection_manager = McpConnectionManager::new(
|
||||
&mcp_servers,
|
||||
config.mcp_oauth_credentials_store_mode,
|
||||
config.auth_keyring_backend_kind(),
|
||||
auth_statuses,
|
||||
&session_configuration.approval_policy,
|
||||
INITIAL_SUBMIT_ID.to_owned(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_core::resolve_installation_id;
|
||||
use codex_core::thread_store_from_config;
|
||||
use codex_extension_api::empty_extension_registry;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::originator;
|
||||
@@ -1121,6 +1122,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -466,7 +466,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
keyring_backend_kind: config.cli_auth_keyring_backend_kind(),
|
||||
keyring_backend_kind: config.auth_keyring_backend_kind(),
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
|
||||
@@ -43,7 +43,7 @@ async fn refresh_without_id_token() {
|
||||
let storage = create_auth_storage(
|
||||
codex_home.path().to_path_buf(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
let updated = super::persist_tokens(
|
||||
&storage,
|
||||
@@ -82,7 +82,7 @@ fn login_with_api_key_overwrites_existing_auth_json() {
|
||||
dir.path(),
|
||||
"sk-new",
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("login_with_api_key should succeed");
|
||||
|
||||
@@ -115,7 +115,7 @@ async fn login_with_access_token_writes_only_token() {
|
||||
&agent_identity,
|
||||
AuthCredentialsStoreMode::File,
|
||||
Some(&chatgpt_base_url),
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect("login_with_access_token should succeed");
|
||||
@@ -221,7 +221,7 @@ async fn login_with_access_token_rejects_invalid_jwt() {
|
||||
"not-a-jwt",
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid access token should fail");
|
||||
@@ -252,7 +252,7 @@ async fn login_with_access_token_rejects_unsigned_jwt() {
|
||||
&agent_identity,
|
||||
AuthCredentialsStoreMode::File,
|
||||
Some(&chatgpt_base_url),
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect_err("unsigned access token should fail");
|
||||
@@ -273,7 +273,7 @@ async fn missing_auth_json_returns_none() {
|
||||
dir.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect("call should succeed");
|
||||
@@ -300,7 +300,7 @@ async fn pro_account_with_no_api_key_uses_chatgpt_auth() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -358,7 +358,7 @@ async fn loads_api_key_from_auth_json() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -384,14 +384,14 @@ fn logout_removes_auth_file() -> Result<(), std::io::Error> {
|
||||
dir.path(),
|
||||
&auth_dot_json,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
let auth_file = get_auth_file(dir.path());
|
||||
assert!(auth_file.exists());
|
||||
assert!(logout(
|
||||
dir.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?);
|
||||
assert!(!auth_file.exists());
|
||||
Ok(())
|
||||
@@ -405,7 +405,7 @@ async fn unauthorized_recovery_reports_mode_and_step_names() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await;
|
||||
let managed = UnauthorizedRecovery {
|
||||
@@ -447,7 +447,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -466,7 +466,7 @@ async fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
|
||||
updated_auth_dot_json,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("updated auth should parse");
|
||||
@@ -768,7 +768,7 @@ async fn build_config(
|
||||
AuthConfig {
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
forced_login_method,
|
||||
forced_chatgpt_workspace_id,
|
||||
chatgpt_base_url: None,
|
||||
@@ -851,7 +851,7 @@ async fn load_auth_reads_access_token_from_env() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
Some(&chatgpt_base_url),
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -975,7 +975,7 @@ async fn load_auth_keeps_codex_api_key_env_precedence() {
|
||||
/*enable_codex_api_key_env*/ true,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("env auth should load")
|
||||
@@ -993,7 +993,7 @@ async fn enforce_login_restrictions_logs_out_for_method_mismatch() {
|
||||
codex_home.path(),
|
||||
"sk-test",
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("seed api key");
|
||||
|
||||
@@ -1146,14 +1146,14 @@ async fn enforce_login_restrictions_logs_out_for_agent_identity_workspace_mismat
|
||||
personal_access_token: None,
|
||||
},
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("seed agent identity auth");
|
||||
|
||||
let config = AuthConfig {
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
forced_login_method: None,
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
chatgpt_base_url: Some(chatgpt_base_url),
|
||||
@@ -1181,7 +1181,7 @@ async fn enforce_login_restrictions_allows_api_key_if_login_method_not_set_but_f
|
||||
codex_home.path(),
|
||||
"sk-test",
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.expect("seed api key");
|
||||
|
||||
@@ -1407,7 +1407,7 @@ async fn plan_type_maps_known_plan() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1436,7 +1436,7 @@ async fn plan_type_maps_self_serve_business_usage_based_plan() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1468,7 +1468,7 @@ async fn plan_type_maps_enterprise_cbp_usage_based_plan() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1500,7 +1500,7 @@ async fn plan_type_maps_unknown_to_unknown() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
@@ -1529,7 +1529,7 @@ async fn missing_plan_type_maps_to_unknown() {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await
|
||||
.expect("load auth")
|
||||
|
||||
@@ -32,8 +32,8 @@ pub use crate::auth::agent_identity::AgentIdentityAuth;
|
||||
pub use crate::auth::personal_access_token::PersonalAccessTokenAuth;
|
||||
pub use crate::auth::storage::AgentIdentityAuthRecord;
|
||||
pub use crate::auth::storage::AuthDotJson;
|
||||
pub use crate::auth::storage::AuthKeyringBackendKind;
|
||||
use crate::auth::storage::AuthStorageBackend;
|
||||
pub use crate::auth::storage::CliAuthKeyringBackendKind;
|
||||
use crate::auth::storage::create_auth_storage;
|
||||
use crate::auth::util::try_parse_error_message;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
@@ -211,7 +211,7 @@ impl CodexAuth {
|
||||
auth_dot_json: AuthDotJson,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<Self> {
|
||||
let auth_mode = auth_dot_json.resolved_mode();
|
||||
let client = create_client();
|
||||
@@ -268,7 +268,7 @@ impl CodexAuth {
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
load_auth(
|
||||
codex_home,
|
||||
@@ -491,7 +491,7 @@ impl CodexAuth {
|
||||
let storage = create_auth_storage(
|
||||
PathBuf::from(format!("dummy-chatgpt-auth-{dummy_auth_id}")),
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
Self::Chatgpt(ChatgptAuth { state, storage })
|
||||
}
|
||||
@@ -565,7 +565,7 @@ async fn verified_agent_identity_record(
|
||||
pub fn logout(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<bool> {
|
||||
let storage = create_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
@@ -578,7 +578,7 @@ pub fn logout(
|
||||
pub async fn logout_with_revoke(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<bool> {
|
||||
AuthManager::new(
|
||||
codex_home.to_path_buf(),
|
||||
@@ -597,7 +597,7 @@ pub fn login_with_api_key(
|
||||
codex_home: &Path,
|
||||
api_key: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(ApiAuthMode::ApiKey),
|
||||
@@ -621,7 +621,7 @@ pub async fn login_with_access_token(
|
||||
access_token: &str,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<()> {
|
||||
let auth_dot_json = match classify_codex_access_token(access_token) {
|
||||
CodexAccessToken::PersonalAccessToken(access_token) => {
|
||||
@@ -677,7 +677,7 @@ pub fn login_with_chatgpt_auth_tokens(
|
||||
codex_home,
|
||||
&auth_dot_json,
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -686,7 +686,7 @@ pub fn save_auth(
|
||||
codex_home: &Path,
|
||||
auth: &AuthDotJson,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<()> {
|
||||
let storage = create_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
@@ -704,7 +704,7 @@ pub fn save_auth(
|
||||
pub fn load_auth_dot_json(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<Option<AuthDotJson>> {
|
||||
let storage = create_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
@@ -718,7 +718,7 @@ pub fn load_auth_dot_json(
|
||||
pub struct AuthConfig {
|
||||
pub codex_home: PathBuf,
|
||||
pub auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
pub keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
pub keyring_backend_kind: AuthKeyringBackendKind,
|
||||
pub forced_login_method: Option<ForcedLoginMethod>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
@@ -821,7 +821,7 @@ fn logout_with_message(
|
||||
codex_home: &Path,
|
||||
message: String,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<()> {
|
||||
// External auth tokens live in the ephemeral store, but persistent auth may still exist
|
||||
// from earlier logins. Clear both so a forced logout truly removes all active auth.
|
||||
@@ -840,19 +840,19 @@ fn logout_with_message(
|
||||
fn logout_all_stores(
|
||||
codex_home: &Path,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<bool> {
|
||||
if auth_credentials_store_mode == AuthCredentialsStoreMode::Ephemeral {
|
||||
return logout(
|
||||
codex_home,
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
}
|
||||
let removed_ephemeral = logout(
|
||||
codex_home,
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
let removed_managed = logout(
|
||||
codex_home,
|
||||
@@ -867,7 +867,7 @@ async fn load_auth(
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<&str>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> std::io::Result<Option<CodexAuth>> {
|
||||
// API key via env var takes precedence over any other auth method.
|
||||
if enable_codex_api_key_env && let Some(api_key) = read_codex_api_key_from_env() {
|
||||
@@ -879,7 +879,7 @@ async fn load_auth(
|
||||
let ephemeral_storage = create_auth_storage(
|
||||
codex_home.to_path_buf(),
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
if let Some(auth_dot_json) = ephemeral_storage.load()? {
|
||||
let auth = CodexAuth::from_auth_dot_json(
|
||||
@@ -1419,7 +1419,7 @@ pub struct AuthManager {
|
||||
auth_change_tx: watch::Sender<u64>,
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
forced_chatgpt_workspace_id: RwLock<Option<Vec<String>>>,
|
||||
chatgpt_base_url: Option<String>,
|
||||
refresh_lock: Semaphore,
|
||||
@@ -1440,7 +1440,7 @@ pub trait AuthManagerConfig {
|
||||
fn cli_auth_credentials_store_mode(&self) -> AuthCredentialsStoreMode;
|
||||
|
||||
/// Returns the backend to use when CLI auth keyring storage is selected.
|
||||
fn cli_auth_keyring_backend_kind(&self) -> CliAuthKeyringBackendKind;
|
||||
fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind;
|
||||
|
||||
/// Returns the workspace IDs that ChatGPT auth should be restricted to, if any.
|
||||
fn forced_chatgpt_workspace_id(&self) -> Option<Vec<String>>;
|
||||
@@ -1480,7 +1480,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Self {
|
||||
let managed_auth = load_auth(
|
||||
&codex_home,
|
||||
@@ -1524,7 +1524,7 @@ impl AuthManager {
|
||||
auth_change_tx,
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind::default(),
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -1545,7 +1545,7 @@ impl AuthManager {
|
||||
auth_change_tx,
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind::default(),
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -1564,7 +1564,7 @@ impl AuthManager {
|
||||
auth_change_tx,
|
||||
enable_codex_api_key_env: false,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind::default(),
|
||||
keyring_backend_kind: AuthKeyringBackendKind::default(),
|
||||
forced_chatgpt_workspace_id: RwLock::new(None),
|
||||
chatgpt_base_url: None,
|
||||
refresh_lock: Semaphore::new(/*permits*/ 1),
|
||||
@@ -1784,7 +1784,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env: bool,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
chatgpt_base_url: Option<String>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(
|
||||
Self::new(
|
||||
@@ -1808,7 +1808,7 @@ impl AuthManager {
|
||||
enable_codex_api_key_env,
|
||||
config.cli_auth_credentials_store_mode(),
|
||||
Some(config.chatgpt_base_url()),
|
||||
config.cli_auth_keyring_backend_kind(),
|
||||
config.auth_keyring_backend_kind(),
|
||||
)
|
||||
.await;
|
||||
auth_manager.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id());
|
||||
@@ -2073,7 +2073,7 @@ impl AuthManager {
|
||||
&self.codex_home,
|
||||
&auth_dot_json,
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.map_err(RefreshTokenError::Transient)?;
|
||||
self.reload().await;
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_agent_identity::AgentIdentityJwtClaims;
|
||||
use codex_agent_identity::decode_agent_identity_jwt;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
pub use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_keyring_store::DefaultKeyringStore;
|
||||
use codex_keyring_store::KeyringStore;
|
||||
use codex_protocol::account::PlanType as AccountPlanType;
|
||||
@@ -106,25 +107,6 @@ pub(super) trait AuthStorageBackend: Debug + Send + Sync {
|
||||
fn delete(&self) -> std::io::Result<bool>;
|
||||
}
|
||||
|
||||
/// Backend used when CLI auth storage is configured for keyring-backed persistence.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CliAuthKeyringBackendKind {
|
||||
/// Store the serialized auth payload directly in the OS keyring.
|
||||
Direct,
|
||||
/// Store auth in the local encrypted secrets file, with the file key in the OS keyring.
|
||||
Secrets,
|
||||
}
|
||||
|
||||
impl Default for CliAuthKeyringBackendKind {
|
||||
fn default() -> Self {
|
||||
if cfg!(windows) {
|
||||
Self::Secrets
|
||||
} else {
|
||||
Self::Direct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct FileAuthStorage {
|
||||
codex_home: PathBuf,
|
||||
@@ -351,7 +333,7 @@ impl AutoAuthStorage {
|
||||
fn new(
|
||||
codex_home: PathBuf,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Self {
|
||||
Self {
|
||||
keyring_storage: create_keyring_auth_storage(
|
||||
@@ -438,7 +420,7 @@ impl AuthStorageBackend for EphemeralAuthStorage {
|
||||
pub(super) fn create_auth_storage(
|
||||
codex_home: PathBuf,
|
||||
mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Arc<dyn AuthStorageBackend> {
|
||||
let keyring_store: Arc<dyn KeyringStore> = Arc::new(DefaultKeyringStore);
|
||||
create_auth_storage_with_store(codex_home, mode, keyring_store, keyring_backend_kind)
|
||||
@@ -448,7 +430,7 @@ fn create_auth_storage_with_store(
|
||||
codex_home: PathBuf,
|
||||
mode: AuthCredentialsStoreMode,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Arc<dyn AuthStorageBackend> {
|
||||
match mode {
|
||||
AuthCredentialsStoreMode::File => Arc::new(FileAuthStorage::new(codex_home)),
|
||||
@@ -467,13 +449,13 @@ fn create_auth_storage_with_store(
|
||||
fn create_keyring_auth_storage(
|
||||
codex_home: PathBuf,
|
||||
keyring_store: Arc<dyn KeyringStore>,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Arc<dyn AuthStorageBackend> {
|
||||
match keyring_backend_kind {
|
||||
CliAuthKeyringBackendKind::Direct => {
|
||||
AuthKeyringBackendKind::Direct => {
|
||||
Arc::new(DirectKeyringAuthStorage::new(codex_home, keyring_store))
|
||||
}
|
||||
CliAuthKeyringBackendKind::Secrets => {
|
||||
AuthKeyringBackendKind::Secrets => {
|
||||
Arc::new(SecretsKeyringAuthStorage::new(codex_home, keyring_store))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ fn file_storage_delete_removes_auth_file() -> anyhow::Result<()> {
|
||||
let storage = create_auth_storage(
|
||||
dir.path().to_path_buf(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
storage.save(&auth_dot_json)?;
|
||||
assert!(dir.path().join("auth.json").exists());
|
||||
@@ -170,7 +170,7 @@ fn ephemeral_storage_save_load_delete_is_in_memory_only() -> anyhow::Result<()>
|
||||
let storage = create_auth_storage(
|
||||
dir.path().to_path_buf(),
|
||||
AuthCredentialsStoreMode::Ephemeral,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
let auth_dot_json = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
@@ -410,7 +410,7 @@ fn factory_uses_secrets_backend_only_when_requested() -> anyhow::Result<()> {
|
||||
direct_home.path().to_path_buf(),
|
||||
AuthCredentialsStoreMode::Keyring,
|
||||
Arc::new(direct_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
);
|
||||
let direct_auth = auth_with_prefix("factory-direct");
|
||||
direct_storage.save(&direct_auth)?;
|
||||
@@ -427,7 +427,7 @@ fn factory_uses_secrets_backend_only_when_requested() -> anyhow::Result<()> {
|
||||
secrets_home.path().to_path_buf(),
|
||||
AuthCredentialsStoreMode::Keyring,
|
||||
Arc::new(secrets_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
let secrets_auth = auth_with_prefix("factory-secrets");
|
||||
secrets_storage.save(&secrets_auth)?;
|
||||
@@ -503,7 +503,7 @@ fn auto_auth_storage_load_prefers_keyring_value() -> anyhow::Result<()> {
|
||||
let storage = AutoAuthStorage::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
Arc::new(mock_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
let keyring_auth = auth_with_prefix("keyring");
|
||||
seed_secrets_backend_with_auth(&mock_keyring, codex_home.path(), &keyring_auth)?;
|
||||
@@ -523,7 +523,7 @@ fn auto_auth_storage_load_uses_file_when_keyring_empty() -> anyhow::Result<()> {
|
||||
let storage = AutoAuthStorage::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
Arc::new(mock_keyring),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
|
||||
let expected = auth_with_prefix("file-only");
|
||||
@@ -541,7 +541,7 @@ fn auto_auth_storage_load_falls_back_when_keyring_errors() -> anyhow::Result<()>
|
||||
let storage = AutoAuthStorage::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
Arc::new(mock_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
let key = compute_keyring_account(codex_home.path());
|
||||
|
||||
@@ -564,7 +564,7 @@ fn auto_auth_storage_save_prefers_keyring() -> anyhow::Result<()> {
|
||||
let storage = AutoAuthStorage::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
Arc::new(mock_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
let stale = auth_with_prefix("stale");
|
||||
storage.file_storage.save(&stale)?;
|
||||
@@ -583,7 +583,7 @@ fn auto_auth_storage_save_falls_back_when_keyring_errors() -> anyhow::Result<()>
|
||||
let storage = AutoAuthStorage::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
Arc::new(mock_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
let key = compute_keyring_account(codex_home.path());
|
||||
mock_keyring.set_error(&key, KeyringError::Invalid("error".into(), "save".into()));
|
||||
@@ -615,7 +615,7 @@ fn auto_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()> {
|
||||
let storage = AutoAuthStorage::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
Arc::new(mock_keyring.clone()),
|
||||
CliAuthKeyringBackendKind::Secrets,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
);
|
||||
let auth = auth_with_prefix("to-delete");
|
||||
let auth_file = seed_secrets_backend_and_fallback_auth_file_for_delete(
|
||||
|
||||
@@ -217,7 +217,7 @@ pub async fn complete_device_code_login(
|
||||
tokens.access_token,
|
||||
tokens.refresh_token,
|
||||
opts.cli_auth_credentials_store_mode,
|
||||
opts.cli_auth_keyring_backend_kind,
|
||||
opts.auth_keyring_backend_kind,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ pub use server::run_login_server;
|
||||
|
||||
pub use auth::AuthConfig;
|
||||
pub use auth::AuthDotJson;
|
||||
pub use auth::AuthKeyringBackendKind;
|
||||
pub use auth::AuthManager;
|
||||
pub use auth::AuthManagerConfig;
|
||||
pub use auth::CLIENT_ID;
|
||||
pub use auth::CODEX_ACCESS_TOKEN_ENV_VAR;
|
||||
pub use auth::CODEX_API_KEY_ENV_VAR;
|
||||
pub use auth::CliAuthKeyringBackendKind;
|
||||
pub use auth::CodexAuth;
|
||||
pub use auth::ExternalAuth;
|
||||
pub use auth::ExternalAuthChatgptMetadata;
|
||||
|
||||
@@ -25,7 +25,7 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::auth::AuthDotJson;
|
||||
use crate::auth::CliAuthKeyringBackendKind;
|
||||
use crate::auth::AuthKeyringBackendKind;
|
||||
use crate::auth::load_auth_dot_json;
|
||||
use crate::auth::revoke_auth_tokens;
|
||||
use crate::auth::save_auth;
|
||||
@@ -73,7 +73,7 @@ pub struct ServerOptions {
|
||||
pub forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
pub codex_streamlined_login: bool,
|
||||
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
pub cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
pub auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
}
|
||||
|
||||
impl ServerOptions {
|
||||
@@ -83,7 +83,7 @@ impl ServerOptions {
|
||||
client_id: String,
|
||||
forced_chatgpt_workspace_id: Option<Vec<String>>,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
@@ -95,7 +95,7 @@ impl ServerOptions {
|
||||
forced_chatgpt_workspace_id,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_credentials_store_mode,
|
||||
cli_auth_keyring_backend_kind,
|
||||
auth_keyring_backend_kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,7 +366,7 @@ async fn process_request(
|
||||
tokens.access_token.clone(),
|
||||
tokens.refresh_token.clone(),
|
||||
opts.cli_auth_credentials_store_mode,
|
||||
opts.cli_auth_keyring_backend_kind,
|
||||
opts.auth_keyring_backend_kind,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -798,7 +798,7 @@ pub(crate) async fn persist_tokens_async(
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
auth_credentials_store_mode: AuthCredentialsStoreMode,
|
||||
keyring_backend_kind: CliAuthKeyringBackendKind,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> io::Result<()> {
|
||||
// Reuse existing synchronous logic but run it off the async runtime.
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
@@ -1201,7 +1201,7 @@ mod tests {
|
||||
use wiremock::matchers::path;
|
||||
|
||||
use crate::auth::AuthDotJson;
|
||||
use crate::auth::CliAuthKeyringBackendKind;
|
||||
use crate::auth::AuthKeyringBackendKind;
|
||||
use crate::auth::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use crate::auth::load_auth_dot_json;
|
||||
use crate::auth::save_auth;
|
||||
@@ -1249,7 +1249,7 @@ mod tests {
|
||||
codex_home.path(),
|
||||
&chatgpt_auth("old-access", "old-refresh", "old-account"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
persist_tokens_async(
|
||||
@@ -1259,14 +1259,14 @@ mod tests {
|
||||
"new-access".to_string(),
|
||||
"new-refresh".to_string(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?
|
||||
.context("auth.json should exist after login")?;
|
||||
assert_eq!(
|
||||
@@ -1315,7 +1315,7 @@ mod tests {
|
||||
codex_home.path(),
|
||||
&chatgpt_auth("old-access", "shared-refresh", "old-account"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
persist_tokens_async(
|
||||
@@ -1325,7 +1325,7 @@ mod tests {
|
||||
"new-access".to_string(),
|
||||
"shared-refresh".to_string(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::Direct,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use chrono::Utc;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthDotJson;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_login::RefreshTokenError;
|
||||
use codex_login::load_auth_dot_json;
|
||||
@@ -292,7 +292,7 @@ async fn refresh_token_skips_refresh_when_auth_changed() -> Result<()> {
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
ctx.auth_manager
|
||||
@@ -361,7 +361,7 @@ async fn refresh_token_errors_on_account_mismatch() -> Result<()> {
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let err = ctx
|
||||
@@ -534,7 +534,7 @@ async fn auth_reloads_disk_auth_when_cached_auth_is_stale() -> Result<()> {
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let cached_auth = ctx
|
||||
@@ -600,7 +600,7 @@ async fn auth_reloads_disk_auth_without_calling_expired_refresh_token() -> Resul
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let cached_auth = ctx
|
||||
@@ -865,7 +865,7 @@ async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
ctx.auth_manager
|
||||
@@ -991,7 +991,7 @@ async fn unauthorized_recovery_reloads_then_refreshes_tokens() -> Result<()> {
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let cached_before = ctx
|
||||
@@ -1089,7 +1089,7 @@ async fn unauthorized_recovery_errors_on_account_mismatch() -> Result<()> {
|
||||
ctx.codex_home.path(),
|
||||
&disk_auth,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let cached_before = ctx
|
||||
@@ -1181,7 +1181,7 @@ impl RefreshTokenTestContext {
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1196,7 +1196,7 @@ impl RefreshTokenTestContext {
|
||||
load_auth_dot_json(
|
||||
self.codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("load auth.json")?
|
||||
.context("auth.json should exist")
|
||||
@@ -1207,7 +1207,7 @@ impl RefreshTokenTestContext {
|
||||
self.codex_home.path(),
|
||||
auth_dot_json,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
self.auth_manager.reload().await;
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Context;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::auth::load_auth_dot_json;
|
||||
use codex_login::run_device_code_login;
|
||||
@@ -111,7 +111,7 @@ fn server_opts(
|
||||
"client-id".to_string(),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
cli_auth_credentials_store_mode,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -152,7 +152,7 @@ async fn device_code_login_integration_succeeds() -> anyhow::Result<()> {
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("auth.json should load after login succeeds")?
|
||||
.context("auth.json written")?;
|
||||
@@ -202,7 +202,7 @@ async fn device_code_login_rejects_workspace_mismatch() -> anyhow::Result<()> {
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("auth.json should load after login fails")?;
|
||||
assert!(
|
||||
@@ -237,7 +237,7 @@ async fn device_code_login_integration_handles_usercode_http_failure() -> anyhow
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("auth.json should load after login fails")?;
|
||||
assert!(
|
||||
@@ -276,7 +276,7 @@ async fn device_code_login_integration_persists_without_api_key_on_exchange_fail
|
||||
"client-id".to_string(),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -288,7 +288,7 @@ async fn device_code_login_integration_persists_without_api_key_on_exchange_fail
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("auth.json should load after login succeeds")?
|
||||
.context("auth.json written")?;
|
||||
@@ -331,7 +331,7 @@ async fn device_code_login_integration_handles_error_payload() -> anyhow::Result
|
||||
"client-id".to_string(),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -349,7 +349,7 @@ async fn device_code_login_integration_handles_error_payload() -> anyhow::Result
|
||||
let auth = load_auth_dot_json(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.context("auth.json should load after login fails")?;
|
||||
assert!(
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::ServerOptions;
|
||||
use codex_login::run_login_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
@@ -129,7 +129,7 @@ async fn end_to_end_login_flow_persists_auth_json() -> Result<()> {
|
||||
force_state: Some(state),
|
||||
forced_chatgpt_workspace_id: Some(vec![chatgpt_account_id.to_string()]),
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -192,7 +192,7 @@ async fn creates_missing_codex_home_dir() -> Result<()> {
|
||||
force_state: Some(state),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -236,7 +236,7 @@ async fn login_server_includes_forced_workspaces_as_one_query_param() -> Result<
|
||||
WORKSPACE_ID_SECOND_ALLOWED.to_string(),
|
||||
]),
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let auth_url = Url::parse(&server.auth_url)?;
|
||||
@@ -275,7 +275,7 @@ async fn forced_chatgpt_workspace_id_mismatch_blocks_login() -> Result<()> {
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: Some(vec![WORKSPACE_ID_ALLOWED.to_string()]),
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
assert!(
|
||||
@@ -336,7 +336,7 @@ async fn oauth_access_denied_missing_entitlement_blocks_login_with_clear_error()
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -405,7 +405,7 @@ async fn oauth_access_denied_unknown_reason_uses_generic_error_page() -> Result<
|
||||
force_state: Some(state.clone()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
let server = run_login_server(opts)?;
|
||||
let login_port = server.actual_port;
|
||||
@@ -506,7 +506,7 @@ async fn falls_back_to_registered_fallback_port_when_default_port_is_in_use() ->
|
||||
codex_login::CLIENT_ID.to_string(),
|
||||
/*forced_chatgpt_workspace_id*/ None,
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
);
|
||||
opts.issuer = issuer;
|
||||
opts.open_browser = false;
|
||||
@@ -552,7 +552,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
force_state: Some("cancel_state".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
|
||||
let first_server = run_login_server(first_opts)?;
|
||||
@@ -574,7 +574,7 @@ async fn cancels_previous_login_server_when_port_is_in_use() -> Result<()> {
|
||||
force_state: Some("cancel_state_2".to_string()),
|
||||
forced_chatgpt_workspace_id: None,
|
||||
codex_streamlined_login: false,
|
||||
cli_auth_keyring_backend_kind: CliAuthKeyringBackendKind::Direct,
|
||||
auth_keyring_backend_kind: AuthKeyringBackendKind::Direct,
|
||||
};
|
||||
|
||||
let second_server = run_login_server(second_opts)?;
|
||||
|
||||
@@ -4,9 +4,9 @@ use base64::Engine;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthDotJson;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::REVOKE_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
use codex_login::logout_with_revoke;
|
||||
use codex_login::save_auth;
|
||||
@@ -51,13 +51,13 @@ async fn logout_with_revoke_revokes_refresh_token_then_removes_auth() -> Result<
|
||||
codex_home.path(),
|
||||
&chatgpt_auth(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let removed = logout_with_revoke(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -109,13 +109,13 @@ async fn logout_with_revoke_removes_auth_when_revoke_fails() -> Result<()> {
|
||||
codex_home.path(),
|
||||
&chatgpt_auth(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let removed = logout_with_revoke(
|
||||
codex_home.path(),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -150,21 +150,21 @@ async fn auth_manager_logout_with_revoke_uses_cached_auth() -> Result<()> {
|
||||
codex_home.path(),
|
||||
&chatgpt_auth_with_refresh_token(REFRESH_TOKEN),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
let manager = AuthManager::new(
|
||||
codex_home.path().to_path_buf(),
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await;
|
||||
save_auth(
|
||||
codex_home.path(),
|
||||
&chatgpt_auth_with_refresh_token("newer-disk-refresh-token"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let removed = manager.logout_with_revoke().await?;
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::ModelsManagerConfig;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_login::AuthCredentialsStoreMode;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CliAuthKeyringBackendKind;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::ExternalAuth;
|
||||
use codex_login::ExternalAuthRefreshContext;
|
||||
@@ -225,7 +225,7 @@ c2ln",
|
||||
codex_home,
|
||||
AuthCredentialsStoreMode::File,
|
||||
/*chatgpt_base_url*/ None,
|
||||
CliAuthKeyringBackendKind::default(),
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect("auth should load")
|
||||
|
||||
@@ -21,6 +21,7 @@ codex-config = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-keyring-store = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-secrets = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::oauth::StoredOAuthTokenStatus;
|
||||
use crate::oauth::oauth_token_status;
|
||||
use crate::utils::apply_default_headers;
|
||||
use crate::utils::build_default_headers;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
|
||||
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -35,6 +36,7 @@ pub async fn determine_streamable_http_auth_status(
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Result<McpAuthStatus> {
|
||||
if bearer_token_env_var.is_some() {
|
||||
return Ok(McpAuthStatus::BearerToken);
|
||||
@@ -45,7 +47,7 @@ pub async fn determine_streamable_http_auth_status(
|
||||
return Ok(McpAuthStatus::BearerToken);
|
||||
}
|
||||
|
||||
match oauth_token_status(server_name, url, store_mode)? {
|
||||
match oauth_token_status(server_name, url, store_mode, keyring_backend_kind)? {
|
||||
StoredOAuthTokenStatus::Usable => return Ok(McpAuthStatus::OAuth),
|
||||
StoredOAuthTokenStatus::AuthorizationRequired => {
|
||||
return Ok(McpAuthStatus::NotLoggedIn);
|
||||
@@ -288,6 +290,7 @@ mod tests {
|
||||
)])),
|
||||
/*env_http_headers*/ None,
|
||||
OAuthCredentialsStoreMode::Keyring,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect("status should compute");
|
||||
@@ -309,6 +312,7 @@ mod tests {
|
||||
"CODEX_RMCP_CLIENT_AUTH_STATUS_TEST_TOKEN".to_string(),
|
||||
)])),
|
||||
OAuthCredentialsStoreMode::Keyring,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)
|
||||
.await
|
||||
.expect("status should compute");
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Error;
|
||||
use anyhow::Result;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_secrets::LocalSecretsBackend;
|
||||
use codex_secrets::SecretName;
|
||||
use codex_secrets::SecretScope;
|
||||
use oauth2::AccessToken;
|
||||
use oauth2::RefreshToken;
|
||||
use oauth2::Scope;
|
||||
@@ -51,6 +55,7 @@ use tokio::sync::Mutex;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
|
||||
const KEYRING_SERVICE: &str = "Codex MCP Credentials";
|
||||
const MCP_OAUTH_SECRET_PREFIX: &str = "MCP_OAUTH";
|
||||
const REFRESH_SKEW_MILLIS: u64 = 30_000;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -87,15 +92,19 @@ pub(crate) fn load_oauth_tokens(
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Result<Option<StoredOAuthTokens>> {
|
||||
let keyring_store = DefaultKeyringStore;
|
||||
match store_mode {
|
||||
OAuthCredentialsStoreMode::Auto => {
|
||||
load_oauth_tokens_from_keyring_with_fallback_to_file(&keyring_store, server_name, url)
|
||||
}
|
||||
OAuthCredentialsStoreMode::Auto => load_oauth_tokens_from_keyring_with_fallback_to_file(
|
||||
&keyring_store,
|
||||
keyring_backend_kind,
|
||||
server_name,
|
||||
url,
|
||||
),
|
||||
OAuthCredentialsStoreMode::File => load_oauth_tokens_from_file(server_name, url),
|
||||
OAuthCredentialsStoreMode::Keyring => {
|
||||
load_oauth_tokens_from_keyring(&keyring_store, server_name, url)
|
||||
load_oauth_tokens_from_keyring(&keyring_store, keyring_backend_kind, server_name, url)
|
||||
.with_context(|| "failed to read OAuth tokens from keyring".to_string())
|
||||
}
|
||||
}
|
||||
@@ -105,9 +114,10 @@ pub(crate) fn oauth_token_status(
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Result<StoredOAuthTokenStatus> {
|
||||
Ok(
|
||||
match load_oauth_tokens(server_name, url, store_mode)?.as_ref() {
|
||||
match load_oauth_tokens(server_name, url, store_mode, keyring_backend_kind)?.as_ref() {
|
||||
None => StoredOAuthTokenStatus::Missing,
|
||||
Some(tokens) if oauth_tokens_are_usable(tokens) => StoredOAuthTokenStatus::Usable,
|
||||
Some(_) => StoredOAuthTokenStatus::AuthorizationRequired,
|
||||
@@ -152,12 +162,13 @@ fn refresh_expires_in_from_timestamp(tokens: &mut StoredOAuthTokens) {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_oauth_tokens_from_keyring_with_fallback_to_file<K: KeyringStore>(
|
||||
fn load_oauth_tokens_from_keyring_with_fallback_to_file<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<Option<StoredOAuthTokens>> {
|
||||
match load_oauth_tokens_from_keyring(keyring_store, server_name, url) {
|
||||
match load_oauth_tokens_from_keyring(keyring_store, keyring_backend_kind, server_name, url) {
|
||||
Ok(Some(tokens)) => Ok(Some(tokens)),
|
||||
Ok(None) => load_oauth_tokens_from_file(server_name, url),
|
||||
Err(error) => {
|
||||
@@ -168,7 +179,23 @@ fn load_oauth_tokens_from_keyring_with_fallback_to_file<K: KeyringStore>(
|
||||
}
|
||||
}
|
||||
|
||||
fn load_oauth_tokens_from_keyring<K: KeyringStore>(
|
||||
fn load_oauth_tokens_from_keyring<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<Option<StoredOAuthTokens>> {
|
||||
match keyring_backend_kind {
|
||||
AuthKeyringBackendKind::Direct => {
|
||||
load_oauth_tokens_from_direct_keyring(keyring_store, server_name, url)
|
||||
}
|
||||
AuthKeyringBackendKind::Secrets => {
|
||||
load_oauth_tokens_from_secrets_keyring(keyring_store, server_name, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_oauth_tokens_from_direct_keyring<K: KeyringStore>(
|
||||
keyring_store: &K,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
@@ -186,26 +213,70 @@ fn load_oauth_tokens_from_keyring<K: KeyringStore>(
|
||||
}
|
||||
}
|
||||
|
||||
fn load_oauth_tokens_from_secrets_keyring<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<Option<StoredOAuthTokens>> {
|
||||
let codex_home = find_codex_home()?;
|
||||
let backend =
|
||||
LocalSecretsBackend::new(codex_home.to_path_buf(), Arc::new(keyring_store.clone()));
|
||||
let secret_name = compute_secret_name(server_name, url)?;
|
||||
match backend
|
||||
.get(&SecretScope::Global, &secret_name)
|
||||
.context("failed to load MCP OAuth tokens from encrypted storage")?
|
||||
{
|
||||
Some(serialized) => {
|
||||
let mut tokens: StoredOAuthTokens = serde_json::from_str(&serialized)
|
||||
.context("failed to deserialize OAuth tokens from encrypted storage")?;
|
||||
refresh_expires_in_from_timestamp(&mut tokens);
|
||||
Ok(Some(tokens))
|
||||
}
|
||||
None => load_oauth_tokens_from_direct_keyring(keyring_store, server_name, url),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_oauth_tokens(
|
||||
server_name: &str,
|
||||
tokens: &StoredOAuthTokens,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Result<()> {
|
||||
let keyring_store = DefaultKeyringStore;
|
||||
match store_mode {
|
||||
OAuthCredentialsStoreMode::Auto => save_oauth_tokens_with_keyring_with_fallback_to_file(
|
||||
&keyring_store,
|
||||
keyring_backend_kind,
|
||||
server_name,
|
||||
tokens,
|
||||
),
|
||||
OAuthCredentialsStoreMode::File => save_oauth_tokens_to_file(tokens),
|
||||
OAuthCredentialsStoreMode::Keyring => {
|
||||
save_oauth_tokens_with_keyring(&keyring_store, server_name, tokens)
|
||||
OAuthCredentialsStoreMode::Keyring => save_oauth_tokens_with_keyring(
|
||||
&keyring_store,
|
||||
keyring_backend_kind,
|
||||
server_name,
|
||||
tokens,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_oauth_tokens_with_keyring<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
server_name: &str,
|
||||
tokens: &StoredOAuthTokens,
|
||||
) -> Result<()> {
|
||||
match keyring_backend_kind {
|
||||
AuthKeyringBackendKind::Direct => {
|
||||
save_oauth_tokens_to_direct_keyring(keyring_store, server_name, tokens)
|
||||
}
|
||||
AuthKeyringBackendKind::Secrets => {
|
||||
save_oauth_tokens_to_secrets_keyring(keyring_store, server_name, tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_oauth_tokens_with_keyring<K: KeyringStore>(
|
||||
fn save_oauth_tokens_to_direct_keyring<K: KeyringStore>(
|
||||
keyring_store: &K,
|
||||
server_name: &str,
|
||||
tokens: &StoredOAuthTokens,
|
||||
@@ -231,12 +302,37 @@ fn save_oauth_tokens_with_keyring<K: KeyringStore>(
|
||||
}
|
||||
}
|
||||
|
||||
fn save_oauth_tokens_with_keyring_with_fallback_to_file<K: KeyringStore>(
|
||||
fn save_oauth_tokens_to_secrets_keyring<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
server_name: &str,
|
||||
tokens: &StoredOAuthTokens,
|
||||
) -> Result<()> {
|
||||
match save_oauth_tokens_with_keyring(keyring_store, server_name, tokens) {
|
||||
let serialized = serde_json::to_string(tokens).context("failed to serialize OAuth tokens")?;
|
||||
let codex_home = find_codex_home()?;
|
||||
let backend =
|
||||
LocalSecretsBackend::new(codex_home.to_path_buf(), Arc::new(keyring_store.clone()));
|
||||
let secret_name = compute_secret_name(server_name, &tokens.url)?;
|
||||
backend
|
||||
.set(&SecretScope::Global, &secret_name, &serialized)
|
||||
.context("failed to write OAuth tokens to encrypted storage")?;
|
||||
|
||||
let key = compute_store_key(server_name, &tokens.url)?;
|
||||
if let Err(error) = delete_oauth_tokens_from_file(&key) {
|
||||
warn!("failed to remove OAuth tokens from fallback storage: {error:?}");
|
||||
}
|
||||
if let Err(error) = keyring_store.delete(KEYRING_SERVICE, &key) {
|
||||
warn!("failed to remove legacy OAuth tokens from direct keyring storage: {error:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_oauth_tokens_with_keyring_with_fallback_to_file<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
server_name: &str,
|
||||
tokens: &StoredOAuthTokens,
|
||||
) -> Result<()> {
|
||||
match save_oauth_tokens_with_keyring(keyring_store, keyring_backend_kind, server_name, tokens) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => {
|
||||
let message = error.to_string();
|
||||
@@ -251,28 +347,36 @@ pub fn delete_oauth_tokens(
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
) -> Result<bool> {
|
||||
let keyring_store = DefaultKeyringStore;
|
||||
delete_oauth_tokens_from_keyring_and_file(&keyring_store, store_mode, server_name, url)
|
||||
delete_oauth_tokens_from_keyring_and_file(
|
||||
&keyring_store,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
server_name,
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
||||
fn delete_oauth_tokens_from_keyring_and_file<K: KeyringStore>(
|
||||
fn delete_oauth_tokens_from_keyring_and_file<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<bool> {
|
||||
let key = compute_store_key(server_name, url)?;
|
||||
let keyring_result = keyring_store.delete(KEYRING_SERVICE, &key);
|
||||
let keyring_result =
|
||||
delete_oauth_tokens_from_keyring(keyring_store, keyring_backend_kind, server_name, url);
|
||||
let keyring_removed = match keyring_result {
|
||||
Ok(removed) => removed,
|
||||
Err(error) => {
|
||||
let message = error.message();
|
||||
let message = error.to_string();
|
||||
warn!("failed to delete OAuth tokens from keyring: {message}");
|
||||
match store_mode {
|
||||
OAuthCredentialsStoreMode::Auto | OAuthCredentialsStoreMode::Keyring => {
|
||||
return Err(error.into_error())
|
||||
.context("failed to delete OAuth tokens from keyring");
|
||||
return Err(error).context("failed to delete OAuth tokens from keyring");
|
||||
}
|
||||
OAuthCredentialsStoreMode::File => false,
|
||||
}
|
||||
@@ -283,6 +387,49 @@ fn delete_oauth_tokens_from_keyring_and_file<K: KeyringStore>(
|
||||
Ok(keyring_removed || file_removed)
|
||||
}
|
||||
|
||||
fn delete_oauth_tokens_from_keyring<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<bool> {
|
||||
match keyring_backend_kind {
|
||||
AuthKeyringBackendKind::Direct => {
|
||||
delete_oauth_tokens_from_direct_keyring(keyring_store, server_name, url)
|
||||
}
|
||||
AuthKeyringBackendKind::Secrets => {
|
||||
delete_oauth_tokens_from_secrets_keyring(keyring_store, server_name, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_oauth_tokens_from_direct_keyring<K: KeyringStore>(
|
||||
keyring_store: &K,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<bool> {
|
||||
let key = compute_store_key(server_name, url)?;
|
||||
keyring_store
|
||||
.delete(KEYRING_SERVICE, &key)
|
||||
.map_err(|error| Error::new(error.into_error()))
|
||||
}
|
||||
|
||||
fn delete_oauth_tokens_from_secrets_keyring<K: KeyringStore + Clone + 'static>(
|
||||
keyring_store: &K,
|
||||
server_name: &str,
|
||||
url: &str,
|
||||
) -> Result<bool> {
|
||||
let codex_home = find_codex_home()?;
|
||||
let backend =
|
||||
LocalSecretsBackend::new(codex_home.to_path_buf(), Arc::new(keyring_store.clone()));
|
||||
let secret_name = compute_secret_name(server_name, url)?;
|
||||
let secrets_removed = backend
|
||||
.delete(&SecretScope::Global, &secret_name)
|
||||
.context("failed to delete OAuth tokens from encrypted storage")?;
|
||||
let direct_removed = delete_oauth_tokens_from_direct_keyring(keyring_store, server_name, url)?;
|
||||
Ok(secrets_removed || direct_removed)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct OAuthPersistor {
|
||||
inner: Arc<OAuthPersistorInner>,
|
||||
@@ -293,6 +440,7 @@ struct OAuthPersistorInner {
|
||||
url: String,
|
||||
authorization_manager: Arc<Mutex<AuthorizationManager>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
last_credentials: Mutex<Option<StoredOAuthTokens>>,
|
||||
}
|
||||
|
||||
@@ -302,6 +450,7 @@ impl OAuthPersistor {
|
||||
url: String,
|
||||
authorization_manager: Arc<Mutex<AuthorizationManager>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
initial_credentials: Option<StoredOAuthTokens>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -310,6 +459,7 @@ impl OAuthPersistor {
|
||||
url,
|
||||
authorization_manager,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
last_credentials: Mutex::new(initial_credentials),
|
||||
}),
|
||||
}
|
||||
@@ -349,7 +499,12 @@ impl OAuthPersistor {
|
||||
expires_at,
|
||||
};
|
||||
if last_credentials.as_ref() != Some(&stored) {
|
||||
save_oauth_tokens(&self.inner.server_name, &stored, self.inner.store_mode)?;
|
||||
save_oauth_tokens(
|
||||
&self.inner.server_name,
|
||||
&stored,
|
||||
self.inner.store_mode,
|
||||
self.inner.keyring_backend_kind,
|
||||
)?;
|
||||
*last_credentials = Some(stored);
|
||||
}
|
||||
}
|
||||
@@ -360,6 +515,7 @@ impl OAuthPersistor {
|
||||
&self.inner.server_name,
|
||||
&self.inner.url,
|
||||
self.inner.store_mode,
|
||||
self.inner.keyring_backend_kind,
|
||||
)
|
||||
{
|
||||
warn!(
|
||||
@@ -561,6 +717,15 @@ fn compute_store_key(server_name: &str, server_url: &str) -> Result<String> {
|
||||
Ok(format!("{server_name}|{truncated}"))
|
||||
}
|
||||
|
||||
fn compute_secret_name(server_name: &str, server_url: &str) -> Result<SecretName> {
|
||||
let key = compute_store_key(server_name, server_url)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(key.as_bytes());
|
||||
let digest = hasher.finalize();
|
||||
let hex = format!("{digest:X}");
|
||||
SecretName::new(&format!("{MCP_OAUTH_SECRET_PREFIX}_{}", &hex[..32]))
|
||||
}
|
||||
|
||||
fn fallback_file_path() -> Result<PathBuf> {
|
||||
Ok(find_codex_home()?.join(FALLBACK_FILENAME).to_path_buf())
|
||||
}
|
||||
@@ -629,8 +794,10 @@ fn sha_256_prefix(value: &Value) -> Result<String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use codex_secrets::compute_keyring_account;
|
||||
use keyring::Error as KeyringError;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::MutexGuard;
|
||||
use std::sync::OnceLock;
|
||||
@@ -660,6 +827,10 @@ mod tests {
|
||||
_dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &std::path::Path {
|
||||
self._dir.path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempCodexHome {
|
||||
@@ -680,9 +851,13 @@ mod tests {
|
||||
let key = super::compute_store_key(&tokens.server_name, &tokens.url)?;
|
||||
store.save(KEYRING_SERVICE, &key, &serialized)?;
|
||||
|
||||
let loaded =
|
||||
super::load_oauth_tokens_from_keyring(&store, &tokens.server_name, &tokens.url)?
|
||||
.expect("tokens should load from keyring");
|
||||
let loaded = super::load_oauth_tokens_from_keyring(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?
|
||||
.expect("tokens should load from keyring");
|
||||
assert_tokens_match_without_expiry(&loaded, &expected);
|
||||
Ok(())
|
||||
}
|
||||
@@ -698,6 +873,7 @@ mod tests {
|
||||
|
||||
let loaded = super::load_oauth_tokens_from_keyring_with_fallback_to_file(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?
|
||||
@@ -719,6 +895,7 @@ mod tests {
|
||||
|
||||
let loaded = super::load_oauth_tokens_from_keyring_with_fallback_to_file(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?
|
||||
@@ -738,6 +915,7 @@ mod tests {
|
||||
|
||||
super::save_oauth_tokens_with_keyring_with_fallback_to_file(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens,
|
||||
)?;
|
||||
@@ -759,6 +937,7 @@ mod tests {
|
||||
|
||||
super::save_oauth_tokens_with_keyring_with_fallback_to_file(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens,
|
||||
)?;
|
||||
@@ -779,6 +958,114 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_oauth_tokens_with_secrets_backend_writes_encrypted_storage() -> Result<()> {
|
||||
let env = TempCodexHome::new();
|
||||
let store = MockKeyringStore::default();
|
||||
let tokens = sample_tokens();
|
||||
let key = super::compute_store_key(&tokens.server_name, &tokens.url)?;
|
||||
let serialized = serde_json::to_string(&tokens)?;
|
||||
store.save(KEYRING_SERVICE, &key, &serialized)?;
|
||||
super::save_oauth_tokens_to_file(&tokens)?;
|
||||
|
||||
super::save_oauth_tokens_with_keyring_with_fallback_to_file(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
&tokens.server_name,
|
||||
&tokens,
|
||||
)?;
|
||||
|
||||
let backend = LocalSecretsBackend::new(env.path().to_path_buf(), Arc::new(store.clone()));
|
||||
let secret_name = super::compute_secret_name(&tokens.server_name, &tokens.url)?;
|
||||
let stored = backend
|
||||
.get(&SecretScope::Global, &secret_name)?
|
||||
.expect("tokens should be saved to encrypted storage");
|
||||
assert_eq!(serde_json::from_str::<StoredOAuthTokens>(&stored)?, tokens);
|
||||
assert!(store.saved_value(&key).is_none());
|
||||
assert!(!super::fallback_file_path()?.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_oauth_tokens_with_secrets_backend_reads_legacy_direct_entry() -> Result<()> {
|
||||
let _env = TempCodexHome::new();
|
||||
let store = MockKeyringStore::default();
|
||||
let tokens = sample_tokens();
|
||||
let expected = tokens.clone();
|
||||
let key = super::compute_store_key(&tokens.server_name, &tokens.url)?;
|
||||
let serialized = serde_json::to_string(&tokens)?;
|
||||
store.save(KEYRING_SERVICE, &key, &serialized)?;
|
||||
|
||||
let loaded = super::load_oauth_tokens_from_keyring(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?
|
||||
.expect("tokens should load from legacy direct keyring storage");
|
||||
|
||||
assert_tokens_match_without_expiry(&loaded, &expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_oauth_tokens_with_secrets_backend_falls_back_to_file_when_keyring_fails() -> Result<()>
|
||||
{
|
||||
let env = TempCodexHome::new();
|
||||
let store = MockKeyringStore::default();
|
||||
store.set_error(
|
||||
&compute_keyring_account(env.path()),
|
||||
KeyringError::Invalid("error".into(), "save".into()),
|
||||
);
|
||||
let tokens = sample_tokens();
|
||||
|
||||
super::save_oauth_tokens_with_keyring_with_fallback_to_file(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
&tokens.server_name,
|
||||
&tokens,
|
||||
)?;
|
||||
|
||||
let saved = super::read_fallback_file()?.expect("fallback file should load");
|
||||
let key = super::compute_store_key(&tokens.server_name, &tokens.url)?;
|
||||
assert!(saved.contains_key(&key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_oauth_tokens_with_secrets_backend_removes_secrets_direct_and_file() -> Result<()> {
|
||||
let env = TempCodexHome::new();
|
||||
let store = MockKeyringStore::default();
|
||||
let tokens = sample_tokens();
|
||||
let serialized = serde_json::to_string(&tokens)?;
|
||||
let key = super::compute_store_key(&tokens.server_name, &tokens.url)?;
|
||||
store.save(KEYRING_SERVICE, &key, &serialized)?;
|
||||
super::save_oauth_tokens_with_keyring(
|
||||
&store,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
&tokens.server_name,
|
||||
&tokens,
|
||||
)?;
|
||||
store.save(KEYRING_SERVICE, &key, &serialized)?;
|
||||
super::save_oauth_tokens_to_file(&tokens)?;
|
||||
|
||||
let removed = super::delete_oauth_tokens_from_keyring_and_file(
|
||||
&store,
|
||||
OAuthCredentialsStoreMode::Auto,
|
||||
AuthKeyringBackendKind::Secrets,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?;
|
||||
|
||||
let backend = LocalSecretsBackend::new(env.path().to_path_buf(), Arc::new(store.clone()));
|
||||
let secret_name = super::compute_secret_name(&tokens.server_name, &tokens.url)?;
|
||||
assert!(removed);
|
||||
assert!(backend.get(&SecretScope::Global, &secret_name)?.is_none());
|
||||
assert!(store.saved_value(&key).is_none());
|
||||
assert!(!super::fallback_file_path()?.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_oauth_tokens_removes_all_storage() -> Result<()> {
|
||||
let _env = TempCodexHome::new();
|
||||
@@ -792,6 +1079,7 @@ mod tests {
|
||||
let removed = super::delete_oauth_tokens_from_keyring_and_file(
|
||||
&store,
|
||||
OAuthCredentialsStoreMode::Auto,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?;
|
||||
@@ -814,6 +1102,7 @@ mod tests {
|
||||
let removed = super::delete_oauth_tokens_from_keyring_and_file(
|
||||
&store,
|
||||
OAuthCredentialsStoreMode::Auto,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
)?;
|
||||
@@ -835,6 +1124,7 @@ mod tests {
|
||||
let result = super::delete_oauth_tokens_from_keyring_and_file(
|
||||
&store,
|
||||
OAuthCredentialsStoreMode::Auto,
|
||||
AuthKeyringBackendKind::Direct,
|
||||
&tokens.server_name,
|
||||
&tokens.url,
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ use crate::oauth::compute_expires_at_millis;
|
||||
use crate::save_oauth_tokens;
|
||||
use crate::utils::apply_default_headers;
|
||||
use crate::utils::build_default_headers;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
|
||||
struct OauthHeaders {
|
||||
@@ -81,6 +82,7 @@ pub async fn perform_oauth_login(
|
||||
server_name: &str,
|
||||
server_url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
@@ -93,6 +95,7 @@ pub async fn perform_oauth_login(
|
||||
server_name,
|
||||
server_url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
scopes,
|
||||
@@ -110,6 +113,7 @@ pub async fn perform_oauth_login_silent(
|
||||
server_name: &str,
|
||||
server_url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
@@ -122,6 +126,7 @@ pub async fn perform_oauth_login_silent(
|
||||
server_name,
|
||||
server_url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
scopes,
|
||||
@@ -139,6 +144,7 @@ async fn perform_oauth_login_with_browser_output(
|
||||
server_name: &str,
|
||||
server_url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
@@ -156,6 +162,7 @@ async fn perform_oauth_login_with_browser_output(
|
||||
server_name,
|
||||
server_url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
headers,
|
||||
scopes,
|
||||
oauth_client_id,
|
||||
@@ -175,6 +182,7 @@ pub async fn perform_oauth_login_return_url(
|
||||
server_name: &str,
|
||||
server_url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
scopes: &[String],
|
||||
@@ -192,6 +200,7 @@ pub async fn perform_oauth_login_return_url(
|
||||
server_name,
|
||||
server_url,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
headers,
|
||||
scopes,
|
||||
oauth_client_id,
|
||||
@@ -350,6 +359,7 @@ struct OauthLoginFlow {
|
||||
server_name: String,
|
||||
server_url: String,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
launch_browser: bool,
|
||||
timeout: Duration,
|
||||
}
|
||||
@@ -445,6 +455,7 @@ impl OauthLoginFlow {
|
||||
server_name: &str,
|
||||
server_url: &str,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
headers: OauthHeaders,
|
||||
scopes: &[String],
|
||||
oauth_client_id: Option<&str>,
|
||||
@@ -508,6 +519,7 @@ impl OauthLoginFlow {
|
||||
server_name: server_name.to_string(),
|
||||
server_url: server_url.to_string(),
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
launch_browser,
|
||||
timeout,
|
||||
})
|
||||
@@ -571,7 +583,12 @@ impl OauthLoginFlow {
|
||||
token_response: WrappedOAuthTokenResponse(credentials),
|
||||
expires_at,
|
||||
};
|
||||
save_oauth_tokens(&self.server_name, &stored, self.store_mode)?;
|
||||
save_oauth_tokens(
|
||||
&self.server_name,
|
||||
&stored,
|
||||
self.store_mode,
|
||||
self.keyring_backend_kind,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ use std::time::Instant;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_client::build_reqwest_client_with_custom_ca;
|
||||
use codex_client::maybe_build_rustls_client_config_with_custom_ca;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_exec_server::HttpClient;
|
||||
use futures::FutureExt;
|
||||
@@ -124,6 +126,7 @@ enum TransportRecipe {
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
auth_provider: Option<SharedAuthProvider>,
|
||||
},
|
||||
@@ -371,6 +374,7 @@ impl RmcpClient {
|
||||
http_headers: Option<HashMap<String, String>>,
|
||||
env_http_headers: Option<HashMap<String, String>>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Result<Self> {
|
||||
@@ -381,6 +385,7 @@ impl RmcpClient {
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_client,
|
||||
auth_provider,
|
||||
};
|
||||
@@ -757,6 +762,7 @@ impl RmcpClient {
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
keyring_backend_kind,
|
||||
http_client,
|
||||
auth_provider,
|
||||
} => {
|
||||
@@ -767,7 +773,7 @@ impl RmcpClient {
|
||||
&& auth_provider.is_none()
|
||||
&& !default_headers.contains_key(AUTHORIZATION)
|
||||
{
|
||||
match load_oauth_tokens(server_name, url, *store_mode) {
|
||||
match load_oauth_tokens(server_name, url, *store_mode, *keyring_backend_kind) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(err) => {
|
||||
warn!("failed to read tokens for server `{server_name}`: {err}");
|
||||
@@ -784,6 +790,7 @@ impl RmcpClient {
|
||||
url,
|
||||
initial_tokens.clone(),
|
||||
*store_mode,
|
||||
*keyring_backend_kind,
|
||||
default_headers.clone(),
|
||||
Arc::clone(http_client),
|
||||
)
|
||||
@@ -1127,6 +1134,7 @@ async fn create_oauth_transport_and_runtime(
|
||||
url: &str,
|
||||
initial_tokens: StoredOAuthTokens,
|
||||
credentials_store: OAuthCredentialsStoreMode,
|
||||
keyring_backend_kind: AuthKeyringBackendKind,
|
||||
default_headers: HeaderMap,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
) -> Result<(
|
||||
@@ -1175,6 +1183,7 @@ async fn create_oauth_transport_and_runtime(
|
||||
url.to_string(),
|
||||
auth_manager,
|
||||
credentials_store,
|
||||
keyring_backend_kind,
|
||||
Some(initial_tokens),
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use codex_config::types::AuthKeyringBackendKind;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecServerClient;
|
||||
@@ -93,6 +94,7 @@ pub(crate) async fn create_client_with_http_client(
|
||||
/*http_headers*/ None,
|
||||
/*env_http_headers*/ None,
|
||||
OAuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
http_client,
|
||||
/*auth_provider*/ None,
|
||||
)
|
||||
@@ -136,6 +138,7 @@ pub(crate) async fn create_remote_client(
|
||||
/*http_headers*/ None,
|
||||
/*env_http_headers*/ None,
|
||||
OAuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
Arc::new(http_client),
|
||||
/*auth_provider*/ None,
|
||||
)
|
||||
|
||||
@@ -1227,7 +1227,7 @@ pub async fn run_main(
|
||||
if let Err(err) = enforce_login_restrictions(&AuthConfig {
|
||||
codex_home: config.codex_home.to_path_buf(),
|
||||
auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
|
||||
keyring_backend_kind: config.cli_auth_keyring_backend_kind(),
|
||||
keyring_backend_kind: config.auth_keyring_backend_kind(),
|
||||
forced_login_method: config.forced_login_method,
|
||||
forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
|
||||
chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
|
||||
|
||||
Reference in New Issue
Block a user