Skip to content

Commit 1294543

Browse files
committed
new logic for keyring
1 parent 7d16894 commit 1294543

File tree

4 files changed

+135
-34
lines changed

4 files changed

+135
-34
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ reqwest = { workspace = true, features = ["json", "stream"] }
4545
serde = { workspace = true, features = ["derive"] }
4646
serde_json = { workspace = true }
4747
sha1 = { workspace = true }
48+
sha2 = { workspace = true }
4849
shlex = { workspace = true }
4950
similar = { workspace = true }
5051
strum_macros = { workspace = true }

codex-rs/core/src/auth.rs

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use serde::Deserialize;
44
use serde::Serialize;
55
#[cfg(test)]
66
use serial_test::serial;
7+
use sha2::Digest;
8+
use sha2::Sha256;
79
use std::env;
810
use std::fmt::Debug;
911
use std::fs::File;
@@ -17,9 +19,9 @@ use std::path::PathBuf;
1719
use std::sync::Arc;
1820
use std::sync::Mutex;
1921
use std::time::Duration;
22+
use tracing::warn;
2023

2124
use codex_app_server_protocol::AuthMode;
22-
use codex_keyring_store::CredentialStoreError;
2325
use codex_keyring_store::DefaultKeyringStore;
2426
use codex_keyring_store::KeyringStore;
2527
use codex_protocol::config_types::ForcedLoginMethod;
@@ -48,6 +50,19 @@ trait AuthStorageBackend: Debug + Send + Sync {
4850
fn delete(&self) -> std::io::Result<bool>;
4951
}
5052

