From 6d3a2d279a3150dc14fa00032e6eecb2d2dc1f71 Mon Sep 17 00:00:00 2001 From: Alexander Petric Date: Thu, 5 Jun 2025 03:30:23 -0400 Subject: [PATCH] invalidate auth cache when logging out (#5869) * invalidate auth cache when logging out * use proper sqlx migrate add * formatting * pg notify on token, not email --- ...604181226_auth_cache_invalidation.down.sql | 4 +++ ...50604181226_auth_cache_invalidation.up.sql | 17 ++++++++++++ backend/src/main.rs | 9 +++++-- backend/windmill-api/src/auth.rs | 26 ++++++++++++++----- backend/windmill-api/src/lib.rs | 2 +- 5 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/20250604181226_auth_cache_invalidation.down.sql create mode 100644 backend/migrations/20250604181226_auth_cache_invalidation.up.sql diff --git a/backend/migrations/20250604181226_auth_cache_invalidation.down.sql b/backend/migrations/20250604181226_auth_cache_invalidation.down.sql new file mode 100644 index 0000000000..b7f0f12204 --- /dev/null +++ b/backend/migrations/20250604181226_auth_cache_invalidation.down.sql @@ -0,0 +1,4 @@ +-- Remove token invalidation notification trigger + +DROP TRIGGER IF EXISTS token_invalidation_trigger ON token; +DROP FUNCTION IF EXISTS notify_token_invalidation(); diff --git a/backend/migrations/20250604181226_auth_cache_invalidation.up.sql b/backend/migrations/20250604181226_auth_cache_invalidation.up.sql new file mode 100644 index 0000000000..11be3961c0 --- /dev/null +++ b/backend/migrations/20250604181226_auth_cache_invalidation.up.sql @@ -0,0 +1,17 @@ +-- Add token invalidation notification trigger + +CREATE OR REPLACE FUNCTION notify_token_invalidation() +RETURNS TRIGGER AS $$ +BEGIN + -- Only notify for session token deletions when the invalidation settings are enabled + IF OLD.label = 'session' AND OLD.email IS NOT NULL THEN + PERFORM pg_notify('notify_token_invalidation', OLD.token); + END IF; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER token_invalidation_trigger +AFTER DELETE ON token +FOR EACH ROW +EXECUTE FUNCTION notify_token_invalidation(); diff --git a/backend/src/main.rs b/backend/src/main.rs index f99f9c7b27..d9283f6d9c 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -861,6 +861,11 @@ Windmill Community Edition {GIT_VERSION} } }; }, + "notify_token_invalidation" => { + let token = n.payload(); + tracing::info!("Token invalidation detected for token: {}...", &token[..token.len().min(8)]); + windmill_api::auth::invalidate_token_from_cache(token); + }, "notify_global_setting_change" => { tracing::info!("Global setting change detected: {}", n.payload()); match n.payload() { @@ -893,7 +898,7 @@ Windmill Community Edition {GIT_VERSION} if let Err(e) = load_tag_per_workspace_workspaces(&db).await { tracing::error!("Error loading default tag per workspace workspaces: {e:#}"); } - } + }, SMTP_SETTING => { reload_smtp_config(&db).await; }, @@ -1010,7 +1015,6 @@ Windmill Community Edition {GIT_VERSION} tracing::error!(error = %e, "Could not reload critical alert UI setting"); } }, - a @_ => { tracing::info!("Unrecognized Global Setting Change Payload: {:?}", a); } @@ -1183,6 +1187,7 @@ async fn listen_pg(url: &str) -> Option { "notify_webhook_change", "notify_workspace_envs_change", "notify_runnable_version_change", + "notify_token_invalidation", ]; #[cfg(feature = "http_trigger")] diff --git a/backend/windmill-api/src/auth.rs b/backend/windmill-api/src/auth.rs index 25dc0efd4a..6af5ea1e50 100644 --- a/backend/windmill-api/src/auth.rs +++ b/backend/windmill-api/src/auth.rs @@ -26,6 +26,20 @@ use windmill_common::{ users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL}, }; +lazy_static::lazy_static! { + // Global auth cache accessible from main.rs for direct invalidation + pub static ref AUTH_CACHE: Cache<(String, String), ExpiringAuthCache> = Cache::new(300); +} + +// Global function to invalidate a specific token from cache +pub fn invalidate_token_from_cache(token: &str) { + // Remove all cache entries for this token (across all workspaces) + AUTH_CACHE.retain(|(_workspace_id, cached_token), _cached_value| { + cached_token != token + }); + tracing::info!("Invalidated token from auth cache: {}...", &token[..token.len().min(8)]); +} + #[derive(Clone)] pub struct ExpiringAuthCache { pub authed: ApiAuthed, @@ -33,7 +47,6 @@ pub struct ExpiringAuthCache { } pub struct AuthCache { - cache: Cache<(String, String), ExpiringAuthCache>, db: DB, superadmin_secret: Option, #[cfg(feature = "enterprise")] @@ -47,7 +60,6 @@ impl AuthCache { #[cfg(feature = "enterprise")] ext_jwks: Option>>, ) -> Self { AuthCache { - cache: Cache::new(300), db, superadmin_secret, #[cfg(feature = "enterprise")] @@ -56,7 +68,7 @@ impl AuthCache { } pub async fn invalidate(&self, w_id: &str, token: String) { - self.cache.remove(&(w_id.to_string(), token)); + AUTH_CACHE.remove(&(w_id.to_string(), token)); } pub async fn get_authed(&self, w_id: Option, token: &str) -> Option { @@ -64,7 +76,7 @@ impl AuthCache { w_id.as_ref().unwrap_or(&"".to_string()).to_string(), token.to_string(), ); - let s = self.cache.get(&key).map(|c| c.to_owned()); + let s = AUTH_CACHE.get(&key).map(|c| c.to_owned()); match s { Some(ExpiringAuthCache { authed, expiry }) if expiry > chrono::Utc::now() => { Some(authed) @@ -86,7 +98,7 @@ impl AuthCache { }; if let Some((authed, exp)) = authed_and_exp.clone() { - self.cache.insert( + AUTH_CACHE.insert( key, ExpiringAuthCache { authed: authed.clone(), @@ -123,7 +135,7 @@ impl AuthCache { username_override, }; - self.cache.insert( + AUTH_CACHE.insert( key, ExpiringAuthCache { authed: authed.clone(), @@ -317,7 +329,7 @@ impl AuthCache { } }; if let Some(authed) = authed_o.as_ref() { - self.cache.insert( + AUTH_CACHE.insert( key, ExpiringAuthCache { authed: authed.clone(), diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 19b17e4d9d..8a42902508 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -69,7 +69,7 @@ mod ai; mod apps; pub mod args; mod audit; -mod auth; +pub mod auth; mod capture; mod concurrency_groups; mod configs;