dmsc/auth/revocation.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # JWT Token Revocation Module
19//!
20//! This module provides JWT token revocation functionality, including:
21//! - Token blacklist management
22//! - User-based token revocation
23//! - Revoked token validation
24//!
25//! ## Security Considerations
26//!
27//! - Revoked tokens are stored in-memory and will be lost on restart
28//! - For production use, integrate with Redis or database-backed storage
29//! - Consider implementing token versioning for per-user revocation
30
31use dashmap::DashSet;
32use std::time::{SystemTime, UNIX_EPOCH};
33use std::vec::Vec;
34use uuid::Uuid;
35
36/// Information about a revoked JWT token.
37///
38/// This struct stores metadata about a token that has been revoked,
39/// including when it was revoked, when it expires, and the reason for revocation.
40#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
41#[derive(Debug, Clone)]
42pub struct DMSCRevokedTokenInfo {
43 /// Unique identifier for the revocation record
44 pub token_id: String,
45 /// User ID associated with the revoked token
46 pub user_id: String,
47 /// Unix timestamp when the token was revoked
48 pub revoked_at: u64,
49 /// Unix timestamp when the token expires (may differ from original token expiry)
50 pub expires_at: u64,
51 /// Optional reason for revocation (e.g., "user_logout", "security_breach")
52 pub reason: Option<String>,
53}
54
55#[cfg(feature = "pyo3")]
56#[pyo3::prelude::pymethods]
57impl DMSCRevokedTokenInfo {
58 #[new]
59 fn py_new(
60 token_id: String,
61 user_id: String,
62 revoked_at: u64,
63 expires_at: u64,
64 reason: Option<String>,
65 ) -> Self {
66 Self {
67 token_id,
68 user_id,
69 revoked_at,
70 expires_at,
71 reason,
72 }
73 }
74}
75
76/// JWT token revocation list for managing invalidated tokens.
77///
78/// This struct provides functionality to revoke JWT tokens and check if tokens
79/// have been revoked. It uses concurrent data structures for thread-safe access.
80///
81/// ## Usage
82///
83/// The revocation list can be used to implement token invalidation scenarios:
84/// - User logout (revoke specific token)
85/// - Password change (revoke all user tokens)
86/// - Security incidents (bulk revocation)
87///
88/// ## Storage
89///
90/// By default, revoked tokens are stored in-memory. For production use,
91/// consider integrating with Redis or a database-backed storage solution
92/// to persist revocations across application restarts.
93#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
94pub struct DMSCJWTRevocationList {
95 /// Set of revoked token strings for O(1) lookup
96 revoked_tokens: DashSet<String>,
97 /// Map of token string to DMSCRevokedTokenInfo for metadata storage
98 token_info: DashMap<String, DMSCRevokedTokenInfo>,
99 /// Maximum number of revoked tokens to store
100 max_tokens: usize,
101}
102
103use dashmap::DashMap;
104
105/// Default maximum number of revoked tokens to store in the list.
106const DEFAULT_MAX_REVOKED_TOKENS: usize = 10000;
107
108impl DMSCJWTRevocationList {
109 /// Creates a new JWT revocation list with default capacity.
110 ///
111 /// This constructor initializes an empty revocation list with the default
112 /// maximum capacity for storing revoked tokens.
113 ///
114 /// # Returns
115 ///
116 /// A new instance of `DMSCJWTRevocationList`
117 pub fn new() -> Self {
118 Self {
119 revoked_tokens: DashSet::new(),
120 token_info: DashMap::new(),
121 max_tokens: DEFAULT_MAX_REVOKED_TOKENS,
122 }
123 }
124
125 /// Creates a new JWT revocation list with specified capacity.
126 ///
127 /// This constructor allows specifying the maximum number of revoked tokens
128 /// that can be stored. When the capacity is exceeded, oldest revoked tokens
129 /// are automatically removed.
130 ///
131 /// # Parameters
132 ///
133 /// - `capacity`: Maximum number of revoked tokens to store
134 ///
135 /// # Returns
136 ///
137 /// A new instance of `DMSCJWTRevocationList` with specified capacity
138 pub fn with_capacity(capacity: usize) -> Self {
139 Self {
140 revoked_tokens: DashSet::with_capacity(capacity),
141 token_info: DashMap::with_capacity(capacity),
142 max_tokens: capacity,
143 }
144 }
145}
146
147#[cfg(feature = "pyo3")]
148#[pyo3::prelude::pymethods]
149impl DMSCJWTRevocationList {
150 #[new]
151 fn py_new() -> Self {
152 Self {
153 revoked_tokens: DashSet::new(),
154 token_info: DashMap::new(),
155 max_tokens: DEFAULT_MAX_REVOKED_TOKENS,
156 }
157 }
158
159 #[pyo3(name = "get_revoked_count")]
160 fn get_revoked_count_impl(&self) -> usize {
161 self.revoked_tokens.len()
162 }
163
164 #[pyo3(name = "is_revoked")]
165 fn is_revoked_impl(&self, token: &str) -> bool {
166 self.revoked_tokens.contains(token)
167 }
168
169 #[pyo3(name = "revoke_token")]
170 fn revoke_token_impl(&self, token: String, user_id: String, reason: Option<String>, ttl_secs: u64) {
171 self.revoke_token(&token, &user_id, reason, ttl_secs);
172 }
173
174 #[pyo3(name = "revoke_by_user")]
175 fn revoke_by_user_impl(&self, user_id: &str) -> bool {
176 let mut removed = false;
177
178 let to_remove: Vec<String> = self.token_info
179 .iter()
180 .filter(|x| x.user_id == user_id)
181 .map(|x| x.token_id.clone())
182 .collect();
183
184 for token in to_remove {
185 self.revoked_tokens.remove(&token);
186 self.token_info.remove(&token);
187 removed = true;
188 }
189 removed
190 }
191
192 #[pyo3(name = "cleanup")]
193 fn cleanup_impl(&self) -> usize {
194 let now = SystemTime::now()
195 .duration_since(UNIX_EPOCH)
196 .unwrap()
197 .as_secs();
198
199 let expired: Vec<String> = self.token_info
200 .iter()
201 .filter(|x| x.expires_at <= now)
202 .map(|x| x.token_id.clone())
203 .collect();
204
205 for token in &expired {
206 self.revoked_tokens.remove(token);
207 self.token_info.remove(token);
208 }
209 expired.len()
210 }
211}
212
213impl DMSCJWTRevocationList {
214 /// Revokes a specific JWT token.
215 ///
216 /// This method adds a token to the revocation list with associated metadata.
217 /// The token will be considered invalid for the specified time-to-live duration.
218 ///
219 /// ## Automatic Cleanup
220 ///
221 /// After revocation, expired tokens are automatically cleaned up if the
222 /// revocation list exceeds its maximum capacity.
223 ///
224 /// # Parameters
225 ///
226 /// - `token`: The JWT token string to revoke
227 /// - `user_id`: The user ID associated with the token
228 /// - `reason`: Optional reason for revocation
229 /// - `ttl_secs`: Time-to-live in seconds for this revocation record
230 pub fn revoke_token(
231 &self,
232 token: &str,
233 user_id: &str,
234 reason: Option<String>,
235 ttl_secs: u64,
236 ) {
237 let now = SystemTime::now()
238 .duration_since(UNIX_EPOCH)
239 .map_or(0, |d| d.as_secs());
240
241 self.revoked_tokens.insert(token.to_string());
242
243 let info = DMSCRevokedTokenInfo {
244 token_id: Uuid::new_v4().to_string(),
245 user_id: user_id.to_string(),
246 revoked_at: now,
247 expires_at: now + ttl_secs,
248 reason,
249 };
250
251 self.token_info.insert(token.to_string(), info);
252
253 self.cleanup_expired();
254 }
255
256 /// Revokes all tokens for a specific user.
257 ///
258 /// This method finds all tokens associated with the given user ID and
259 /// marks them as revoked. Useful for implementing "logout everywhere"
260 /// functionality or revoking tokens after a security incident.
261 ///
262 /// # Parameters
263 ///
264 /// - `user_id`: The user ID whose tokens should be revoked
265 /// - `reason`: Optional reason for the mass revocation
266 ///
267 /// # Returns
268 ///
269 /// The number of tokens that were revoked
270 pub fn revoke_all_user_tokens(&self, user_id: &str, reason: Option<String>) -> usize {
271 let mut count = 0;
272 let now = SystemTime::now()
273 .duration_since(UNIX_EPOCH)
274 .map_or(0, |d| d.as_secs());
275
276 for entry in self.token_info.iter() {
277 let info = entry.value();
278 if info.user_id == user_id {
279 self.revoked_tokens.insert(entry.key().clone());
280
281 let updated_info = DMSCRevokedTokenInfo {
282 token_id: info.token_id.clone(),
283 user_id: info.user_id.clone(),
284 revoked_at: info.revoked_at,
285 expires_at: now + 86400,
286 reason: reason.clone(),
287 };
288
289 self.token_info.insert(entry.key().clone(), updated_info);
290 count += 1;
291 }
292 }
293
294 count
295 }
296
297 /// Checks if a token has been revoked.
298 ///
299 /// This method performs an O(1) lookup to determine if a token exists
300 /// in the revocation list. If found, it also checks if the revocation
301 /// record has expired and removes it if so.
302 ///
303 /// ## Expiration Handling
304 ///
305 /// If the token is found but its revocation record has expired,
306 /// the token is automatically removed and treated as not revoked.
307 ///
308 /// # Parameters
309 ///
310 /// - `token`: The JWT token string to check
311 ///
312 /// # Returns
313 ///
314 /// `true` if the token is revoked and valid, `false` otherwise
315 pub fn is_revoked(&self, token: &str) -> bool {
316 if self.revoked_tokens.contains(token) {
317 if let Some(info) = self.token_info.get(token) {
318 let now = SystemTime::now()
319 .duration_since(UNIX_EPOCH)
320 .map_or(0, |d| d.as_secs());
321
322 if now > info.expires_at {
323 self.remove_revoked_token(token);
324 return false;
325 }
326 }
327 return true;
328 }
329 false
330 }
331
332 /// Retrieves revocation information for a specific token.
333 ///
334 /// This method returns the metadata associated with a revoked token,
335 /// including when it was revoked and the reason (if provided).
336 ///
337 /// # Parameters
338 ///
339 /// - `token`: The JWT token string to look up
340 ///
341 /// # Returns
342 ///
343 /// `Some(DMSCRevokedTokenInfo)` if the token is revoked, `None` otherwise
344 pub fn get_revocation_info(&self, token: &str) -> Option<DMSCRevokedTokenInfo> {
345 self.token_info.get(token).map(|i| i.clone())
346 }
347
348 /// Removes a single revoked token from the list.
349 ///
350 /// This is an internal method used for cleanup operations.
351 ///
352 /// # Parameters
353 ///
354 /// - `token`: The JWT token string to remove
355 fn remove_revoked_token(&self, token: &str) {
356 self.revoked_tokens.remove(token);
357 self.token_info.remove(token);
358 }
359
360 /// Removes all expired revocation records from the list.
361 ///
362 /// This internal method is called after token revocation to clean up
363 /// expired entries and enforce the maximum capacity limit.
364 ///
365 /// ## Cleanup Criteria
366 ///
367 /// - Removes all revocation records where the expiry time has passed
368 /// - If capacity is exceeded, removes oldest entries first
369 fn cleanup_expired(&self) {
370 let now = SystemTime::now()
371 .duration_since(UNIX_EPOCH)
372 .map_or(0, |d| d.as_secs());
373
374 let tokens_to_remove: Vec<String> = self
375 .token_info
376 .iter()
377 .filter(|entry| entry.expires_at <= now)
378 .map(|entry| entry.key().clone())
379 .collect();
380
381 for token in tokens_to_remove {
382 self.remove_revoked_token(&token);
383 }
384
385 while self.revoked_tokens.len() > self.max_tokens {
386 if let Some(entry) = self.token_info.iter().next() {
387 self.remove_revoked_token(entry.key());
388 } else {
389 break;
390 }
391 }
392 }
393
394 /// Returns the current count of revoked tokens.
395 ///
396 /// This method provides the number of tokens currently in the revocation list.
397 ///
398 /// # Returns
399 ///
400 /// The number of revoked tokens stored
401 pub fn get_revoked_count(&self) -> usize {
402 self.revoked_tokens.len()
403 }
404
405 /// Clears all revoked tokens from the list.
406 ///
407 /// This method removes all entries from both the revoked tokens set
408 /// and the token info map. Use with caution as it cannot be undone.
409 pub fn clear(&self) {
410 self.revoked_tokens.clear();
411 self.token_info.clear();
412 }
413}
414
415impl Default for DMSCJWTRevocationList {
416 fn default() -> Self {
417 Self::new()
418 }
419}