1use crate::env_api_keys;
7use chrono::Utc;
8use parking_lot::RwLock;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct OAuthTokenInfo {
15 pub access_token: String,
17 pub refresh_token: Option<String>,
19 pub expires_at: i64,
21 pub token_type: String,
23}
24
25impl OAuthTokenInfo {
26 pub fn is_expired(&self) -> bool {
28 let now = Utc::now().timestamp();
29 now >= self.expires_at
30 }
31
32 pub fn needs_refresh(&self) -> bool {
34 let now = Utc::now().timestamp();
35 now >= (self.expires_at - 300) }
37
38 pub fn new(access_token: String, refresh_token: Option<String>, expires_in_secs: i64) -> Self {
40 let now = Utc::now().timestamp();
41 Self {
42 access_token,
43 refresh_token,
44 expires_at: now + expires_in_secs,
45 token_type: "Bearer".to_string(),
46 }
47 }
48}
49
50pub trait ProviderAuth: Send + Sync {
52 fn provider_name(&self) -> &str;
54
55 fn is_configured(&self) -> bool;
57
58 fn get_api_key(&self) -> Option<String>;
60
61 fn needs_oauth_refresh(&self) -> bool;
63
64 fn get_oauth_token(&self) -> Option<OAuthTokenInfo>;
66
67 fn set_oauth_token(&mut self, token: OAuthTokenInfo);
69
70 fn set_api_key(&mut self, api_key: String);
72}
73
74#[derive(Debug, Clone)]
76pub struct ApiKeyAuth {
77 api_key: Option<String>,
78 source: AuthSource,
79}
80
81#[derive(Debug, Clone, PartialEq)]
82pub enum AuthSource {
84 Stored,
86 Runtime,
88 Environment,
90 Ambient,
92}
93
94impl ApiKeyAuth {
95 pub fn new(api_key: Option<String>, source: AuthSource) -> Self {
97 Self { api_key, source }
98 }
99}
100
101impl ProviderAuth for ApiKeyAuth {
102 fn provider_name(&self) -> &str {
103 "api_key"
104 }
105
106 fn is_configured(&self) -> bool {
107 self.api_key.is_some()
108 }
109
110 fn get_api_key(&self) -> Option<String> {
111 self.api_key.clone()
112 }
113
114 fn needs_oauth_refresh(&self) -> bool {
115 false
116 }
117
118 fn get_oauth_token(&self) -> Option<OAuthTokenInfo> {
119 None
120 }
121
122 fn set_oauth_token(&mut self, _token: OAuthTokenInfo) {
123 }
125
126 fn set_api_key(&mut self, api_key: String) {
127 self.api_key = Some(api_key);
128 self.source = AuthSource::Stored;
129 }
130}
131
132pub struct OAuthAuth {
134 provider_name: String,
135 token: Option<OAuthTokenInfo>,
136 #[allow(clippy::type_complexity)]
137 on_refresh: Option<Box<dyn Fn(&OAuthTokenInfo) + Send + Sync>>,
138}
139
140impl OAuthAuth {
141 pub fn new(provider_name: &str) -> Self {
143 Self {
144 provider_name: provider_name.to_string(),
145 token: None,
146 on_refresh: None,
147 }
148 }
149
150 pub fn with_token(provider_name: &str, token: OAuthTokenInfo) -> Self {
152 Self {
153 provider_name: provider_name.to_string(),
154 token: Some(token),
155 on_refresh: None,
156 }
157 }
158
159 pub fn on_token_refresh<F>(&mut self, callback: F)
161 where
162 F: Fn(&OAuthTokenInfo) + Send + Sync + 'static,
163 {
164 self.on_refresh = Some(Box::new(callback));
165 }
166}
167
168impl ProviderAuth for OAuthAuth {
169 fn provider_name(&self) -> &str {
170 &self.provider_name
171 }
172
173 fn is_configured(&self) -> bool {
174 self.token.is_some()
175 }
176
177 fn get_api_key(&self) -> Option<String> {
178 self.token.as_ref().map(|t| t.access_token.clone())
179 }
180
181 fn needs_oauth_refresh(&self) -> bool {
182 self.token
183 .as_ref()
184 .map(|t| t.needs_refresh())
185 .unwrap_or(true)
186 }
187
188 fn get_oauth_token(&self) -> Option<OAuthTokenInfo> {
189 self.token.clone()
190 }
191
192 fn set_oauth_token(&mut self, token: OAuthTokenInfo) {
193 if let Some(ref callback) = self.on_refresh {
194 callback(&token);
195 }
196 self.token = Some(token);
197 }
198
199 fn set_api_key(&mut self, _api_key: String) {
200 }
202}
203
204pub struct AmbientAuth {
206 provider_name: String,
207 check_fn: Box<dyn Fn() -> bool + Send + Sync>,
208}
209
210impl AmbientAuth {
211 pub fn new<F>(provider_name: &str, check_fn: F) -> Self
213 where
214 F: Fn() -> bool + Send + Sync + 'static,
215 {
216 Self {
217 provider_name: provider_name.to_string(),
218 check_fn: Box::new(check_fn),
219 }
220 }
221}
222
223impl ProviderAuth for AmbientAuth {
224 fn provider_name(&self) -> &str {
225 &self.provider_name
226 }
227
228 fn is_configured(&self) -> bool {
229 (self.check_fn)()
230 }
231
232 fn get_api_key(&self) -> Option<String> {
233 if (self.check_fn)() {
234 Some("<authenticated>".to_string())
235 } else {
236 None
237 }
238 }
239
240 fn needs_oauth_refresh(&self) -> bool {
241 false
242 }
243
244 fn get_oauth_token(&self) -> Option<OAuthTokenInfo> {
245 None
246 }
247
248 fn set_oauth_token(&mut self, _token: OAuthTokenInfo) {
249 }
251
252 fn set_api_key(&mut self, _api_key: String) {
253 }
255}
256
257pub struct ProviderAuthRegistry {
269 providers: HashMap<String, Box<dyn ProviderAuth>>,
270 runtime_overrides: RwLock<HashMap<String, String>>,
271 #[allow(clippy::type_complexity)]
272 fallback_resolver: RwLock<Option<Box<dyn Fn(&str) -> Option<String> + Send + Sync>>>,
273}
274
275impl Default for ProviderAuthRegistry {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281impl ProviderAuthRegistry {
282 pub fn new() -> Self {
284 Self {
285 providers: HashMap::new(),
286 runtime_overrides: RwLock::new(HashMap::new()),
287 fallback_resolver: RwLock::new(None),
288 }
289 }
290
291 pub fn with_defaults() -> Self {
293 let mut registry = Self::new();
294 registry.register_defaults();
295 registry
296 }
297
298 pub fn register_defaults(&mut self) {
300 self.register_ambient("vertex", env_api_keys::has_vertex_adc_full);
302 self.register_ambient("google-vertex", env_api_keys::has_vertex_adc_full);
303 self.register_ambient("bedrock", env_api_keys::has_bedrock_creds);
304 self.register_ambient("amazon-bedrock", env_api_keys::has_bedrock_creds);
305 self.register_ambient("aws-bedrock", env_api_keys::has_bedrock_creds);
306 }
307
308 pub fn register_api_key(&mut self, provider: &str, api_key: Option<String>) {
310 self.providers.insert(
311 provider.to_string(),
312 Box::new(ApiKeyAuth::new(api_key, AuthSource::Stored)),
313 );
314 }
315
316 pub fn register_oauth(&mut self, provider: &str, token: OAuthTokenInfo) {
318 self.providers.insert(
319 provider.to_string(),
320 Box::new(OAuthAuth::with_token(provider, token)),
321 );
322 }
323
324 pub fn register_ambient<F>(&mut self, provider: &str, check_fn: F)
326 where
327 F: Fn() -> bool + Send + Sync + 'static,
328 {
329 self.providers.insert(
330 provider.to_string(),
331 Box::new(AmbientAuth::new(provider, check_fn)),
332 );
333 }
334
335 pub fn register<P: ProviderAuth + 'static>(&mut self, provider: &str, auth: P) {
337 self.providers.insert(provider.to_string(), Box::new(auth));
338 }
339
340 pub fn set_runtime_key(&self, provider: &str, api_key: String) {
342 self.runtime_overrides
343 .write()
344 .insert(provider.to_string(), api_key);
345 }
346
347 pub fn remove_runtime_key(&self, provider: &str) {
349 self.runtime_overrides.write().remove(provider);
350 }
351
352 pub fn set_fallback_resolver<F>(&self, resolver: F)
354 where
355 F: Fn(&str) -> Option<String> + Send + Sync + 'static,
356 {
357 *self.fallback_resolver.write() = Some(Box::new(resolver));
358 }
359
360 pub fn clear_fallback_resolver(&self) {
362 *self.fallback_resolver.write() = None;
363 }
364
365 pub fn get_api_key(&self, provider: &str) -> Option<String> {
375 {
377 let overrides = self.runtime_overrides.read();
378 if let Some(key) = overrides.get(provider) {
379 return Some(key.clone());
380 }
381 }
382
383 if let Some(auth) = self.providers.get(provider)
385 && let Some(key) = auth.get_api_key()
386 {
387 return Some(key);
388 }
389
390 {
392 let resolver = self.fallback_resolver.read();
393 if let Some(ref fallback) = *resolver
394 && let Some(key) = fallback(provider)
395 {
396 return Some(key);
397 }
398 }
399
400 env_api_keys::get_env_api_key(provider)
402 }
403
404 pub fn has_auth(&self, provider: &str) -> bool {
410 if self.runtime_overrides.read().contains_key(provider) {
412 return true;
413 }
414
415 if let Some(auth) = self.providers.get(provider)
417 && auth.is_configured()
418 {
419 return true;
420 }
421
422 false
426 }
427
428 pub fn needs_oauth_refresh(&self, provider: &str) -> bool {
430 self.providers
431 .get(provider)
432 .map(|auth| auth.needs_oauth_refresh())
433 .unwrap_or(false)
434 }
435
436 pub fn set_oauth_token(&mut self, provider: &str, token: OAuthTokenInfo) {
438 if let Some(auth) = self.providers.get_mut(provider) {
439 auth.set_oauth_token(token);
440 }
441 }
442
443 pub fn set_api_key(&mut self, provider: &str, api_key: String) {
445 if let Some(auth) = self.providers.get_mut(provider) {
446 auth.set_api_key(api_key);
447 } else {
448 self.register_api_key(provider, Some(api_key));
450 }
451 }
452
453 pub fn list_providers(&self) -> Vec<String> {
455 let mut providers: Vec<String> = self
456 .providers
457 .iter()
458 .filter(|(_, auth)| auth.is_configured())
459 .map(|(name, _)| name.clone())
460 .collect();
461
462 let overrides: Vec<String> = self.runtime_overrides.read().keys().cloned().collect();
464
465 for key in overrides {
466 if !providers.contains(&key) {
467 providers.push(key);
468 }
469 }
470
471 providers.sort();
472 providers.dedup();
473 providers
474 }
475
476 pub fn get_auth_status(&self, provider: &str) -> AuthStatus {
478 if self.runtime_overrides.read().contains_key(provider) {
479 return AuthStatus {
480 configured: true,
481 source: AuthSource::Runtime,
482 label: Some("--api-key".to_string()),
483 };
484 }
485
486 if let Some(auth) = self.providers.get(provider)
487 && auth.is_configured()
488 {
489 return AuthStatus {
490 configured: true,
491 source: AuthSource::Stored,
492 label: None,
493 };
494 }
495
496 if env_api_keys::has_env_key(provider) {
498 return AuthStatus {
499 configured: false, source: AuthSource::Environment,
501 label: None,
502 };
503 }
504
505 AuthStatus {
506 configured: false,
507 source: AuthSource::Stored,
508 label: Some("run 'oxicode setup' to configure".to_string()),
509 }
510 }
511}
512
513#[derive(Debug, Clone)]
515pub struct AuthStatus {
516 pub configured: bool,
518 pub source: AuthSource,
520 pub label: Option<String>,
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_oauth_token_info_expired() {
530 let token = OAuthTokenInfo::new(
532 "access".to_string(),
533 Some("refresh".to_string()),
534 -1, );
536
537 assert!(token.is_expired());
538 assert!(token.needs_refresh());
539 }
540
541 #[test]
542 fn test_oauth_token_info_valid() {
543 let token = OAuthTokenInfo::new("access".to_string(), Some("refresh".to_string()), 3600);
545
546 assert!(!token.is_expired());
547 assert!(!token.needs_refresh());
548 }
549
550 #[test]
551 fn test_oauth_token_info_needs_refresh_buffer() {
552 let token = OAuthTokenInfo::new("access".to_string(), Some("refresh".to_string()), 120);
554
555 assert!(!token.is_expired());
556 assert!(token.needs_refresh());
557 }
558
559 #[test]
560 fn test_api_key_auth() {
561 let auth = ApiKeyAuth::new(Some("sk-test".to_string()), AuthSource::Stored);
562
563 assert!(auth.is_configured());
564 assert_eq!(auth.get_api_key(), Some("sk-test".to_string()));
565 assert!(!auth.needs_oauth_refresh());
566 }
567
568 #[test]
569 fn test_api_key_auth_not_configured() {
570 let auth = ApiKeyAuth::new(None, AuthSource::Environment);
571
572 assert!(!auth.is_configured());
573 assert!(auth.get_api_key().is_none());
574 }
575
576 #[test]
577 fn test_oauth_auth() {
578 let token = OAuthTokenInfo::new(
579 "access_token".to_string(),
580 Some("refresh_token".to_string()),
581 3600,
582 );
583 let auth = OAuthAuth::with_token("anthropic", token);
584
585 assert!(auth.is_configured());
586 assert_eq!(auth.get_api_key(), Some("access_token".to_string()));
587 assert_eq!(auth.provider_name(), "anthropic");
588 }
589
590 #[test]
591 fn test_oauth_auth_refresh_callback() {
592 let mut auth = OAuthAuth::new("anthropic");
593
594 let refreshed = std::sync::Arc::new(std::sync::Mutex::new(false));
595 let refreshed_clone = refreshed.clone();
596 auth.on_token_refresh(move |_| {
597 *refreshed_clone.lock().unwrap() = true;
598 });
599
600 let new_token = OAuthTokenInfo::new("new_access".to_string(), None, 3600);
601 auth.set_oauth_token(new_token);
602
603 assert!(*refreshed.lock().unwrap());
604 assert_eq!(auth.get_api_key(), Some("new_access".to_string()));
605 }
606
607 #[test]
608 fn test_ambient_auth() {
609 let auth = AmbientAuth::new("bedrock", || true);
610
611 assert!(auth.is_configured());
612 assert_eq!(auth.get_api_key(), Some("<authenticated>".to_string()));
613 }
614
615 #[test]
616 fn test_ambient_auth_not_configured() {
617 let auth = AmbientAuth::new("bedrock", || false);
618
619 assert!(!auth.is_configured());
620 assert!(auth.get_api_key().is_none());
621 }
622
623 #[test]
624 fn test_registry_new() {
625 let registry = ProviderAuthRegistry::new();
626
627 assert!(registry.list_providers().is_empty());
628 assert!(!registry.has_auth("openai"));
629 }
630
631 #[test]
632 fn test_registry_with_defaults() {
633 let registry = ProviderAuthRegistry::with_defaults();
634
635 assert!(registry.providers.contains_key("bedrock"));
637 assert!(registry.providers.contains_key("vertex"));
638 }
639
640 #[test]
641 fn test_registry_runtime_override() {
642 let registry = ProviderAuthRegistry::new();
643
644 registry.set_runtime_key("openai", "sk-runtime".to_string());
646
647 assert_eq!(
649 registry.get_api_key("openai"),
650 Some("sk-runtime".to_string())
651 );
652 assert!(registry.has_auth("openai"));
653 }
654
655 #[test]
656 fn test_registry_remove_runtime_key() {
657 let registry = ProviderAuthRegistry::new();
658
659 registry.set_runtime_key("openai", "sk-runtime".to_string());
660 assert_eq!(
661 registry.get_api_key("openai"),
662 Some("sk-runtime".to_string())
663 );
664
665 registry.remove_runtime_key("openai");
666 assert!(registry.get_api_key("openai").is_none());
667 }
668
669 #[test]
670 fn test_registry_register_api_key() {
671 let mut registry = ProviderAuthRegistry::new();
672 registry.register_api_key("anthropic", Some("sk-stored".to_string()));
673
674 assert!(registry.has_auth("anthropic"));
675 assert_eq!(
676 registry.get_api_key("anthropic"),
677 Some("sk-stored".to_string())
678 );
679 }
680
681 #[test]
682 fn test_registry_register_oauth() {
683 let mut registry = ProviderAuthRegistry::new();
684 let token = OAuthTokenInfo::new(
685 "oauth-access".to_string(),
686 Some("refresh".to_string()),
687 3600,
688 );
689 registry.register_oauth("anthropic", token);
690
691 assert!(registry.has_auth("anthropic"));
692 assert_eq!(
693 registry.get_api_key("anthropic"),
694 Some("oauth-access".to_string())
695 );
696 }
697
698 #[test]
699 fn test_registry_env_key_fallback() {
700 unsafe { std::env::set_var("OPENAI_API_KEY", "sk-env-key") };
701
702 let registry = ProviderAuthRegistry::new();
703 assert_eq!(
707 registry.get_api_key("openai"),
708 Some("sk-env-key".to_string())
709 );
710
711 unsafe { std::env::remove_var("OPENAI_API_KEY") };
712 }
713
714 #[test]
715 fn test_registry_fallback_resolver() {
716 let registry = ProviderAuthRegistry::new();
717
718 registry.set_fallback_resolver(|provider| {
719 if provider == "custom" {
720 Some("custom-key".to_string())
721 } else {
722 None
723 }
724 });
725
726 assert_eq!(
727 registry.get_api_key("custom"),
728 Some("custom-key".to_string())
729 );
730 assert!(registry.get_api_key("unknown").is_none());
731 }
732
733 #[test]
734 fn test_registry_priority() {
735 unsafe { std::env::set_var("ANTHROPIC_API_KEY", "sk-env") };
736
737 let mut registry = ProviderAuthRegistry::new();
738
739 registry.register_api_key("anthropic", Some("sk-stored".to_string()));
741 registry.set_runtime_key("anthropic", "sk-runtime".to_string());
742
743 assert_eq!(
745 registry.get_api_key("anthropic"),
746 Some("sk-runtime".to_string())
747 );
748
749 registry.remove_runtime_key("anthropic");
751
752 assert_eq!(
754 registry.get_api_key("anthropic"),
755 Some("sk-stored".to_string())
756 );
757
758 unsafe { std::env::remove_var("ANTHROPIC_API_KEY") };
759 }
760
761 #[test]
762 fn test_registry_list_providers() {
763 let mut registry = ProviderAuthRegistry::new();
764
765 registry.register_api_key("openai", Some("key1".to_string()));
766 registry.register_oauth(
767 "anthropic",
768 OAuthTokenInfo::new("access".to_string(), None, 3600),
769 );
770 registry.set_runtime_key("google", "runtime-key".to_string());
771
772 let providers = registry.list_providers();
773 assert!(providers.contains(&"openai".to_string()));
774 assert!(providers.contains(&"anthropic".to_string()));
775 assert!(providers.contains(&"google".to_string()));
776 }
777
778 #[test]
779 fn test_registry_get_auth_status() {
780 let registry = ProviderAuthRegistry::new();
781
782 registry.set_runtime_key("openai", "key".to_string());
783
784 let status = registry.get_auth_status("openai");
785 assert!(status.configured);
786 assert_eq!(status.source, AuthSource::Runtime);
787 assert_eq!(status.label, Some("--api-key".to_string()));
788 }
789
790 #[test]
791 fn test_registry_env_source_status() {
792 unsafe { std::env::set_var("DEEPSEEK_API_KEY", "sk-test") };
793
794 let registry = ProviderAuthRegistry::new();
795
796 let status = registry.get_auth_status("deepseek");
797 assert!(!status.configured); assert_eq!(status.source, AuthSource::Environment);
799
800 unsafe { std::env::remove_var("DEEPSEEK_API_KEY") };
801 }
802
803 #[test]
804 fn test_registry_update_oauth_token() {
805 let mut registry = ProviderAuthRegistry::new();
806 let token = OAuthTokenInfo::new("old".to_string(), None, 0);
807 registry.register_oauth("anthropic", token);
808
809 assert!(registry.needs_oauth_refresh("anthropic"));
811
812 let new_token = OAuthTokenInfo::new("new".to_string(), None, 3600);
814 registry.set_oauth_token("anthropic", new_token);
815
816 assert!(!registry.needs_oauth_refresh("anthropic"));
817 assert_eq!(registry.get_api_key("anthropic"), Some("new".to_string()));
818 }
819}