33 * Manages blacklisted JWT tokens for secure logout and token invalidation
44 */
55
6- import { query } from '../config/database.js' ;
7- import logger from './logger.js' ;
6+ import { query } from '../config/database.js'
7+ import logger from './logger.js'
88
99// In-memory cache for frequently checked tokens (optional Redis could be used here)
10- const tokenCache = new Map ( ) ;
11- const CACHE_TTL = 5 * 60 * 1000 ; // 5 minutes
10+ const tokenCache = new Map ( )
11+ const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
1212
1313/**
1414 * Initialize token blacklist table
1515 */
16- export async function initializeTokenBlacklist ( ) {
16+ export async function initializeTokenBlacklist ( ) {
1717 try {
1818 await query ( `
1919 CREATE TABLE IF NOT EXISTS blacklisted_tokens (
@@ -23,174 +23,170 @@ export async function initializeTokenBlacklist() {
2323 blacklisted_at TIMESTAMPTZ DEFAULT NOW(),
2424 reason VARCHAR(100) DEFAULT 'logout'
2525 );
26- ` ) ;
26+ ` )
2727
2828 await query ( `
2929 CREATE INDEX IF NOT EXISTS idx_blacklisted_tokens_hash ON blacklisted_tokens(token_hash);
3030 CREATE INDEX IF NOT EXISTS idx_blacklisted_tokens_expires ON blacklisted_tokens(expires_at);
31- ` ) ;
31+ ` )
3232
3333 // Clean up expired tokens periodically
34- await cleanupExpiredTokens ( ) ;
34+ await cleanupExpiredTokens ( )
3535
36- logger . startup ( 'TokenBlacklist' , 'initialized' ) ;
36+ logger . startup ( 'TokenBlacklist' , 'initialized' )
3737 } catch ( error ) {
38- logger . error ( 'Failed to initialize token blacklist:' , error ) ;
39- throw error ;
38+ logger . error ( 'Failed to initialize token blacklist:' , error )
39+ throw error
4040 }
4141}
4242
4343/**
4444 * Add token to blacklist
4545 */
46- export async function blacklistToken ( token , expiresAt , reason = 'logout' ) {
46+ export async function blacklistToken ( token , expiresAt , reason = 'logout' ) {
4747 try {
4848 if ( ! token ) {
49- throw new Error ( 'Token is required' ) ;
49+ throw new Error ( 'Token is required' )
5050 }
5151
5252 // Hash the token for security (don't store full token)
53- const tokenHash = hashToken ( token ) ;
53+ const tokenHash = hashToken ( token )
5454
5555 // Convert Unix timestamp to Date if needed
5656 const expirationDate = typeof expiresAt === 'number'
5757 ? new Date ( expiresAt * 1000 )
58- : new Date ( expiresAt ) ;
58+ : new Date ( expiresAt )
5959
6060 await query ( `
6161 INSERT INTO blacklisted_tokens (token_hash, expires_at, reason)
6262 VALUES ($1, $2, $3)
6363 ON CONFLICT (token_hash) DO NOTHING
64- ` , [ tokenHash , expirationDate , reason ] ) ;
64+ ` , [ tokenHash , expirationDate , reason ] )
6565
6666 // Add to cache
6767 tokenCache . set ( tokenHash , {
6868 blacklisted : true ,
6969 expiresAt : expirationDate . getTime ( ) ,
7070 cachedAt : Date . now ( )
71- } ) ;
71+ } )
7272
7373 logger . audit ( 'TOKEN_BLACKLISTED' , {
7474 tokenHash : tokenHash . substring ( 0 , 8 ) + '...' ,
7575 reason,
7676 expiresAt : expirationDate
77- } ) ;
78-
77+ } )
7978 } catch ( error ) {
80- logger . error ( 'Failed to blacklist token:' , error ) ;
81- throw error ;
79+ logger . error ( 'Failed to blacklist token:' , error )
80+ throw error
8281 }
8382}
8483
8584/**
8685 * Check if token is blacklisted
8786 */
88- export async function isTokenBlacklisted ( token ) {
87+ export async function isTokenBlacklisted ( token ) {
8988 try {
9089 if ( ! token ) {
91- return false ;
90+ return false
9291 }
9392
94- const tokenHash = hashToken ( token ) ;
93+ const tokenHash = hashToken ( token )
9594
9695 // Check cache first
97- const cached = tokenCache . get ( tokenHash ) ;
96+ const cached = tokenCache . get ( tokenHash )
9897 if ( cached ) {
9998 // Check if cache entry is still valid
10099 if ( Date . now ( ) - cached . cachedAt < CACHE_TTL ) {
101100 // Check if token has expired
102101 if ( cached . expiresAt && Date . now ( ) > cached . expiresAt ) {
103- tokenCache . delete ( tokenHash ) ;
104- return false ;
102+ tokenCache . delete ( tokenHash )
103+ return false
105104 }
106- return cached . blacklisted ;
105+ return cached . blacklisted
107106 } else {
108107 // Cache entry is stale
109- tokenCache . delete ( tokenHash ) ;
108+ tokenCache . delete ( tokenHash )
110109 }
111110 }
112111
113112 // Check database
114113 const result = await query ( `
115114 SELECT 1 FROM blacklisted_tokens
116115 WHERE token_hash = $1 AND expires_at > NOW()
117- ` , [ tokenHash ] ) ;
116+ ` , [ tokenHash ] )
118117
119- const isBlacklisted = result . rows . length > 0 ;
118+ const isBlacklisted = result . rows . length > 0
120119
121120 // Cache the result
122121 tokenCache . set ( tokenHash , {
123122 blacklisted : isBlacklisted ,
124123 expiresAt : null , // We'll let the database handle expiration
125124 cachedAt : Date . now ( )
126- } ) ;
127-
128- return isBlacklisted ;
125+ } )
129126
127+ return isBlacklisted
130128 } catch ( error ) {
131- logger . error ( 'Failed to check token blacklist:' , error ) ;
129+ logger . error ( 'Failed to check token blacklist:' , error )
132130 // In case of error, allow the token (fail open for availability)
133- return false ;
131+ return false
134132 }
135133}
136134
137135/**
138136 * Blacklist all tokens for a user (useful for account compromise)
139137 */
140- export function blacklistAllUserTokens ( userId , reason = 'security_breach' ) {
138+ export function blacklistAllUserTokens ( userId , reason = 'security_breach' ) {
141139 try {
142140 // This would require storing user ID with tokens or implementing a different strategy
143141 // For now, we'll just log the action and rely on token expiration
144142 logger . audit ( 'ALL_USER_TOKENS_BLACKLISTED' , {
145143 userId,
146144 reason
147- } ) ;
145+ } )
148146
149147 // In a more sophisticated implementation, you might:
150148 // 1. Store user_id with tokens
151149 // 2. Maintain a user blacklist with timestamps
152150 // 3. Use Redis with user-specific keys
153-
154151 } catch ( error ) {
155- logger . error ( 'Failed to blacklist all user tokens:' , error ) ;
156- throw error ;
152+ logger . error ( 'Failed to blacklist all user tokens:' , error )
153+ throw error
157154 }
158155}
159156
160157/**
161158 * Clean up expired tokens from database and cache
162159 */
163- export async function cleanupExpiredTokens ( ) {
160+ export async function cleanupExpiredTokens ( ) {
164161 try {
165162 const result = await query ( `
166163 DELETE FROM blacklisted_tokens
167164 WHERE expires_at <= NOW()
168- ` ) ;
165+ ` )
169166
170167 if ( result . rowCount > 0 ) {
171- logger . info ( `Cleaned up ${ result . rowCount } expired blacklisted tokens` ) ;
168+ logger . info ( `Cleaned up ${ result . rowCount } expired blacklisted tokens` )
172169 }
173170
174171 // Clean up expired cache entries
175- const now = Date . now ( ) ;
172+ const now = Date . now ( )
176173 for ( const [ tokenHash , entry ] of tokenCache . entries ( ) ) {
177174 if ( entry . expiresAt && now > entry . expiresAt ) {
178- tokenCache . delete ( tokenHash ) ;
175+ tokenCache . delete ( tokenHash )
179176 } else if ( now - entry . cachedAt > CACHE_TTL * 2 ) {
180177 // Remove stale cache entries
181- tokenCache . delete ( tokenHash ) ;
178+ tokenCache . delete ( tokenHash )
182179 }
183180 }
184-
185181 } catch ( error ) {
186- logger . error ( 'Failed to cleanup expired tokens:' , error ) ;
182+ logger . error ( 'Failed to cleanup expired tokens:' , error )
187183 }
188184}
189185
190186/**
191187 * Get blacklist statistics
192188 */
193- export async function getBlacklistStats ( ) {
189+ export async function getBlacklistStats ( ) {
194190 try {
195191 const result = await query ( `
196192 SELECT
@@ -199,94 +195,91 @@ export async function getBlacklistStats() {
199195 MIN(blacklisted_at) as oldest_entry,
200196 MAX(blacklisted_at) as newest_entry
201197 FROM blacklisted_tokens
202- ` ) ;
198+ ` )
203199
204- const stats = result . rows [ 0 ] ;
200+ const stats = result . rows [ 0 ]
205201
206202 return {
207203 totalBlacklisted : parseInt ( stats . total_blacklisted ) ,
208204 activeBlacklisted : parseInt ( stats . active_blacklisted ) ,
209205 oldestEntry : stats . oldest_entry ,
210206 newestEntry : stats . newest_entry ,
211207 cacheSize : tokenCache . size
212- } ;
213-
208+ }
214209 } catch ( error ) {
215- logger . error ( 'Failed to get blacklist stats:' , error ) ;
216- return null ;
210+ logger . error ( 'Failed to get blacklist stats:' , error )
211+ return null
217212 }
218213}
219214
220215/**
221216 * Hash token for secure storage
222217 */
223- function hashToken ( token ) {
224- const crypto = require ( 'crypto' ) ;
225- return crypto . createHash ( 'sha256' ) . update ( token ) . digest ( 'hex' ) ;
218+ function hashToken ( token ) {
219+ const crypto = require ( 'crypto' )
220+ return crypto . createHash ( 'sha256' ) . update ( token ) . digest ( 'hex' )
226221}
227222
228223/**
229224 * Schedule periodic cleanup
230225 */
231- export function scheduleTokenCleanup ( ) {
226+ export function scheduleTokenCleanup ( ) {
232227 // Clean up every hour
233228 setInterval ( async ( ) => {
234229 try {
235- await cleanupExpiredTokens ( ) ;
230+ await cleanupExpiredTokens ( )
236231 } catch ( error ) {
237- logger . error ( 'Scheduled token cleanup failed:' , error ) ;
232+ logger . error ( 'Scheduled token cleanup failed:' , error )
238233 }
239- } , 60 * 60 * 1000 ) ; // 1 hour
234+ } , 60 * 60 * 1000 ) // 1 hour
240235
241- logger . startup ( 'TokenBlacklist' , 'cleanup scheduled' ) ;
236+ logger . startup ( 'TokenBlacklist' , 'cleanup scheduled' )
242237}
243238
244239/**
245240 * Clear all blacklisted tokens (use with caution)
246241 */
247- export async function clearAllBlacklistedTokens ( ) {
242+ export async function clearAllBlacklistedTokens ( ) {
248243 try {
249244 const result = await query ( `
250245 DELETE FROM blacklisted_tokens
251- ` ) ;
246+ ` )
252247
253248 // Clear cache
254- tokenCache . clear ( ) ;
249+ tokenCache . clear ( )
255250
256251 logger . audit ( 'ALL_BLACKLISTED_TOKENS_CLEARED' , {
257252 tokensCleared : result . rowCount
258- } ) ;
259-
260- return result . rowCount ;
253+ } )
261254
255+ return result . rowCount
262256 } catch ( error ) {
263- logger . error ( 'Failed to clear all blacklisted tokens:' , error ) ;
264- throw error ;
257+ logger . error ( 'Failed to clear all blacklisted tokens:' , error )
258+ throw error
265259 }
266260}
267261
268262/**
269263 * Get recently blacklisted tokens (for monitoring)
270264 */
271- export async function getRecentlyBlacklistedTokens ( limit = 100 ) {
265+ export async function getRecentlyBlacklistedTokens ( limit = 100 ) {
272266 try {
273267 const result = await query ( `
274268 SELECT token_hash, blacklisted_at, expires_at, reason
275269 FROM blacklisted_tokens
276270 ORDER BY blacklisted_at DESC
277271 LIMIT $1
278- ` , [ limit ] ) ;
272+ ` , [ limit ] )
279273
280274 return result . rows . map ( row => ( {
281275 tokenHash : row . token_hash . substring ( 0 , 8 ) + '...' , // Partial hash for security
282276 blacklistedAt : row . blacklisted_at ,
283277 expiresAt : row . expires_at ,
284278 reason : row . reason
285- } ) ) ;
286-
279+ } ) )
287280 } catch ( error ) {
288- logger . error ( 'Failed to get recently blacklisted tokens:' , error ) ;
289- return [ ] ;
281+ logger . error ( 'Failed to get recently blacklisted tokens:' , error )
282+ return [ ]
290283 }
291284}
292285
@@ -300,4 +293,4 @@ export default {
300293 scheduleTokenCleanup,
301294 clearAllBlacklistedTokens,
302295 getRecentlyBlacklistedTokens
303- } ;
296+ }
0 commit comments