auth.js
1 import crypto from "crypto"; 2 import { authService } from "@/services/userService.js"; 3 import { sendError, ErrorCodes, StatusCodes } from "@/utils/responses.js"; 4 import { logger } from "@/middleware/logging.js"; 5 import config from "@/config.js"; 6 import { timingSafeCompare } from "@/utils/cryptoUtils.js"; 7 import cache, { CacheEvents } from "@/utils/cache.js"; 8 9 const requestTracker = new Map(); 10 11 const invalidateUserCache = (userId) => { 12 const authDeleted = cache.invalidateByPattern("auth", `.*:${userId}:.*`); 13 const sessionDeleted = cache.invalidateByPattern("session", `.*:${userId}:.*`); 14 15 logger.info(`Invalidated ${authDeleted + sessionDeleted} cache entries for user ${userId}`); 16 17 cache.emit(CacheEvents.CACHE_INVALIDATED, { 18 type: "user", 19 userId, 20 entriesDeleted: authDeleted + sessionDeleted, 21 timestamp: new Date(), 22 }); 23 }; 24 25 const cleanupRequestTracker = () => { 26 const now = Date.now(); 27 const windowStart = now - (config.timeouts.short || 5000); 28 const maxAge = now - (config.timeouts.long || 300000); 29 let cleanedCount = 0; 30 31 for (const [key, value] of requestTracker.entries()) { 32 const hasRecentRequests = value.requests.some(t => t > windowStart); 33 const isExpired = !hasRecentRequests && (!value.blocked || now > value.blockedUntil); 34 const isVeryOld = value.requests.length > 0 && value.requests[0] < maxAge; 35 36 if (isExpired || isVeryOld) { 37 requestTracker.delete(key); 38 cleanedCount++; 39 } 40 } 41 42 if (cleanedCount > 0) { 43 logger.info(`Cleaned ${cleanedCount} expired entries from request tracker`); 44 } 45 46 if (requestTracker.size > config.security.auth.requestTrackerMaxSize) { 47 const entriesToKeep = config.security.auth.requestTrackerMaxSize * 0.8; 48 const sortedEntries = Array.from(requestTracker.entries()) 49 .sort((a, b) => { 50 const aLatest = Math.max(...a[1].requests, a[1].blockedUntil || 0); 51 const bLatest = Math.max(...b[1].requests, b[1].blockedUntil || 0); 52 return bLatest - aLatest; 53 }); 54 55 const toDelete = sortedEntries.slice(entriesToKeep); 56 toDelete.forEach(([key]) => requestTracker.delete(key)); 57 58 logger.warn(`Request tracker exceeded max size, removed ${toDelete.length} oldest entries`); 59 } 60 }; 61 62 setInterval(() => { 63 const stats = cache.getStats(); 64 logger.debug("Cache stats:", stats); 65 }, config.timeouts.veryLong || 300000); 66 67 const hashToken = (token) => { 68 return crypto.createHash("sha256").update(token).digest("hex"); 69 }; 70 71 const validateTokenFormat = (token) => { 72 if (!token || typeof token !== "string") { 73 return false; 74 } else if (token.length < 32 || token.length > 128) { 75 return false; 76 } else if (!/^[a-zA-Z0-9_-]+$/.test(token)) { 77 return false; 78 } else { 79 return true; 80 } 81 }; 82 83 const forceRevalidation = async (token, req) => { 84 try { 85 logger.warn("Forcing token revalidation due to potential cache poisoning", { 86 requestId: req.requestId, 87 ip: req.ip, 88 }); 89 90 const validation = await authService.validateSession(token); 91 92 if (validation.valid && validation.user && !validation.user.isBanned) { 93 const cacheKey = `token:${hashToken(token)}`; 94 cache.set("auth", cacheKey, { 95 valid: true, 96 user: validation.user, 97 userId: validation.user.id, 98 timestamp: Date.now(), 99 forceValidated: true, 100 }); 101 return validation.user; 102 } else if (validation.user && validation.user.isBanned) { 103 invalidateUserCache(validation.user.id); 104 throw new Error("Account is suspended"); 105 } 106 throw new Error("Invalid session"); 107 } catch (error) { 108 logger.error("Revalidation failed:", error); 109 throw error; 110 } 111 }; 112 113 const checkRequestRate = (identifier) => { 114 const now = Date.now(); 115 const windowStart = now - (config.timeouts.short || 5000); 116 let tracker = requestTracker.get(identifier); 117 if (!tracker) { 118 tracker = { requests: [], blocked: false, blockedUntil: 0 }; 119 requestTracker.set(identifier, tracker); 120 } 121 if (tracker.blocked && now < tracker.blockedUntil) { 122 return false; 123 } 124 tracker.blocked = false; 125 tracker.requests = tracker.requests.filter(t => t > windowStart); 126 if (tracker.requests.length >= config.security.auth.requestTrackerMaxRequestsPerSecond) { 127 tracker.blocked = true; 128 tracker.blockedUntil = now + 60000; 129 return false; 130 } 131 tracker.requests.push(now); 132 return true; 133 }; 134 135 setInterval(cleanupRequestTracker, config.timeouts.medium || 10000); 136 137 export const verifySession = async (req, res, next) => { 138 const startTime = Date.now(); 139 const requestId = crypto.randomBytes(8).toString("hex"); 140 req.requestId = requestId; 141 142 try { 143 const clientIp = req.ip || req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || "unknown"; 144 if (!checkRequestRate(clientIp)) { 145 logger.warn("Rate limit exceeded for session verification", { ip: clientIp, requestId }); 146 return sendError( 147 res, 148 ErrorCodes.TOO_MANY_REQUESTS, 149 "Too many requests", 150 StatusCodes.TOO_MANY_REQUESTS, 151 ); 152 } 153 154 const authHeader = req.headers["authorization"]; 155 if (!authHeader || typeof authHeader !== "string") { 156 return sendError( 157 res, 158 ErrorCodes.UNAUTHORIZED, 159 "Authentication required", 160 StatusCodes.UNAUTHORIZED, 161 ); 162 } 163 164 const parts = authHeader.split(" "); 165 if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { 166 return sendError( 167 res, 168 ErrorCodes.UNAUTHORIZED, 169 "Invalid authorization format", 170 StatusCodes.UNAUTHORIZED, 171 ); 172 } 173 174 const token = parts[1]; 175 if (!validateTokenFormat(token)) { 176 await new Promise(resolve => setTimeout(resolve, crypto.randomInt(50, 150))); 177 return sendError( 178 res, 179 ErrorCodes.UNAUTHORIZED, 180 "Invalid token format", 181 StatusCodes.UNAUTHORIZED, 182 ); 183 } 184 185 const tokenHash = hashToken(token); 186 const cacheKey = `token:${tokenHash}`; 187 const cached = cache.get("auth", cacheKey); 188 189 if (cached) { 190 if (cached.valid && !cached.user.isBanned) { 191 if (cached.forceValidated || (crypto.randomBytes(4).readUInt32LE(0) / 0xFFFFFFFF) > 0.01) { 192 req.user = cached.user; 193 return next(); 194 } else { 195 try { 196 req.user = await forceRevalidation(token, req); 197 return next(); 198 } catch { 199 return sendError( 200 res, 201 ErrorCodes.SESSION_EXPIRED, 202 "Session expired", 203 StatusCodes.FORBIDDEN, 204 ); 205 } 206 } 207 } else if (cached.user.isBanned) { 208 invalidateUserCache(cached.user.id); 209 return sendError( 210 res, 211 ErrorCodes.FORBIDDEN, 212 "Account is suspended", 213 StatusCodes.FORBIDDEN, 214 ); 215 } 216 return sendError( 217 res, 218 ErrorCodes.SESSION_EXPIRED, 219 "Session expired", 220 StatusCodes.FORBIDDEN, 221 ); 222 } 223 224 const validation = await authService.validateSession(token); 225 const processingTime = Date.now() - startTime; 226 const minProcessingTime = 100; 227 if (processingTime < minProcessingTime) { 228 await new Promise(resolve => setTimeout(resolve, minProcessingTime - processingTime + crypto.randomInt(0, 50))); 229 } 230 231 if (validation.valid && validation.user && !validation.user.isBanned) { 232 cache.set("auth", cacheKey, { 233 valid: true, 234 user: validation.user, 235 userId: validation.user.id, 236 timestamp: Date.now(), 237 }); 238 req.user = validation.user; 239 return next(); 240 } else if (validation.user && validation.user.isBanned) { 241 invalidateUserCache(validation.user.id); 242 return sendError( 243 res, 244 ErrorCodes.FORBIDDEN, 245 "Account is suspended", 246 StatusCodes.FORBIDDEN, 247 ); 248 } 249 250 cache.set("auth", cacheKey, { 251 valid: false, 252 timestamp: Date.now(), 253 }); 254 255 return sendError( 256 res, 257 ErrorCodes.SESSION_EXPIRED, 258 "Invalid or expired session", 259 StatusCodes.FORBIDDEN, 260 ); 261 } catch (error) { 262 logger.error("Session verification error", { 263 requestId, 264 message: error.message, 265 ip: req.ip, 266 }); 267 await new Promise(resolve => setTimeout(resolve, crypto.randomInt(100, 200))); 268 return sendError( 269 res, 270 ErrorCodes.INTERNAL_ERROR, 271 "Authentication failed", 272 StatusCodes.INTERNAL_SERVER_ERROR, 273 ); 274 } 275 }; 276 277 export const requirePassword = async (req, res, next) => { 278 try { 279 if (!req.user || !req.user.id) { 280 return sendError( 281 res, 282 ErrorCodes.UNAUTHORIZED, 283 "Authentication required", 284 StatusCodes.UNAUTHORIZED, 285 ); 286 } 287 if (!req.user.hasPassword) { 288 return sendError( 289 res, 290 ErrorCodes.PASSWORD_REQUIRED, 291 "Password setup required", 292 StatusCodes.FORBIDDEN, 293 ); 294 } 295 return next(); 296 } catch (error) { 297 logger.error("Password requirement check error", { message: error.message }); 298 return sendError( 299 res, 300 ErrorCodes.INTERNAL_ERROR, 301 "Authorization check failed", 302 StatusCodes.INTERNAL_SERVER_ERROR, 303 ); 304 } 305 }; 306 307 export const requireUnbanned = async (req, res, next) => { 308 try { 309 if (!req.user || !req.user.id) { 310 return sendError( 311 res, 312 ErrorCodes.UNAUTHORIZED, 313 "Authentication required", 314 StatusCodes.UNAUTHORIZED, 315 ); 316 } 317 if (req.user.isBanned) { 318 return sendError( 319 res, 320 ErrorCodes.FORBIDDEN, 321 "Account suspended", 322 StatusCodes.FORBIDDEN, 323 ); 324 } 325 return next(); 326 } catch (error) { 327 logger.error("Ban check error", { message: error.message }); 328 return sendError( 329 res, 330 ErrorCodes.INTERNAL_ERROR, 331 "Authorization check failed", 332 StatusCodes.INTERNAL_SERVER_ERROR, 333 ); 334 } 335 }; 336 337 export const invalidateSessionCache = (token) => { 338 if (!token) { 339 return; 340 } 341 const tokenHash = hashToken(token); 342 cache.delete("auth", `token:${tokenHash}`); 343 }; 344 345 export { timingSafeCompare };