1use std::future::Future;
17use std::sync::Arc;
18
19use dashmap::DashMap;
20use rmcp::transport::AuthClient;
21use rmcp::transport::streamable_http_client::StreamableHttpClient;
22use rmcp_client as rmcp;
23use tokio::sync::Mutex;
24
25use crate::upstream::config::{UpstreamConfig, UpstreamOauthRegistration};
26use crate::upstream::manager::UpstreamOauthManager;
27use crate::upstream::types::OauthError;
28
29pub struct CachedAuthClient {
33 pub client: Arc<AuthClient<reqwest::Client>>,
34 fingerprint: String,
35}
36
37type CacheKey = (String, String);
43
44#[derive(Clone)]
45pub struct OauthClientCache {
46 clients: Arc<DashMap<CacheKey, Arc<CachedAuthClient>>>,
48 managers: Arc<DashMap<String, UpstreamOauthManager>>,
51 build_locks: Arc<DashMap<CacheKey, Arc<Mutex<()>>>>,
54}
55
56impl OauthClientCache {
57 #[must_use]
59 pub fn new(managers: Arc<DashMap<String, UpstreamOauthManager>>) -> Self {
60 Self {
61 clients: Arc::new(DashMap::new()),
62 managers,
63 build_locks: Arc::new(DashMap::new()),
64 }
65 }
66
67 #[allow(dead_code)]
85 pub async fn get_or_build(
86 &self,
87 config: &UpstreamConfig,
88 subject: &str,
89 ) -> Result<Arc<AuthClient<reqwest::Client>>, OauthError> {
90 let dynamic_client_id: Option<String> = if config.oauth.as_ref().is_some_and(|o| {
93 matches!(
94 o.registration,
95 UpstreamOauthRegistration::Auto | UpstreamOauthRegistration::Dynamic
96 )
97 }) {
98 self.managers
99 .get(&config.name)
100 .map(|r| r.clone())
101 .ok_or_else(|| {
102 OauthError::Internal(format!(
103 "no oauth manager registered for upstream '{}'",
104 config.name
105 ))
106 })?
107 .stored_dynamic_client_id(subject)
108 .await?
109 } else {
110 None
111 };
112
113 self.get_or_insert_with(config, subject, dynamic_client_id.as_deref(), || async {
114 let manager = self
115 .managers
116 .get(&config.name)
117 .map(|r| r.clone())
118 .ok_or_else(|| {
119 OauthError::Internal(format!(
120 "no oauth manager registered for upstream '{}'",
121 config.name
122 ))
123 })?;
124 let auth_client = manager.build_auth_client(subject).await?;
125 Ok(Arc::new(auth_client))
126 })
127 .await
128 }
129
130 pub async fn get_or_build_capped<C>(
140 &self,
141 config: &UpstreamConfig,
142 subject: &str,
143 http_client: C,
144 ) -> Result<AuthClient<C>, OauthError>
145 where
146 C: StreamableHttpClient + Clone,
147 {
148 let manager = self
149 .managers
150 .get(&config.name)
151 .map(|r| r.clone())
152 .ok_or_else(|| {
153 OauthError::Internal(format!(
154 "no oauth manager registered for upstream '{}'",
155 config.name
156 ))
157 })?;
158 manager.build_auth_client_with(subject, http_client).await
159 }
160
161 #[allow(dead_code)]
162 async fn get_or_insert_with<F, Fut>(
163 &self,
164 config: &UpstreamConfig,
165 subject: &str,
166 dynamic_client_id: Option<&str>,
169 builder: F,
170 ) -> Result<Arc<AuthClient<reqwest::Client>>, OauthError>
171 where
172 F: FnOnce() -> Fut,
173 Fut: Future<Output = Result<Arc<AuthClient<reqwest::Client>>, OauthError>>,
174 {
175 let fingerprint = registration_fingerprint(config, dynamic_client_id)?;
176 let key = (config.name.clone(), subject.to_string());
177
178 if let Some(entry) = self.clients.get(&key)
179 && entry.fingerprint == fingerprint
180 {
181 return Ok(Arc::clone(&entry.client));
182 }
183
184 let lock = self
185 .build_locks
186 .entry(key.clone())
187 .or_insert_with(|| Arc::new(Mutex::new(())))
188 .clone();
189 let _guard = lock.lock().await;
190
191 if let Some(entry) = self.clients.get(&key)
194 && entry.fingerprint == fingerprint
195 {
196 return Ok(Arc::clone(&entry.client));
197 }
198
199 let arc_client = builder().await?;
200
201 self.clients.insert(
202 key,
203 Arc::new(CachedAuthClient {
204 client: Arc::clone(&arc_client),
205 fingerprint,
206 }),
207 );
208
209 Ok(arc_client)
210 }
211
212 pub fn evict_subject(&self, upstream: &str, subject: &str) {
217 let key = (upstream.to_string(), subject.to_string());
218 self.clients.remove(&key);
219 }
224
225 pub fn evict_upstream(&self, upstream: &str) {
231 self.clients.retain(|(name, _), _| name != upstream);
232 }
234
235 pub fn evict_upstreams_not_in(&self, known: &std::collections::HashSet<&str>) {
240 self.clients
241 .retain(|(name, _), _| known.contains(name.as_str()));
242 }
243
244 #[allow(dead_code)]
246 #[must_use]
247 pub fn len(&self) -> usize {
248 self.clients.len()
249 }
250
251 #[allow(dead_code)]
253 #[must_use]
254 pub fn is_empty(&self) -> bool {
255 self.clients.is_empty()
256 }
257
258 #[cfg(any(test, debug_assertions))]
264 pub fn insert_for_tests(
265 &self,
266 upstream: &str,
267 subject: &str,
268 fingerprint: &str,
269 client: Arc<AuthClient<reqwest::Client>>,
270 ) {
271 self.clients.insert(
272 (upstream.to_string(), subject.to_string()),
273 Arc::new(CachedAuthClient {
274 client,
275 fingerprint: fingerprint.to_string(),
276 }),
277 );
278 }
279}
280
281#[allow(dead_code)]
288fn registration_fingerprint(
289 config: &UpstreamConfig,
290 dynamic_client_id: Option<&str>,
291) -> Result<String, OauthError> {
292 let oauth = config
293 .oauth
294 .as_ref()
295 .ok_or_else(|| OauthError::Internal("upstream has no oauth config".to_string()))?;
296
297 Ok(match &oauth.registration {
298 UpstreamOauthRegistration::Auto => {
299 format!(
300 "auto:{}",
301 dynamic_client_id.unwrap_or("cimd-or-unregistered")
302 )
303 }
304 UpstreamOauthRegistration::Preregistered { client_id, .. } => {
305 format!("preregistered:{client_id}")
306 }
307 UpstreamOauthRegistration::ClientMetadataDocument { url } => {
308 format!("client_metadata_document:{url}")
309 }
310 UpstreamOauthRegistration::Dynamic => {
311 format!("dynamic:{}", dynamic_client_id.unwrap_or("none"))
316 }
317 })
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::upstream::config::{UpstreamOauthConfig, UpstreamOauthMode};
324 use rmcp_client::transport::AuthorizationManager;
325 use std::sync::atomic::{AtomicUsize, Ordering};
326
327 fn cfg(name: &str, client_id: &str) -> UpstreamConfig {
328 UpstreamConfig {
329 name: name.to_string(),
330 url: Some(format!("https://{name}.example/mcp")),
331 oauth: Some(UpstreamOauthConfig {
332 mode: UpstreamOauthMode::AuthorizationCodePkce,
333 registration: UpstreamOauthRegistration::Preregistered {
334 client_id: client_id.to_string(),
335 client_secret_env: None,
336 },
337 scopes: None,
338 prefer_client_metadata_document: None,
339 }),
340 }
341 }
342
343 #[test]
344 fn fingerprint_differs_on_client_id_change() {
345 let a = registration_fingerprint(&cfg("acme", "id-1"), None).unwrap();
346 let b = registration_fingerprint(&cfg("acme", "id-2"), None).unwrap();
347 assert_ne!(a, b);
348 }
349
350 #[test]
351 fn auto_fingerprint_tracks_dynamic_fallback_registration() {
352 let mut config = cfg("acme", "ignored");
353 config.oauth.as_mut().expect("oauth").registration = UpstreamOauthRegistration::Auto;
354 let first = registration_fingerprint(&config, Some("client-one")).unwrap();
355 let second = registration_fingerprint(&config, Some("client-two")).unwrap();
356 assert_ne!(first, second);
357 }
358
359 #[test]
360 fn fingerprint_stable_for_identical_config() {
361 let a = registration_fingerprint(&cfg("acme", "id-1"), None).unwrap();
362 let b = registration_fingerprint(&cfg("acme", "id-1"), None).unwrap();
363 assert_eq!(a, b);
364 }
365
366 #[test]
367 fn empty_cache_is_empty() {
368 let cache = OauthClientCache::new(Arc::new(DashMap::new()));
369 assert!(cache.is_empty());
370 assert_eq!(cache.len(), 0);
371 }
372
373 async fn dummy_auth_client() -> Arc<AuthClient<reqwest::Client>> {
374 drop(rustls::crypto::ring::default_provider().install_default());
377 let manager = AuthorizationManager::new("http://localhost")
378 .await
379 .expect("authorization manager");
380 Arc::new(AuthClient::new(reqwest::Client::new(), manager))
381 }
382
383 #[tokio::test]
384 async fn cache_atomic_first_request_no_double_build() {
385 let cache = OauthClientCache::new(Arc::new(DashMap::new()));
386 let config = cfg("acme", "id-1");
387 let builds = Arc::new(AtomicUsize::new(0));
388
389 let left = {
390 let cache = cache.clone();
391 let config = config.clone();
392 let builds = Arc::clone(&builds);
393 tokio::spawn(async move {
394 cache
395 .get_or_insert_with(&config, "alice", None, || {
396 let builds = Arc::clone(&builds);
397 async move {
398 builds.fetch_add(1, Ordering::SeqCst);
399 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
400 Ok(dummy_auth_client().await)
401 }
402 })
403 .await
404 .expect("left client")
405 })
406 };
407 let right = {
408 let cache = cache.clone();
409 let config = config.clone();
410 let builds = Arc::clone(&builds);
411 tokio::spawn(async move {
412 cache
413 .get_or_insert_with(&config, "alice", None, || {
414 let builds = Arc::clone(&builds);
415 async move {
416 builds.fetch_add(1, Ordering::SeqCst);
417 Ok(dummy_auth_client().await)
418 }
419 })
420 .await
421 .expect("right client")
422 })
423 };
424
425 let left = left.await.expect("join left");
426 let right = right.await.expect("join right");
427
428 assert_eq!(builds.load(Ordering::SeqCst), 1);
429 assert!(Arc::ptr_eq(&left, &right));
430 }
431
432 #[tokio::test]
433 async fn cache_refuses_stale_client_id_after_config_change() {
434 let cache = OauthClientCache::new(Arc::new(DashMap::new()));
435 let old = cfg("acme", "id-1");
436 let new = cfg("acme", "id-2");
437 let old_fingerprint = registration_fingerprint(&old, None).expect("old fingerprint");
438 cache.insert_for_tests("acme", "alice", &old_fingerprint, dummy_auth_client().await);
439
440 let rebuilt = Arc::new(AtomicUsize::new(0));
441 let client = cache
442 .get_or_insert_with(&new, "alice", None, || {
443 let rebuilt = Arc::clone(&rebuilt);
444 async move {
445 rebuilt.fetch_add(1, Ordering::SeqCst);
446 Ok(dummy_auth_client().await)
447 }
448 })
449 .await
450 .expect("rebuilt client");
451
452 assert_eq!(rebuilt.load(Ordering::SeqCst), 1);
453 assert_eq!(cache.len(), 1);
454 let stored = cache
455 .clients
456 .get(&(String::from("acme"), String::from("alice")))
457 .expect("stored client");
458 assert_eq!(
459 stored.fingerprint,
460 registration_fingerprint(&new, None).unwrap()
461 );
462 assert!(Arc::ptr_eq(&stored.client, &client));
463 }
464
465 }