53+
fn get_auth_file(codex_home: &Path) -> PathBuf {
54+
codex_home.join("auth.json")
55+
}
56+
57+
fn delete_file_if_exists(codex_home: &Path) -> std::io::Result<bool> {
58+
let auth_file = get_auth_file(codex_home);
59+
match std::fs::remove_file(&auth_file) {
60+
Ok(()) => Ok(true),
61+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
62+
Err(err) => Err(err),
63+
}
64+
}
65+
5166
#[derive(Clone, Debug)]
5267
struct FileAuthStorage {
5368
codex_home: PathBuf,
@@ -59,7 +74,7 @@ impl FileAuthStorage {
5974
}
6075

6176
fn load_from_file(&self) -> std::io::Result<Option<AuthDotJson>> {
62-
let auth_file = self.get_auth_file(&self.codex_home);
77+
let auth_file = get_auth_file(&self.codex_home);
6378
let auth_dot_json = match self.try_read_auth_json(&auth_file) {
6479
Ok(auth) => auth,
6580
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
@@ -69,16 +84,7 @@ impl FileAuthStorage {
6984
}
7085

7186
fn save_to_file(&self, auth: &AuthDotJson) -> std::io::Result<()> {
72-
self.write_auth_json(&self.get_auth_file(&self.codex_home), auth)
73-
}
74-
75-
fn delete_file_if_exists(&self) -> std::io::Result<bool> {
76-
let auth_file = self.get_auth_file(&self.codex_home);
77-
match std::fs::remove_file(&auth_file) {
78-
Ok(()) => Ok(true),
79-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
80-
Err(err) => Err(err),
81-
}
87+
self.write_auth_json(&get_auth_file(&self.codex_home), auth)
8288
}
8389

8490
/// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory.
@@ -92,10 +98,6 @@ impl FileAuthStorage {
9298
Ok(auth_dot_json)
9399
}
94100

95-
fn get_auth_file(&self, codex_home: &Path) -> PathBuf {
96-
codex_home.join("auth.json")
97-
}
98-
99101
fn write_auth_json(
100102
&self,
101103
auth_file: &Path,
@@ -127,66 +129,162 @@ impl AuthStorageBackend for FileAuthStorage {
127129
self.save_to_file(auth)
128130
}
129131
fn delete(&self) -> std::io::Result<bool> {
130-
self.delete_file_if_exists()
132+
delete_file_if_exists(&self.codex_home)
131133
}
132134
}
133135

136+
const KEYRING_SERVICE: &str = "Codex CLI Auth";
137+
134138
#[derive(Clone, Debug)]
135139
struct KeyringAuthStorage {
136140
codex_home: PathBuf,
141+
keyring_store: Arc<dyn KeyringStore>,
137142
}
138143

139144
impl KeyringAuthStorage {
140-
fn new(codex_home: PathBuf) -> Self {
141-
Self { codex_home }
145+
fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
146+
Self {
147+
codex_home,
148+
keyring_store,
149+
}
150+
}
151+
152+
// turns codex_home path into a stable, short key string
153+
fn compute_store_key(&self, codex_home: &Path) -> std::io::Result<String> {
154+
let canonical = codex_home
155+
.canonicalize()
156+
.unwrap_or_else(|_| codex_home.to_path_buf());
157+
let path_str = canonical.to_string_lossy();
158+
let mut hasher = Sha256::new();
159+
hasher.update(path_str.as_bytes());
160+
let digest = hasher.finalize();
161+
let hex = format!("{digest:x}");
162+
let truncated = hex.get(..16).unwrap_or(&hex);
163+
Ok(format!("cli|{truncated}"))
164+
}
165+
166+
fn load_from_keyring(&self, key: &str) -> std::io::Result<Option<AuthDotJson>> {
167+
match self.keyring_store.load(KEYRING_SERVICE, key) {
168+
Ok(Some(serialized)) => serde_json::from_str(&serialized).map(Some).map_err(|err| {
169+
std::io::Error::other(format!(
170+
"failed to deserialize CLI auth from keyring: {err}"
171+
))
172+
}),
173+
Ok(None) => Ok(None),
174+
Err(error) => Err(std::io::Error::other(format!(
175+
"failed to load CLI auth from keyring: {}",
176+
error.message()
177+
))),
178+
}
179+
}
180+
181+
fn save_to_keyring(&self, key: &str, value: &str) -> std::io::Result<()> {
182+
match self.keyring_store.save(KEYRING_SERVICE, key, value) {
183+
Ok(()) => Ok(()),
184+
Err(error) => {
185+
let message = format!(
186+
"failed to write OAuth tokens to keyring: {}",
187+
error.message()
188+
);
189+
warn!("{message}");
190+
Err(std::io::Error::other(message))
191+
}
192+
}
142193
}
143194
}
144195

145196
impl AuthStorageBackend for KeyringAuthStorage {
146197
fn load(&self) -> std::io::Result<Option<AuthDotJson>> {
147-
Ok(None)
198+
let key = self.compute_store_key(&self.codex_home)?;
199+
self.load_from_keyring(&key)
148200
}
149201

150202
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
203+
let key = self.compute_store_key(&self.codex_home)?;
204+
// Simpler error mapping per style: prefer method reference over closure
205+
let serialized = serde_json::to_string(auth).map_err(std::io::Error::other)?;
206+
self.save_to_keyring(&key, &serialized)?;
207+
if let Err(err) = delete_file_if_exists(&self.codex_home) {
208+
warn!("failed to remove CLI auth fallback file: {err}");
209+
}
151210
Ok(())
152211
}
212+
153213
fn delete(&self) -> std::io::Result<bool> {
154-
Ok(false)
214+
let key = self.compute_store_key(&self.codex_home)?;
215+
let keyring_removed = self
216+
.keyring_store
217+
.delete(KEYRING_SERVICE, &key)
218+
.map_err(|err| {
219+
std::io::Error::other(format!("failed to delete auth from keyring: {err}"))
220+
})?;
221+
let file_removed = delete_file_if_exists(&self.codex_home)?;
222+
Ok(keyring_removed || file_removed)
155223
}
156224
}
157225

158226
#[derive(Clone, Debug)]
159227
struct AutoAuthStorage {
160-
codex_home: PathBuf,
228+
keyring_storage: Arc<KeyringAuthStorage>,
229+
file_storage: Arc<FileAuthStorage>,
161230
}
162231

163232
impl AutoAuthStorage {
164-
fn new(codex_home: PathBuf) -> Self {
165-
Self { codex_home }
233+
fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
234+
Self {
235+
keyring_storage: Arc::new(KeyringAuthStorage::new(codex_home.clone(), keyring_store)),
236+
file_storage: Arc::new(FileAuthStorage::new(codex_home.clone())),
237+
}
166238
}
167239
}
168240

169241
impl AuthStorageBackend for AutoAuthStorage {
170242
fn load(&self) -> std::io::Result<Option<AuthDotJson>> {
171-
Ok(None)
243+
match self.keyring_storage.load() {
244+
Ok(Some(auth)) => Ok(Some(auth)),
245+
Ok(None) => self.file_storage.load(),
246+
Err(err) => {
247+
warn!("failed to load CLI auth from keyring, falling back to file storage: {err}");
248+
self.file_storage.load()
249+
}
250+
}
172251
}
173252

174253
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> {
175-
Ok(())
254+
match self.keyring_storage.save(auth) {
255+
Ok(()) => Ok(()),
256+
Err(err) => {
257+
warn!("failed to save auth to keyring, falling back to file storage: {err}");
258+
return self.file_storage.save(auth);
259+
}
260+
}
176261
}
262+
177263
fn delete(&self) -> std::io::Result<bool> {
178-
Ok(false)
264+
// Keyring storage will delete from disk as well
265+
self.keyring_storage.delete()
179266
}
180267
}
181268

182269
fn create_auth_storage(
183270
codex_home: PathBuf,
184271
mode: AuthCredentialsStoreMode,
272+
) -> Arc<dyn AuthStorageBackend> {
273+
let keyring_store: Arc<dyn KeyringStore> = Arc::new(DefaultKeyringStore);
274+
create_auth_storage_with_keyring_store(codex_home, mode, keyring_store)
275+
}
276+
277+
fn create_auth_storage_with_keyring_store(
278+
codex_home: PathBuf,
279+
mode: AuthCredentialsStoreMode,
280+
keyring_store: Arc<dyn KeyringStore>,
185281
) -> Arc<dyn AuthStorageBackend> {
186282
match mode {
187283
AuthCredentialsStoreMode::File => Arc::new(FileAuthStorage::new(codex_home)),
188-
AuthCredentialsStoreMode::Keyring => Arc::new(KeyringAuthStorage::new(codex_home)),
189-
AuthCredentialsStoreMode::Auto => Arc::new(AutoAuthStorage::new(codex_home)),
284+
AuthCredentialsStoreMode::Keyring => {
285+
Arc::new(KeyringAuthStorage::new(codex_home, keyring_store))
286+
}
287+
AuthCredentialsStoreMode::Auto => Arc::new(AutoAuthStorage::new(codex_home, keyring_store)),
190288
}
191289
}
192290

@@ -645,7 +743,7 @@ mod tests {
645743
)
646744
.expect("failed to write auth file");
647745

648-
let file = storage.get_auth_file(codex_home.path());
746+
let file = get_auth_file(codex_home.path());
649747
let auth_dot_json = storage.try_read_auth_json(&file).unwrap();
650748
storage.write_auth_json(&file, &auth_dot_json).unwrap();
651749

@@ -781,8 +879,7 @@ mod tests {
781879
}
782880

783881
fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result<String> {
784-
let storage = FileAuthStorage::new(codex_home.to_path_buf());
785-
let auth_file = storage.get_auth_file(codex_home);
882+
let auth_file = get_auth_file(codex_home);
786883
// Create a minimal valid JWT for the id_token field.
787884
#[derive(Serialize)]
788885
struct Header {

codex-rs/keyring-store/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Error as AnyhowError;
22
use keyring::Entry;
33
use std::error::Error;
44
use std::fmt;
5+
use std::fmt::Debug;
56

67
#[derive(Debug)]
78
pub struct CredentialStoreError(AnyhowError);
@@ -26,12 +27,13 @@ impl fmt::Display for CredentialStoreError {
2627
impl Error for CredentialStoreError {}
2728

2829
/// Shared credential store abstraction for keyring-backed implementations.
29-
pub trait KeyringStore: Send + Sync {
30+
pub trait KeyringStore: Debug + Send + Sync {
3031
fn load(&self, service: &str, account: &str) -> Result<Option<String>, CredentialStoreError>;
3132
fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError>;
3233
fn delete(&self, service: &str, account: &str) -> Result<bool, CredentialStoreError>;
3334
}
3435

36+
#[derive(Debug)]
3537
pub struct DefaultKeyringStore;
3638

3739
impl KeyringStore for DefaultKeyringStore {
@@ -69,7 +71,7 @@ pub mod testing {
6971
use std::sync::Arc;
7072
use std::sync::Mutex;
7173

72-
#[derive(Default, Clone)]
74+
#[derive(Default, Clone, Debug)]
7375
pub struct MockKeyringStore {
7476
credentials: Arc<Mutex<HashMap<String, Arc<MockCredential>>>>,
7577
}

0 commit comments

Comments
 (0)