From cb739ebe6d6b5f1cf281c9bdce3b1ed77201e402 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 4 Dec 2025 18:11:27 +0100 Subject: [PATCH] Can use Redis to store user sessions --- Cargo.lock | 55 +++++++++++++++++ Cargo.toml | 2 +- sample_upstream_provider/docker-compose.yaml | 16 +++++ sample_upstream_provider/redis/redis.conf | 3 + src/data/app_config.rs | 39 ++++++++++++ src/main.rs | 20 ++++++- src/middlewares/mod.rs | 1 + src/middlewares/multi_session_store.rs | 63 ++++++++++++++++++++ 8 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 sample_upstream_provider/redis/redis.conf create mode 100644 src/middlewares/multi_session_store.rs diff --git a/Cargo.lock b/Cargo.lock index c6c4a5b..c8fd840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,7 @@ dependencies = [ "anyhow", "derive_more", "rand 0.9.2", + "redis", "serde", "serde_json", "tracing", @@ -395,6 +396,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.9" @@ -506,6 +513,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -854,6 +870,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1323,6 +1353,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", + "futures-sink", "futures-task", "pin-project-lite", "pin-utils", @@ -2593,6 +2624,30 @@ version = "0.10.0-rc-2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "104a23e4e8b77312a823b6b5613edbac78397e2f34320bc7ac4277013ec4478e" +[[package]] +name = "redis" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44" +dependencies = [ + "arc-swap", + "backon", + "bytes", + "cfg-if", + "combine", + "futures-channel", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "socket2 0.6.1", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.18" diff --git a/Cargo.toml b/Cargo.toml index c3e3bed..9feb174 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2024" actix = "0.13.5" actix-identity = "0.9.0" actix-web = "4.12.1" -actix-session = { version = "0.11.0", features = ["cookie-session"] } +actix-session = { version = "0.11.0", features = ["cookie-session", "redis-session"] } actix-remote-ip = "0.1.0" clap = { version = "4.5.53", features = ["derive", "env"] } include_dir = "0.7.4" diff --git a/sample_upstream_provider/docker-compose.yaml b/sample_upstream_provider/docker-compose.yaml index 11d8e65..ec5cf46 100644 --- a/sample_upstream_provider/docker-compose.yaml +++ b/sample_upstream_provider/docker-compose.yaml @@ -1,4 +1,15 @@ services: + redis: + image: redis + network_mode: host + volumes: + - ./redis/redis.conf:/redis.conf:ro + command: [ "redis-server", "/redis.conf" ] + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 10s + timeout: 30s + retries: 3 upstream: image: dexidp/dex user: "1000" @@ -33,9 +44,14 @@ services: image: rust user: "1000" network_mode: host + depends_on: + redis: + condition: service_healthy environment: - STORAGE_PATH=/storage - DISABLE_LOCAL_LOGIN=true + - USE_REDIS=true + - REDIS_PASSWORD=secretsecret #- RUST_LOG=debug volumes: - ../:/app diff --git a/sample_upstream_provider/redis/redis.conf b/sample_upstream_provider/redis/redis.conf new file mode 100644 index 0000000..1eac0ae --- /dev/null +++ b/sample_upstream_provider/redis/redis.conf @@ -0,0 +1,3 @@ +maxmemory 256mb +maxmemory-policy allkeys-lru +requirepass secretsecret \ No newline at end of file diff --git a/src/data/app_config.rs b/src/data/app_config.rs index 1f06b48..d5b60de 100644 --- a/src/data/app_config.rs +++ b/src/data/app_config.rs @@ -67,6 +67,30 @@ pub struct AppConfig { /// to authenticate #[arg(long, env)] pub disable_local_login: bool, + + /// Use Redis to store cookie sessions + #[arg(long, env)] + pub use_redis: bool, + + /// Redis connection hostname + #[clap(long, env, default_value = "localhost")] + redis_hostname: String, + + /// Redis connection port + #[clap(long, env, default_value_t = 6379)] + redis_port: u16, + + /// Redis database number + #[clap(long, env, default_value_t = 0)] + redis_db_number: i64, + + /// Redis username + #[clap(long, env)] + redis_username: Option, + + /// Redis password + #[clap(long, env, default_value = "secretredis")] + redis_password: String, } lazy_static::lazy_static! { @@ -137,6 +161,21 @@ impl AppConfig { let domain = self.domain_name(); domain.split_once(':').map(|i| i.0).unwrap_or(domain) } + + /// Get Redis connection configuration + pub fn redis_connection_string(&self) -> Option { + match self.use_redis { + true => Some(format!( + "redis://{}:{}@{}:{}/{}", + self.redis_username.as_deref().unwrap_or(""), + self.redis_password, + self.redis_hostname, + self.redis_port, + self.redis_db_number + )), + false => None, + } + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index f9177e2..82847f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use actix_identity::IdentityMiddleware; use actix_identity::config::LogoutBehavior; use actix_remote_ip::RemoteIPConfig; use actix_session::SessionMiddleware; -use actix_session::storage::CookieSessionStore; +use actix_session::storage::{CookieSessionStore, RedisSessionStore}; use actix_web::cookie::{Key, SameSite}; use actix_web::middleware::Logger; use actix_web::{App, HttpResponse, HttpServer, get, middleware, web}; @@ -26,6 +26,7 @@ use basic_oidc::data::provider::ProvidersManager; use basic_oidc::data::user::User; use basic_oidc::data::webauthn_manager::WebAuthManager; use basic_oidc::middlewares::auth_middleware::AuthMiddleware; +use basic_oidc::middlewares::multi_session_store::MultiSessionStore; #[get("/health")] async fn health() -> &'static str { @@ -69,6 +70,18 @@ async fn main() -> std::io::Result<()> { .expect("Failed to change default admin password!"); } + let redis_store = match AppConfig::get().redis_connection_string() { + Some(s) => { + log::info!("Connect to Redis session store..."); + Some( + RedisSessionStore::new(s) + .await + .expect("Failed to connect to Redis!"), + ) + } + None => None, + }; + let users_actor = UsersActor::new(users).start(); let bruteforce_actor = BruteForceActor::default().start(); let providers_states_actor = ProvidersStatesActor::default().start(); @@ -91,7 +104,10 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { let session_mw = SessionMiddleware::builder( - CookieSessionStore::default(), + match redis_store.clone() { + None => MultiSessionStore::Cookie(CookieSessionStore::default()), + Some(s) => MultiSessionStore::Redis(s), + }, Key::from(config.token_key.as_bytes()), ) .cookie_name(SESSION_COOKIE_NAME.to_string()) diff --git a/src/middlewares/mod.rs b/src/middlewares/mod.rs index 2a709c0..2007e4a 100644 --- a/src/middlewares/mod.rs +++ b/src/middlewares/mod.rs @@ -1 +1,2 @@ pub mod auth_middleware; +pub mod multi_session_store; diff --git a/src/middlewares/multi_session_store.rs b/src/middlewares/multi_session_store.rs new file mode 100644 index 0000000..906f48b --- /dev/null +++ b/src/middlewares/multi_session_store.rs @@ -0,0 +1,63 @@ +use actix_session::storage::{ + CookieSessionStore, LoadError, RedisSessionStore, SaveError, SessionKey, SessionStore, + UpdateError, +}; +use actix_web::cookie::time::Duration; +use jwt_simple::Error; +use std::collections::HashMap; + +type SessionState = HashMap; + +/// Session store multiplexer. +/// +/// Allows to easily support different store without having a lot of duplicate code +pub enum MultiSessionStore { + Cookie(CookieSessionStore), + Redis(RedisSessionStore), +} + +impl SessionStore for MultiSessionStore { + async fn load(&self, session_key: &SessionKey) -> Result, LoadError> { + match self { + MultiSessionStore::Cookie(c) => c.load(session_key).await, + MultiSessionStore::Redis(r) => r.load(session_key).await, + } + } + + async fn save( + &self, + session_state: SessionState, + ttl: &Duration, + ) -> Result { + match self { + MultiSessionStore::Cookie(c) => c.save(session_state, ttl).await, + MultiSessionStore::Redis(r) => r.save(session_state, ttl).await, + } + } + + async fn update( + &self, + session_key: SessionKey, + session_state: SessionState, + ttl: &Duration, + ) -> Result { + match self { + MultiSessionStore::Cookie(c) => c.update(session_key, session_state, ttl).await, + MultiSessionStore::Redis(r) => r.update(session_key, session_state, ttl).await, + } + } + + async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), Error> { + match self { + MultiSessionStore::Cookie(c) => c.update_ttl(session_key, ttl).await, + MultiSessionStore::Redis(r) => r.update_ttl(session_key, ttl).await, + } + } + + async fn delete(&self, session_key: &SessionKey) -> Result<(), Error> { + match self { + MultiSessionStore::Cookie(c) => c.delete(session_key).await, + MultiSessionStore::Redis(r) => r.delete(session_key).await, + } + } +}