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
93 .oauth
94 .as_ref()
95 .is_some_and(|o| matches!(o.registration, UpstreamOauthRegistration::Dynamic))
96 {
97 self.managers
98 .get(&config.name)
99 .map(|r| r.clone())
100 .ok_or_else(|| {
101 OauthError::Internal(format!(
102 "no oauth manager registered for upstream '{}'",
103 config.name
104 ))
105 })?
106 .stored_dynamic_client_id(subject)
107 .await?
108 } else {
109 None
110 };
111
112 self.get_or_insert_with(config, subject, dynamic_client_id.as_deref(), || async {
113 let manager = self
114 .managers
115 .get(&config.name)
116 .map(|r| r.clone())
117 .ok_or_else(|| {
118 OauthError::Internal(format!(
119 "no oauth manager registered for upstream '{}'",
120 config.name
121 ))
122 })?;
123 let auth_client = manager.build_auth_client(subject).await?;
124 Ok(Arc::new(auth_client))
125 })
126 .await
127 }
128
129 pub async fn get_or_build_capped<C>(
139 &self,
140 config: &UpstreamConfig,
141 subject: &str,
142 http_client: C,
143 ) -> Result<AuthClient<C>, OauthError>
144 where
145 C: StreamableHttpClient + Clone,
146 {
147 let manager = self
148 .managers
149 .get(&config.name)
150 .map(|r| r.clone())
151 .ok_or_else(|| {
152 OauthError::Internal(format!(
153 "no oauth manager registered for upstream '{}'",
154 config.name
155 ))
156 })?;
157 manager.build_auth_client_with(subject, http_client).await
158 }
159
160 #[allow(dead_code)]
161 async fn get_or_insert_with<F, Fut>(
162 &self,
163 config: &UpstreamConfig,
164 subject: &str,
165 dynamic_client_id: Option<&str>,
168 builder: F,
169 ) -> Result<Arc<AuthClient<reqwest::Client>>, OauthError>
170 where
171 F: FnOnce() -> Fut,
172 Fut: Future<Output = Result<Arc<AuthClient<reqwest::Client>>, OauthError>>,
173 {
174 let fingerprint = registration_fingerprint(config, dynamic_client_id)?;
175 let key = (config.name.clone(), subject.to_string());
176
177 if let Some(entry) = self.clients.get(&key)
178 && entry.fingerprint == fingerprint
179 {
180 return Ok(Arc::clone(&entry.client));
181 }
182
183 let lock = self
184 .build_locks
185 .entry(key.clone())
186 .or_insert_with(|| Arc::new(Mutex::new(())))
187 .clone();
188 let _guard = lock.lock().await;
189
190 if let Some(entry) = self.clients.get(&key)
193 && entry.fingerprint == fingerprint
194 {
195 return Ok(Arc::clone(&entry.client));
196 }
197
198 let arc_client = builder().await?;
199
200 self.clients.insert(
201 key,
202 Arc::new(CachedAuthClient {
203 client: Arc::clone(&arc_client),
204 fingerprint,
205 }),
206 );
207
208 Ok(arc_client)
209 }
210
211 pub fn evict_subject(&self, upstream: &str, subject: &str) {
216 let key = (upstream.to_string(), subject.to_string());
217 self.clients.remove(&key);
218 }
223
224 pub fn evict_upstream(&self, upstream: &str) {
230 self.clients.retain(|(name, _), _| name != upstream);
231 }
233
234 pub fn evict_upstreams_not_in(&self, known: &std::collections::HashSet<&str>) {
239 self.clients
240 .retain(|(name, _), _| known.contains(name.as_str()));
241 }
242
243 #[allow(dead_code)]
245 #[must_use]
246 pub fn len(&self) -> usize {
247 self.clients.len()
248 }
249
250 #[allow(dead_code)]
252 #[must_use]
253 pub fn is_empty(&self) -> bool {
254 self.clients.is_empty()
255 }
256
257 #[cfg(any(test, debug_assertions))]
263 pub fn insert_for_tests(
264 &self,
265 upstream: &str,
266 subject: &str,
267 fingerprint: &str,
268 client: Arc<AuthClient<reqwest::Client>>,
269 ) {
270 self.clients.insert(
271 (upstream.to_string(), subject.to_string()),
272 Arc::new(CachedAuthClient {
273 client,
274 fingerprint: fingerprint.to_string(),
275 }),
276 );
277 }
278}
279
280#[allow(dead_code)]
287fn registration_fingerprint(
288 config: &UpstreamConfig,
289 dynamic_client_id: Option<&str>,
290) -> Result<String, OauthError> {
291 let oauth = config
292 .oauth
293 .as_ref()
294 .ok_or_else(|| OauthError::Internal("upstream has no oauth config".to_string()))?;
295
296 Ok(match &oauth.registration {
297 UpstreamOauthRegistration::Preregistered { client_id, .. } => {
298 format!("preregistered:{client_id}")
299 }
300 UpstreamOauthRegistration::ClientMetadataDocument { url } => {
301 format!("client_metadata_document:{url}")
302 }
303 UpstreamOauthRegistration::Dynamic => {
304 format!("dynamic:{}", dynamic_client_id.unwrap_or("none"))
309 }
310 })
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::upstream::config::{UpstreamOauthConfig, UpstreamOauthMode};
317 use rmcp_client::transport::AuthorizationManager;
318 use std::sync::atomic::{AtomicUsize, Ordering};
319
320 fn cfg(name: &str, client_id: &str) -> UpstreamConfig {
321 UpstreamConfig {
322 name: name.to_string(),
323 url: Some(format!("https://{name}.example/mcp")),
324 oauth: Some(UpstreamOauthConfig {
325 mode: UpstreamOauthMode::AuthorizationCodePkce,
326 registration: UpstreamOauthRegistration::Preregistered {
327 client_id: client_id.to_string(),
328 client_secret_env: None,
329 },
330 scopes: None,
331 prefer_client_metadata_document: None,
332 }),
333 }
334 }
335
336 #[test]
337 fn fingerprint_differs_on_client_id_change() {
338 let a = registration_fingerprint(&cfg("acme", "id-1"), None).unwrap();
339 let b = registration_fingerprint(&cfg("acme", "id-2"), None).unwrap();
340 assert_ne!(a, b);
341 }
342
343 #[test]
344 fn fingerprint_stable_for_identical_config() {
345 let a = registration_fingerprint(&cfg("acme", "id-1"), None).unwrap();
346 let b = registration_fingerprint(&cfg("acme", "id-1"), None).unwrap();
347 assert_eq!(a, b);
348 }
349
350 #[test]
351 fn empty_cache_is_empty() {
352 let cache = OauthClientCache::new(Arc::new(DashMap::new()));
353 assert!(cache.is_empty());
354 assert_eq!(cache.len(), 0);
355 }
356
357 async fn dummy_auth_client() -> Arc<AuthClient<reqwest::Client>> {
358 drop(rustls::crypto::ring::default_provider().install_default());
361 let manager = AuthorizationManager::new("http://localhost")
362 .await
363 .expect("authorization manager");
364 Arc::new(AuthClient::new(reqwest::Client::new(), manager))
365 }
366
367 #[tokio::test]
368 async fn cache_atomic_first_request_no_double_build() {
369 let cache = OauthClientCache::new(Arc::new(DashMap::new()));
370 let config = cfg("acme", "id-1");
371 let builds = Arc::new(AtomicUsize::new(0));
372
373 let left = {
374 let cache = cache.clone();
375 let config = config.clone();
376 let builds = Arc::clone(&builds);
377 tokio::spawn(async move {
378 cache
379 .get_or_insert_with(&config, "alice", None, || {
380 let builds = Arc::clone(&builds);
381 async move {
382 builds.fetch_add(1, Ordering::SeqCst);
383 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
384 Ok(dummy_auth_client().await)
385 }
386 })
387 .await
388 .expect("left client")
389 })
390 };
391 let right = {
392 let cache = cache.clone();
393 let config = config.clone();
394 let builds = Arc::clone(&builds);
395 tokio::spawn(async move {
396 cache
397 .get_or_insert_with(&config, "alice", None, || {
398 let builds = Arc::clone(&builds);
399 async move {
400 builds.fetch_add(1, Ordering::SeqCst);
401 Ok(dummy_auth_client().await)
402 }
403 })
404 .await
405 .expect("right client")
406 })
407 };
408
409 let left = left.await.expect("join left");
410 let right = right.await.expect("join right");
411
412 assert_eq!(builds.load(Ordering::SeqCst), 1);
413 assert!(Arc::ptr_eq(&left, &right));
414 }
415
416 #[tokio::test]
417 async fn cache_refuses_stale_client_id_after_config_change() {
418 let cache = OauthClientCache::new(Arc::new(DashMap::new()));
419 let old = cfg("acme", "id-1");
420 let new = cfg("acme", "id-2");
421 let old_fingerprint = registration_fingerprint(&old, None).expect("old fingerprint");
422 cache.insert_for_tests("acme", "alice", &old_fingerprint, dummy_auth_client().await);
423
424 let rebuilt = Arc::new(AtomicUsize::new(0));
425 let client = cache
426 .get_or_insert_with(&new, "alice", None, || {
427 let rebuilt = Arc::clone(&rebuilt);
428 async move {
429 rebuilt.fetch_add(1, Ordering::SeqCst);
430 Ok(dummy_auth_client().await)
431 }
432 })
433 .await
434 .expect("rebuilt client");
435
436 assert_eq!(rebuilt.load(Ordering::SeqCst), 1);
437 assert_eq!(cache.len(), 1);
438 let stored = cache
439 .clients
440 .get(&(String::from("acme"), String::from("alice")))
441 .expect("stored client");
442 assert_eq!(
443 stored.fingerprint,
444 registration_fingerprint(&new, None).unwrap()
445 );
446 assert!(Arc::ptr_eq(&stored.client, &client));
447 }
448
449 }