Skip to main content

rustauth_redis/
lib.rs

1//! Redis-backed integrations for RustAuth.
2//!
3//! The rate limit store uses `redis-rs` with the async
4//! `redis::aio::ConnectionManager`, RESP-compatible Redis or Valkey servers,
5//! Lua scripting for atomic consume decisions, and core commands that are
6//! shared by Redis and Valkey.
7
8mod bundle;
9mod rate_limit;
10mod secondary;
11mod url;
12
13pub use bundle::{RedisOptions, RedisRustAuthOptions, RedisRustAuthStores, RedisStores};
14pub use rate_limit::{RedisRateLimitOptions, RedisRateLimitStore};
15pub use secondary::{RedisSecondaryStorage, RedisSecondaryStorageOptions};
16
17/// Current crate version.
18pub const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20pub(crate) async fn connect_manager(
21    redis_url: &str,
22) -> Result<redis::aio::ConnectionManager, rustauth_core::error::RustAuthError> {
23    let redis_url = url::normalize_redis_url(redis_url);
24    let client = redis::Client::open(redis_url.as_ref())
25        .map_err(|error| rustauth_core::error::RustAuthError::Adapter(error.to_string()))?;
26    redis::aio::ConnectionManager::new(client)
27        .await
28        .map_err(|error| rustauth_core::error::RustAuthError::Adapter(error.to_string()))
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn normalizes_valkey_urls_to_redis_urls() {
37        assert_eq!(
38            url::normalize_redis_url("valkey://localhost:6379").as_ref(),
39            "redis://localhost:6379"
40        );
41        assert_eq!(
42            url::normalize_redis_url("valkeys://localhost:6380").as_ref(),
43            "rediss://localhost:6380"
44        );
45    }
46
47    #[test]
48    fn leaves_non_valkey_urls_unchanged() {
49        assert_eq!(
50            url::normalize_redis_url("redis://localhost:6379").as_ref(),
51            "redis://localhost:6379"
52        );
53        assert_eq!(
54            url::normalize_redis_url("rediss://localhost:6380").as_ref(),
55            "rediss://localhost:6380"
56        );
57        assert_eq!(
58            url::normalize_redis_url("unix:///tmp/redis.sock").as_ref(),
59            "unix:///tmp/redis.sock"
60        );
61    }
62
63    #[test]
64    fn rate_limit_script_uses_current_hash_set_command() {
65        use crate::rate_limit::RATE_LIMIT_SCRIPT;
66
67        assert!(RATE_LIMIT_SCRIPT.contains("HSET"));
68        assert!(!RATE_LIMIT_SCRIPT.contains("HMSET"));
69    }
70
71    #[test]
72    fn rate_limit_script_resets_only_after_window_elapses() {
73        use crate::rate_limit::RATE_LIMIT_SCRIPT;
74
75        assert!(RATE_LIMIT_SCRIPT.contains("(now - last_request) > window"));
76        assert!(!RATE_LIMIT_SCRIPT.contains("(now - last_request) >= window"));
77    }
78
79    #[test]
80    fn scan_pattern_escapes_redis_glob_metacharacters() {
81        use crate::url::secondary_storage_scan_pattern;
82
83        assert_eq!(
84            secondary_storage_scan_pattern(r"tenant:*?[]\:"),
85            r"tenant:\*\?\[\]\\:*"
86        );
87    }
88
89    #[test]
90    fn secondary_storage_uses_separate_key_namespace() {
91        let options = RedisSecondaryStorageOptions {
92            key_prefix: "test:".to_owned(),
93            scan_count: 100,
94        };
95        let key = format!("{}secondary:{}", options.key_prefix, "session:token");
96
97        assert_eq!(key, "test:secondary:session:token");
98    }
99
100    #[cfg(any(feature = "rustls", feature = "native-tls"))]
101    #[test]
102    fn tls_urls_open_as_tls_connections() -> Result<(), redis::RedisError> {
103        for url in ["rediss://localhost:6379", "valkeys://localhost:6380"] {
104            let client = redis::Client::open(url::normalize_redis_url(url).as_ref())?;
105            assert!(
106                matches!(
107                    client.get_connection_info().addr,
108                    redis::ConnectionAddr::TcpTls { .. }
109                ),
110                "{url} should open as a TLS connection"
111            );
112        }
113        Ok(())
114    }
115}