Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced pre-existing formatting drift across the agent and server crates. Apply rustfmt across the workspace so the codebase meets its own CI gate. Pure formatting; no logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
172 lines
4.7 KiB
Rust
172 lines
4.7 KiB
Rust
//! 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<RwLock<HashSet<String>>>,
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|