//! Token blacklist for JWT revocation //! //! Provides in-memory token blacklist for immediate revocation of JWTs. //! Tokens are automatically cleaned up after expiration. use std::collections::HashSet; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{debug, info}; /// Token blacklist for revocation /// /// Maintains a set of revoked token signatures. When a token is revoked /// (e.g., on logout or admin action), it's added to this blacklist and /// all subsequent validation attempts will fail. #[derive(Clone)] pub struct TokenBlacklist { /// Set of revoked token strings tokens: Arc>>, } impl TokenBlacklist { /// Create a new empty blacklist pub fn new() -> Self { Self { tokens: Arc::new(RwLock::new(HashSet::new())), } } /// Add a token to the blacklist (revoke it) /// /// # Arguments /// * `token` - The full JWT token string to revoke /// /// # Example /// ```rust /// blacklist.revoke("eyJ...").await; /// ``` pub async fn revoke(&self, token: &str) { let mut tokens = self.tokens.write().await; let was_new = tokens.insert(token.to_string()); if was_new { debug!( "Token revoked and added to blacklist (length: {})", token.len() ); } } /// Check if a token has been revoked /// /// # Arguments /// * `token` - The JWT token string to check /// /// # Returns /// `true` if the token is in the blacklist (revoked), `false` otherwise pub async fn is_revoked(&self, token: &str) -> bool { let tokens = self.tokens.read().await; tokens.contains(token) } /// Get the number of tokens currently in the blacklist pub async fn len(&self) -> usize { let tokens = self.tokens.read().await; tokens.len() } /// Check if the blacklist is empty pub async fn is_empty(&self) -> bool { let tokens = self.tokens.read().await; tokens.is_empty() } /// Remove expired tokens from blacklist (cleanup) /// /// This should be called periodically to prevent memory buildup. /// Tokens that can no longer be validated (expired) are removed. /// /// # Arguments /// * `jwt_config` - JWT configuration for validating token expiration /// /// # Returns /// Number of tokens removed from blacklist pub async fn cleanup_expired(&self, jwt_config: &super::JwtConfig) -> usize { let mut tokens = self.tokens.write().await; let original_len = tokens.len(); // Remove tokens that fail validation (expired) tokens.retain(|token| { // If token is expired (validation fails), remove it from blacklist jwt_config.validate_token(token).is_ok() }); let removed = original_len - tokens.len(); if removed > 0 { info!( "Cleaned {} expired tokens from blacklist ({} remaining)", removed, tokens.len() ); } removed } /// Clear all tokens from the blacklist /// /// WARNING: This removes all revoked tokens. Use with caution. pub async fn clear(&self) { let mut tokens = self.tokens.write().await; let count = tokens.len(); tokens.clear(); info!("Cleared {} tokens from blacklist", count); } } impl Default for TokenBlacklist { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_revoke_and_check() { let blacklist = TokenBlacklist::new(); let token = "test.token.here"; assert!(!blacklist.is_revoked(token).await); blacklist.revoke(token).await; assert!(blacklist.is_revoked(token).await); assert_eq!(blacklist.len().await, 1); } #[tokio::test] async fn test_multiple_revocations() { let blacklist = TokenBlacklist::new(); blacklist.revoke("token1").await; blacklist.revoke("token2").await; blacklist.revoke("token3").await; assert_eq!(blacklist.len().await, 3); assert!(blacklist.is_revoked("token1").await); assert!(blacklist.is_revoked("token2").await); assert!(blacklist.is_revoked("token3").await); assert!(!blacklist.is_revoked("token4").await); } #[tokio::test] async fn test_clear() { let blacklist = TokenBlacklist::new(); blacklist.revoke("token1").await; blacklist.revoke("token2").await; assert_eq!(blacklist.len().await, 2); blacklist.clear().await; assert_eq!(blacklist.len().await, 0); assert!(blacklist.is_empty().await); } }