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}