@@ -4,6 +4,8 @@ use serde::Deserialize;
44use serde:: Serialize ;
55#[ cfg( test) ]
66use serial_test:: serial;
7+ use sha2:: Digest ;
8+ use sha2:: Sha256 ;
79use std:: env;
810use std:: fmt:: Debug ;
911use std:: fs:: File ;
@@ -17,9 +19,9 @@ use std::path::PathBuf;
1719use std:: sync:: Arc ;
1820use std:: sync:: Mutex ;
1921use std:: time:: Duration ;
22+ use tracing:: warn;
2023
2124use codex_app_server_protocol:: AuthMode ;
22- use codex_keyring_store:: CredentialStoreError ;
2325use codex_keyring_store:: DefaultKeyringStore ;
2426use codex_keyring_store:: KeyringStore ;
2527use 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 ) ]
5267struct 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 ) ]
135139struct KeyringAuthStorage {
136140 codex_home : PathBuf ,
141+ keyring_store : Arc < dyn KeyringStore > ,
137142}
138143
139144impl 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
145196impl 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 ) ]
159227struct AutoAuthStorage {
160- codex_home : PathBuf ,
228+ keyring_storage : Arc < KeyringAuthStorage > ,
229+ file_storage : Arc < FileAuthStorage > ,
161230}
162231
163232impl 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
169241impl 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
182269fn 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 {
0 commit comments