rust_x402/
facilitator_storage.rs

1//! Storage trait for facilitator nonce tracking
2//!
3//! This module provides a trait-based storage abstraction for tracking
4//! processed nonces to prevent replay attacks.
5
6use crate::Result;
7use async_trait::async_trait;
8
9/// Trait for storing and retrieving nonce information
10///
11/// This trait allows different storage backends to be used by the facilitator,
12/// enabling flexibility in deployment scenarios.
13#[async_trait]
14pub trait NonceStorage: Send + Sync {
15    /// Check if a nonce has been processed
16    async fn has_nonce(&self, nonce: &str) -> Result<bool>;
17
18    /// Mark a nonce as processed
19    async fn mark_nonce(&self, nonce: &str) -> Result<()>;
20
21    /// Remove a nonce (optional cleanup)
22    async fn remove_nonce(&self, nonce: &str) -> Result<()>;
23}
24
25/// In-memory storage implementation
26///
27/// This is the default storage implementation that uses an in-memory HashMap.
28/// Data is lost when the server restarts.
29#[derive(Debug, Clone)]
30pub struct InMemoryStorage {
31    nonces: std::sync::Arc<tokio::sync::RwLock<std::collections::HashMap<String, bool>>>,
32}
33
34impl InMemoryStorage {
35    /// Create a new in-memory storage instance
36    pub fn new() -> Self {
37        Self {
38            nonces: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
39        }
40    }
41}
42
43impl Default for InMemoryStorage {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49#[async_trait]
50impl NonceStorage for InMemoryStorage {
51    async fn has_nonce(&self, nonce: &str) -> Result<bool> {
52        let nonces = self.nonces.read().await;
53        Ok(nonces.contains_key(nonce))
54    }
55
56    async fn mark_nonce(&self, nonce: &str) -> Result<()> {
57        let mut nonces = self.nonces.write().await;
58        nonces.insert(nonce.to_string(), true);
59        Ok(())
60    }
61
62    async fn remove_nonce(&self, nonce: &str) -> Result<()> {
63        let mut nonces = self.nonces.write().await;
64        nonces.remove(nonce);
65        Ok(())
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[tokio::test]
74    async fn test_in_memory_storage_creation() {
75        let storage = InMemoryStorage::new();
76        assert!(!storage.has_nonce("test").await.unwrap());
77    }
78
79    #[tokio::test]
80    async fn test_in_memory_storage_has_nonce() {
81        let storage = InMemoryStorage::new();
82        let test_nonce = "test_nonce_123";
83
84        // Initially, nonce should not exist
85        let exists = storage.has_nonce(test_nonce).await.unwrap();
86        assert!(!exists, "Nonce should not exist initially");
87
88        // Mark nonce as processed
89        storage.mark_nonce(test_nonce).await.unwrap();
90
91        // Now nonce should exist
92        let exists = storage.has_nonce(test_nonce).await.unwrap();
93        assert!(exists, "Nonce should exist after marking");
94    }
95
96    #[tokio::test]
97    async fn test_in_memory_storage_mark_nonce() {
98        let storage = InMemoryStorage::new();
99        let test_nonce = "test_nonce_mark_456";
100
101        // Mark nonce should succeed
102        let result = storage.mark_nonce(test_nonce).await;
103        assert!(result.is_ok(), "mark_nonce should succeed");
104
105        // Verify nonce was marked
106        let exists = storage.has_nonce(test_nonce).await.unwrap();
107        assert!(exists, "Nonce should exist after marking");
108    }
109
110    #[tokio::test]
111    async fn test_in_memory_storage_remove_nonce() {
112        let storage = InMemoryStorage::new();
113        let test_nonce = "test_nonce_remove_789";
114
115        // Mark nonce first
116        storage.mark_nonce(test_nonce).await.unwrap();
117        assert!(storage.has_nonce(test_nonce).await.unwrap());
118
119        // Remove nonce
120        let result = storage.remove_nonce(test_nonce).await;
121        assert!(result.is_ok(), "remove_nonce should succeed");
122
123        // Verify nonce was removed
124        let exists = storage.has_nonce(test_nonce).await.unwrap();
125        assert!(!exists, "Nonce should not exist after removal");
126    }
127
128    #[tokio::test]
129    async fn test_in_memory_storage_replay_protection() {
130        let storage = InMemoryStorage::new();
131        let test_nonce = "test_nonce_replay_abc";
132
133        // First mark should succeed
134        assert!(!storage.has_nonce(test_nonce).await.unwrap());
135        storage.mark_nonce(test_nonce).await.unwrap();
136
137        // Second mark should still work (idempotent), but has_nonce should return true
138        storage.mark_nonce(test_nonce).await.unwrap();
139        assert!(
140            storage.has_nonce(test_nonce).await.unwrap(),
141            "Nonce should still exist after second mark"
142        );
143    }
144
145    #[tokio::test]
146    async fn test_in_memory_storage_multiple_nonces() {
147        let storage = InMemoryStorage::new();
148
149        let nonce1 = "nonce1";
150        let nonce2 = "nonce2";
151        let nonce3 = "nonce3";
152
153        // Mark multiple nonces
154        storage.mark_nonce(nonce1).await.unwrap();
155        storage.mark_nonce(nonce2).await.unwrap();
156        storage.mark_nonce(nonce3).await.unwrap();
157
158        // Verify all exist
159        assert!(storage.has_nonce(nonce1).await.unwrap());
160        assert!(storage.has_nonce(nonce2).await.unwrap());
161        assert!(storage.has_nonce(nonce3).await.unwrap());
162
163        // Remove one
164        storage.remove_nonce(nonce2).await.unwrap();
165        assert!(!storage.has_nonce(nonce2).await.unwrap());
166        assert!(storage.has_nonce(nonce1).await.unwrap());
167        assert!(storage.has_nonce(nonce3).await.unwrap());
168    }
169}
170
171#[cfg(feature = "redis")]
172pub mod redis_storage {
173    use super::{NonceStorage, Result};
174    use redis::{AsyncCommands, Client};
175
176    /// Redis-based storage implementation
177    ///
178    /// This implementation uses Redis for persistent nonce storage,
179    /// allowing data to survive server restarts and enabling distributed
180    /// facilitator deployments.
181    #[derive(Debug, Clone)]
182    pub struct RedisStorage {
183        client: Client,
184        key_prefix: String,
185    }
186
187    impl RedisStorage {
188        /// Create a new Redis storage instance
189        ///
190        /// # Arguments
191        ///
192        /// * `redis_url` - Redis connection URL (e.g., "redis://localhost:6379")
193        /// * `key_prefix` - Optional prefix for Redis keys (default: "x402:nonce:")
194        pub async fn new(redis_url: &str, key_prefix: Option<&str>) -> Result<Self> {
195            let client = Client::open(redis_url).map_err(|e| {
196                crate::X402Error::config(format!("Failed to connect to Redis: {}", e))
197            })?;
198
199            let key_prefix = key_prefix.unwrap_or("x402:nonce:").to_string();
200
201            Ok(Self { client, key_prefix })
202        }
203
204        fn make_key(&self, nonce: &str) -> String {
205            format!("{}{}", self.key_prefix, nonce)
206        }
207    }
208
209    #[async_trait::async_trait]
210    impl NonceStorage for RedisStorage {
211        async fn has_nonce(&self, nonce: &str) -> Result<bool> {
212            let mut conn = self
213                .client
214                .get_multiplexed_async_connection()
215                .await
216                .map_err(|e| {
217                    crate::X402Error::config(format!("Failed to get Redis connection: {}", e))
218                })?;
219
220            let key = self.make_key(nonce);
221            let exists: bool = conn.exists(&key).await.map_err(|e| {
222                crate::X402Error::config(format!("Redis EXISTS command failed: {}", e))
223            })?;
224
225            Ok(exists)
226        }
227
228        async fn mark_nonce(&self, nonce: &str) -> Result<()> {
229            let mut conn = self
230                .client
231                .get_multiplexed_async_connection()
232                .await
233                .map_err(|e| {
234                    crate::X402Error::config(format!("Failed to get Redis connection: {}", e))
235                })?;
236
237            let key = self.make_key(nonce);
238            // Set with TTL of 24 hours to prevent unbounded growth
239            conn.set_ex::<_, _, ()>(&key, "1", 86400)
240                .await
241                .map_err(|e| {
242                    crate::X402Error::config(format!("Redis SET command failed: {}", e))
243                })?;
244
245            Ok(())
246        }
247
248        async fn remove_nonce(&self, nonce: &str) -> Result<()> {
249            let mut conn = self
250                .client
251                .get_multiplexed_async_connection()
252                .await
253                .map_err(|e| {
254                    crate::X402Error::config(format!("Failed to get Redis connection: {}", e))
255                })?;
256
257            let key = self.make_key(nonce);
258            conn.del::<_, ()>(&key).await.map_err(|e| {
259                crate::X402Error::config(format!("Redis DEL command failed: {}", e))
260            })?;
261
262            Ok(())
263        }
264    }
265
266    #[cfg(test)]
267    mod tests {
268        use super::*;
269        use std::env;
270
271        /// Helper function to check if Redis is available
272        /// Tests will be skipped if Redis is not available
273        async fn check_redis_available(redis_url: &str) -> bool {
274            match Client::open(redis_url) {
275                Ok(client) => {
276                    match client.get_multiplexed_async_connection().await {
277                        Ok(mut conn) => {
278                            // Try to ping Redis using AsyncCommands
279                            match conn.get::<&str, Option<String>>("__test_key__").await {
280                                Ok(_) => true,
281                                Err(_) => {
282                                    // If GET fails but connection works, try EXISTS
283                                    match conn.exists::<&str, bool>("__test_key__").await {
284                                        Ok(_) => true,
285                                        Err(_) => false,
286                                    }
287                                }
288                            }
289                        }
290                        Err(_) => false,
291                    }
292                }
293                Err(_) => false,
294            }
295        }
296
297        #[tokio::test]
298        async fn test_redis_storage_creation() {
299            let redis_url =
300                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
301
302            if !check_redis_available(&redis_url).await {
303                println!("Skipping Redis test: Redis not available at {}", redis_url);
304                return;
305            }
306
307            let storage = RedisStorage::new(&redis_url, None).await;
308            assert!(storage.is_ok(), "RedisStorage creation should succeed");
309
310            let storage = storage.unwrap();
311            assert_eq!(storage.key_prefix, "x402:nonce:");
312        }
313
314        #[tokio::test]
315        async fn test_redis_storage_custom_prefix() {
316            let redis_url =
317                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
318
319            if !check_redis_available(&redis_url).await {
320                println!("Skipping Redis test: Redis not available at {}", redis_url);
321                return;
322            }
323
324            let storage = RedisStorage::new(&redis_url, Some("test:prefix:")).await;
325            assert!(storage.is_ok());
326
327            let storage = storage.unwrap();
328            assert_eq!(storage.key_prefix, "test:prefix:");
329        }
330
331        #[tokio::test]
332        async fn test_redis_storage_has_nonce() {
333            let redis_url =
334                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
335
336            if !check_redis_available(&redis_url).await {
337                println!("Skipping Redis test: Redis not available at {}", redis_url);
338                return;
339            }
340
341            // Use a unique prefix for this test to avoid conflicts
342            let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
343            let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
344                .await
345                .unwrap();
346
347            let test_nonce = "test_nonce_123";
348
349            // Initially, nonce should not exist
350            let exists = storage.has_nonce(test_nonce).await.unwrap();
351            assert!(!exists, "Nonce should not exist initially");
352
353            // Mark nonce as processed
354            storage.mark_nonce(test_nonce).await.unwrap();
355
356            // Now nonce should exist
357            let exists = storage.has_nonce(test_nonce).await.unwrap();
358            assert!(exists, "Nonce should exist after marking");
359
360            // Clean up
361            storage.remove_nonce(test_nonce).await.unwrap();
362        }
363
364        #[tokio::test]
365        async fn test_redis_storage_mark_nonce() {
366            let redis_url =
367                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
368
369            if !check_redis_available(&redis_url).await {
370                println!("Skipping Redis test: Redis not available at {}", redis_url);
371                return;
372            }
373
374            let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
375            let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
376                .await
377                .unwrap();
378
379            let test_nonce = "test_nonce_mark_456";
380
381            // Mark nonce should succeed
382            let result = storage.mark_nonce(test_nonce).await;
383            assert!(result.is_ok(), "mark_nonce should succeed");
384
385            // Verify nonce was marked
386            let exists = storage.has_nonce(test_nonce).await.unwrap();
387            assert!(exists, "Nonce should exist after marking");
388
389            // Clean up
390            storage.remove_nonce(test_nonce).await.unwrap();
391        }
392
393        #[tokio::test]
394        async fn test_redis_storage_remove_nonce() {
395            let redis_url =
396                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
397
398            if !check_redis_available(&redis_url).await {
399                println!("Skipping Redis test: Redis not available at {}", redis_url);
400                return;
401            }
402
403            let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
404            let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
405                .await
406                .unwrap();
407
408            let test_nonce = "test_nonce_remove_789";
409
410            // Mark nonce first
411            storage.mark_nonce(test_nonce).await.unwrap();
412            assert!(storage.has_nonce(test_nonce).await.unwrap());
413
414            // Remove nonce
415            let result = storage.remove_nonce(test_nonce).await;
416            assert!(result.is_ok(), "remove_nonce should succeed");
417
418            // Verify nonce was removed
419            let exists = storage.has_nonce(test_nonce).await.unwrap();
420            assert!(!exists, "Nonce should not exist after removal");
421        }
422
423        #[tokio::test]
424        async fn test_redis_storage_replay_protection() {
425            let redis_url =
426                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
427
428            if !check_redis_available(&redis_url).await {
429                println!("Skipping Redis test: Redis not available at {}", redis_url);
430                return;
431            }
432
433            let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
434            let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
435                .await
436                .unwrap();
437
438            let test_nonce = "test_nonce_replay_abc";
439
440            // First mark should succeed
441            assert!(storage.has_nonce(test_nonce).await.unwrap() == false);
442            storage.mark_nonce(test_nonce).await.unwrap();
443
444            // Second mark should still work (idempotent), but has_nonce should return true
445            storage.mark_nonce(test_nonce).await.unwrap();
446            assert!(
447                storage.has_nonce(test_nonce).await.unwrap(),
448                "Nonce should still exist after second mark"
449            );
450
451            // Clean up
452            storage.remove_nonce(test_nonce).await.unwrap();
453        }
454
455        #[tokio::test]
456        async fn test_redis_storage_ttl() {
457            let redis_url =
458                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
459
460            if !check_redis_available(&redis_url).await {
461                println!("Skipping Redis test: Redis not available at {}", redis_url);
462                return;
463            }
464
465            let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
466            let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
467                .await
468                .unwrap();
469
470            let test_nonce = "test_nonce_ttl_xyz";
471
472            // Mark nonce (should have TTL of 24 hours)
473            storage.mark_nonce(test_nonce).await.unwrap();
474
475            // Verify key exists and has TTL
476            let mut conn = storage
477                .client
478                .get_multiplexed_async_connection()
479                .await
480                .unwrap();
481            let key = storage.make_key(test_nonce);
482            let ttl: i64 = conn.ttl(&key).await.unwrap();
483
484            // TTL should be positive (less than 86400 seconds = 24 hours)
485            assert!(ttl > 0, "Key should have a positive TTL");
486            assert!(
487                ttl <= 86400,
488                "TTL should be at most 24 hours (86400 seconds)"
489            );
490
491            // Clean up
492            storage.remove_nonce(test_nonce).await.unwrap();
493        }
494
495        #[tokio::test]
496        async fn test_redis_storage_multiple_nonces() {
497            let redis_url =
498                env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
499
500            if !check_redis_available(&redis_url).await {
501                println!("Skipping Redis test: Redis not available at {}", redis_url);
502                return;
503            }
504
505            let test_prefix = format!("test:{}:", uuid::Uuid::new_v4());
506            let storage = RedisStorage::new(&redis_url, Some(&test_prefix))
507                .await
508                .unwrap();
509
510            let nonce1 = "nonce1";
511            let nonce2 = "nonce2";
512            let nonce3 = "nonce3";
513
514            // Mark multiple nonces
515            storage.mark_nonce(nonce1).await.unwrap();
516            storage.mark_nonce(nonce2).await.unwrap();
517            storage.mark_nonce(nonce3).await.unwrap();
518
519            // Verify all exist
520            assert!(storage.has_nonce(nonce1).await.unwrap());
521            assert!(storage.has_nonce(nonce2).await.unwrap());
522            assert!(storage.has_nonce(nonce3).await.unwrap());
523
524            // Remove one
525            storage.remove_nonce(nonce2).await.unwrap();
526            assert!(!storage.has_nonce(nonce2).await.unwrap());
527            assert!(storage.has_nonce(nonce1).await.unwrap());
528            assert!(storage.has_nonce(nonce3).await.unwrap());
529
530            // Clean up
531            storage.remove_nonce(nonce1).await.unwrap();
532            storage.remove_nonce(nonce3).await.unwrap();
533        }
534    }
535}