Master comprehensive API security implementation. Learn authentication patterns, authorization, rate limiting, input validation, and security vulnerability prevention.
Master comprehensive API security implementation. Learn authentication patterns, authorization, rate limiting, input validation, and security vulnerability prevention.
Implement robust authentication and authorization patterns
Design effective rate limiting and throttling mechanisms
Master input validation and sanitization techniques
Prevent common security vulnerabilities (SQL injection, XSS, etc.)
Configure secure CORS and API key management
Apply security monitoring and incident response strategies
Implement comprehensive authentication and authorization systems with multiple strategies and security layers.
Learn about Multi-Strategy Authentication System.
1# Install security dependencies
2npm install passport passport-jwt passport-local passport-oauth2
3npm install bcryptjs jsonwebtoken
4npm install helmet express-rate-limit express-validator
5npm install express-brute express-brute-redis
6npm install --save-dev @types/passport @types/bcryptjs @types/jsonwebtoken1const passport = require('passport');
2const LocalStrategy = require('passport-local').Strategy;
3const JwtStrategy = require('passport-jwt').Strategy;
4const ExtractJwt = require('passport-jwt').ExtractJwt;
5const OAuth2Strategy = require('passport-oauth2');
6const bcrypt = require('bcryptjs');
7const jwt = require('jsonwebtoken');
8const crypto = require('crypto');
9
10class AuthenticationManager {
11 constructor() {
12 this.strategies = new Map();
13 this.tokenBlacklist = new Set(); // In production, use Redis
14 this.refreshTokens = new Map(); // In production, use Redis
15
16 this.config = {
17 jwt: {
18 secret: process.env.JWT_SECRET || crypto.randomBytes(64).toString('hex'),
19 refreshSecret: process.env.JWT_REFRESH_SECRET || crypto.randomBytes(64).toString('hex'),
20 accessTokenExpiry: '15m',
21 refreshTokenExpiry: '7d',
22 issuer: process.env.JWT_ISSUER || 'api.example.com',
23 audience: process.env.JWT_AUDIENCE || 'api-clients'
24 },
25 password: {
26 saltRounds: 12,
27 minLength: 8,
28 requireUppercase: true,
29 requireLowercase: true,
30 requireNumbers: true,
31 requireSpecialChars: true,
32 maxAge: 90 * 24 * 60 * 60 * 1000 // 90 days
33 },
34 session: {
35 maxConcurrentSessions: 5,
36 sessionTimeout: 30 * 60 * 1000, // 30 minutes
37 requireReauth: true
38 }
39 };
40
41 this.setupStrategies();
42 }
43
44 setupStrategies() {
45 // Local username/password strategy
46 this.setupLocalStrategy();
47
48 // JWT token strategy
49 this.setupJWTStrategy();
50
51 // OAuth2 strategy
52 this.setupOAuth2Strategy();
53
54 // API key strategy
55 this.setupApiKeyStrategy();
56 }
57
58 setupLocalStrategy() {
59 const localStrategy = new LocalStrategy(
60 {
61 usernameField: 'email',
62 passwordField: 'password',
63 passReqToCallback: true
64 },
65 async (req, email, password, done) => {
66 try {
67 // Rate limiting check
68 const clientIp = req.ip || req.connection.remoteAddress;
69 if (await this.isRateLimited(clientIp, 'login')) {
70 return done(null, false, {
71 message: 'Too many login attempts. Please try again later.',
72 code: 'RATE_LIMITED'
73 });
74 }
75
76 // Find user with security checks
77 const user = await this.findUserForAuthentication(email);
78
79 if (!user) {
80 await this.recordFailedAttempt(clientIp, email, 'USER_NOT_FOUND');
81 return done(null, false, {
82 message: 'Invalid email or password',
83 code: 'INVALID_CREDENTIALS'
84 });
85 }
86
87 // Check account status
88 if (!user.isActive) {
89 await this.recordFailedAttempt(clientIp, email, 'ACCOUNT_DISABLED');
90 return done(null, false, {
91 message: 'Account is disabled',
92 code: 'ACCOUNT_DISABLED'
93 });
94 }
95
96 if (user.isLocked && user.lockUntil > Date.now()) {
97 await this.recordFailedAttempt(clientIp, email, 'ACCOUNT_LOCKED');
98 return done(null, false, {
99 message: 'Account is temporarily locked',
100 code: 'ACCOUNT_LOCKED',
101 lockUntil: user.lockUntil
102 });
103 }
104
105 // Verify password
106 const isValidPassword = await this.verifyPassword(password, user.passwordHash);
107
108 if (!isValidPassword) {
109 await this.recordFailedAttempt(clientIp, email, 'INVALID_PASSWORD');
110 await this.incrementLoginAttempts(user.id);
111
112 return done(null, false, {
113 message: 'Invalid email or password',
114 code: 'INVALID_CREDENTIALS'
115 });
116 }
117
118 // Check password age
119 if (this.isPasswordExpired(user.passwordChangedAt)) {
120 return done(null, false, {
121 message: 'Password has expired. Please reset your password.',
122 code: 'PASSWORD_EXPIRED'
123 });
124 }
125
126 // Check for suspicious activity
127 if (await this.detectSuspiciousActivity(user.id, req)) {
128 await this.triggerSecurityAlert(user.id, 'SUSPICIOUS_LOGIN', req);
129 return done(null, false, {
130 message: 'Suspicious activity detected. Additional verification required.',
131 code: 'SECURITY_CHECK'
132 });
133 }
134
135 // Successful authentication
136 await this.recordSuccessfulLogin(user.id, req);
137 await this.resetLoginAttempts(user.id);
138
139 // Return sanitized user object
140 const sanitizedUser = this.sanitizeUser(user);
141 return done(null, sanitizedUser);
142
143 } catch (error) {
144 console.error('Local authentication error:', error);
145 return done(error);
146 }
147 }
148 );
149
150 passport.use('local', localStrategy);
151 this.strategies.set('local', localStrategy);
152 }
153
154 setupJWTStrategy() {
155 const jwtOptions = {
156 jwtFromRequest: ExtractJwt.fromExtractors([
157 ExtractJwt.fromAuthHeaderAsBearerToken(),
158 ExtractJwt.fromHeader('x-access-token'),
159 ExtractJwt.fromUrlQueryParameter('token'),
160 (req) => {
161 // Extract from cookies for web clients
162 return req.cookies?.accessToken;
163 }
164 ]),
165 secretOrKey: this.config.jwt.secret,
166 issuer: this.config.jwt.issuer,
167 audience: this.config.jwt.audience,
168 algorithms: ['HS256'],
169 passReqToCallback: true
170 };
171
172 const jwtStrategy = new JwtStrategy(jwtOptions, async (req, payload, done) => {
173 try {
174 // Check if token is blacklisted
175 const tokenId = payload.jti;
176 if (this.tokenBlacklist.has(tokenId)) {
177 return done(null, false, {
178 message: 'Token has been revoked',
179 code: 'TOKEN_REVOKED'
180 });
181 }
182
183 // Check token expiration with grace period
184 const now = Math.floor(Date.now() / 1000);
185 if (payload.exp && payload.exp < now) {
186 return done(null, false, {
187 message: 'Token has expired',
188 code: 'TOKEN_EXPIRED'
189 });
190 }
191
192 // Verify user still exists and is active
193 const user = await this.findUserById(payload.sub);
194 if (!user || !user.isActive) {
195 return done(null, false, {
196 message: 'User not found or inactive',
197 code: 'USER_INVALID'
198 });
199 }
200
201 // Check if user's security context has changed
202 const userSecurityHash = this.calculateUserSecurityHash(user);
203 if (payload.securityHash && payload.securityHash !== userSecurityHash) {
204 return done(null, false, {
205 message: 'Security context has changed. Please re-authenticate.',
206 code: 'SECURITY_CONTEXT_CHANGED'
207 });
208 }
209
210 // Update last seen
211 await this.updateLastSeen(user.id, req);
212
213 // Add request context to user
214 const userWithContext = {
215 ...this.sanitizeUser(user),
216 tokenId,
217 permissions: payload.permissions || [],
218 scope: payload.scope || []
219 };
220
221 return done(null, userWithContext);
222
223 } catch (error) {
224 console.error('JWT authentication error:', error);
225 return done(error);
226 }
227 });
228
229 passport.use('jwt', jwtStrategy);
230 this.strategies.set('jwt', jwtStrategy);
231 }
232
233 setupOAuth2Strategy() {
234 const oauth2Strategy = new OAuth2Strategy({
235 authorizationURL: process.env.OAUTH2_AUTH_URL,
236 tokenURL: process.env.OAUTH2_TOKEN_URL,
237 clientID: process.env.OAUTH2_CLIENT_ID,
238 clientSecret: process.env.OAUTH2_CLIENT_SECRET,
239 callbackURL: process.env.OAUTH2_CALLBACK_URL,
240 scope: ['profile', 'email'],
241 passReqToCallback: true
242 }, async (req, accessToken, refreshToken, profile, done) => {
243 try {
244 // Verify OAuth2 token with provider
245 const userInfo = await this.verifyOAuth2Token(accessToken);
246
247 if (!userInfo || !userInfo.email) {
248 return done(null, false, {
249 message: 'Invalid OAuth2 token or missing email',
250 code: 'OAUTH2_INVALID'
251 });
252 }
253
254 // Find or create user
255 let user = await this.findUserByEmail(userInfo.email);
256
257 if (!user) {
258 // Create new user from OAuth2 profile
259 user = await this.createUserFromOAuth2(userInfo, profile);
260 } else {
261 // Update OAuth2 profile information
262 await this.updateOAuth2Profile(user.id, userInfo, profile);
263 }
264
265 // Store OAuth2 tokens securely
266 await this.storeOAuth2Tokens(user.id, {
267 accessToken: await this.encryptToken(accessToken),
268 refreshToken: refreshToken ? await this.encryptToken(refreshToken) : null,
269 provider: profile.provider,
270 providerId: profile.id
271 });
272
273 return done(null, this.sanitizeUser(user));
274
275 } catch (error) {
276 console.error('OAuth2 authentication error:', error);
277 return done(error);
278 }
279 });
280
281 passport.use('oauth2', oauth2Strategy);
282 this.strategies.set('oauth2', oauth2Strategy);
283 }
284
285 setupApiKeyStrategy() {
286 const apiKeyStrategy = new passport.Strategy('api-key', async (req, done) => {
287 try {
288 const apiKey = req.headers['x-api-key'] || req.query.apiKey;
289
290 if (!apiKey) {
291 return done(null, false, {
292 message: 'API key is required',
293 code: 'API_KEY_MISSING'
294 });
295 }
296
297 // Validate API key format
298 if (!this.isValidApiKeyFormat(apiKey)) {
299 return done(null, false, {
300 message: 'Invalid API key format',
301 code: 'API_KEY_INVALID_FORMAT'
302 });
303 }
304
305 // Find API key in database
306 const apiKeyRecord = await this.findApiKey(apiKey);
307
308 if (!apiKeyRecord) {
309 await this.recordApiKeyAttempt(apiKey, req.ip, 'INVALID_KEY');
310 return done(null, false, {
311 message: 'Invalid API key',
312 code: 'API_KEY_INVALID'
313 });
314 }
315
316 // Check API key status
317 if (!apiKeyRecord.isActive) {
318 await this.recordApiKeyAttempt(apiKey, req.ip, 'KEY_DISABLED');
319 return done(null, false, {
320 message: 'API key is disabled',
321 code: 'API_KEY_DISABLED'
322 });
323 }
324
325 // Check expiration
326 if (apiKeyRecord.expiresAt && apiKeyRecord.expiresAt < new Date()) {
327 await this.recordApiKeyAttempt(apiKey, req.ip, 'KEY_EXPIRED');
328 return done(null, false, {
329 message: 'API key has expired',
330 code: 'API_KEY_EXPIRED'
331 });
332 }
333
334 // Check rate limits
335 if (await this.isApiKeyRateLimited(apiKeyRecord.id)) {
336 return done(null, false, {
337 message: 'API key rate limit exceeded',
338 code: 'API_KEY_RATE_LIMITED'
339 });
340 }
341
342 // Check allowed IPs
343 if (apiKeyRecord.allowedIPs && !this.isIpAllowed(req.ip, apiKeyRecord.allowedIPs)) {
344 await this.recordApiKeyAttempt(apiKey, req.ip, 'IP_NOT_ALLOWED');
345 return done(null, false, {
346 message: 'IP address not allowed for this API key',
347 code: 'API_KEY_IP_RESTRICTED'
348 });
349 }
350
351 // Update usage statistics
352 await this.updateApiKeyUsage(apiKeyRecord.id, req);
353
354 // Create API user context
355 const apiUser = {
356 id: apiKeyRecord.userId,
357 type: 'api',
358 apiKeyId: apiKeyRecord.id,
359 permissions: apiKeyRecord.permissions || [],
360 scopes: apiKeyRecord.scopes || [],
361 rateLimit: apiKeyRecord.rateLimit
362 };
363
364 return done(null, apiUser);
365
366 } catch (error) {
367 console.error('API key authentication error:', error);
368 return done(error);
369 }
370 });
371
372 passport.use('api-key', apiKeyStrategy);
373 this.strategies.set('api-key', apiKeyStrategy);
374 }
375
376 // Token management methods
377 async generateTokenPair(user, options = {}) {
378 const tokenId = crypto.randomUUID();
379 const securityHash = this.calculateUserSecurityHash(user);
380
381 const accessTokenPayload = {
382 sub: user.id, // Subject
383 jti: tokenId, // JWT ID
384 iat: Math.floor(Date.now() / 1000), // Issued at
385 exp: Math.floor(Date.now() / 1000) + this.parseTimeToSeconds(this.config.jwt.accessTokenExpiry),
386 iss: this.config.jwt.issuer, // Issuer
387 aud: this.config.jwt.audience, // Audience
388 type: 'access',
389 securityHash,
390 permissions: user.permissions || [],
391 scope: options.scope || 'read write',
392 sessionId: options.sessionId || crypto.randomUUID()
393 };
394
395 const refreshTokenPayload = {
396 sub: user.id,
397 jti: crypto.randomUUID(),
398 iat: Math.floor(Date.now() / 1000),
399 exp: Math.floor(Date.now() / 1000) + this.parseTimeToSeconds(this.config.jwt.refreshTokenExpiry),
400 iss: this.config.jwt.issuer,
401 aud: this.config.jwt.audience,
402 type: 'refresh',
403 accessTokenId: tokenId
404 };
405
406 const accessToken = jwt.sign(accessTokenPayload, this.config.jwt.secret);
407 const refreshToken = jwt.sign(refreshTokenPayload, this.config.jwt.refreshSecret);
408
409 // Store refresh token
410 this.refreshTokens.set(refreshTokenPayload.jti, {
411 userId: user.id,
412 accessTokenId: tokenId,
413 createdAt: new Date(),
414 expiresAt: new Date(refreshTokenPayload.exp * 1000)
415 });
416
417 return {
418 accessToken,
419 refreshToken,
420 tokenType: 'Bearer',
421 expiresIn: this.parseTimeToSeconds(this.config.jwt.accessTokenExpiry),
422 scope: accessTokenPayload.scope
423 };
424 }
425
426 async refreshAccessToken(refreshToken) {
427 try {
428 const decoded = jwt.verify(refreshToken, this.config.jwt.refreshSecret);
429
430 if (decoded.type !== 'refresh') {
431 throw new Error('Invalid token type');
432 }
433
434 // Check if refresh token exists and is valid
435 const storedToken = this.refreshTokens.get(decoded.jti);
436 if (!storedToken) {
437 throw new Error('Refresh token not found');
438 }
439
440 // Verify user still exists and is active
441 const user = await this.findUserById(decoded.sub);
442 if (!user || !user.isActive) {
443 throw new Error('User not found or inactive');
444 }
445
446 // Revoke old access token
447 this.tokenBlacklist.add(storedToken.accessTokenId);
448
449 // Generate new token pair
450 const newTokens = await this.generateTokenPair(user);
451
452 // Update refresh token store
453 this.refreshTokens.delete(decoded.jti);
454
455 return newTokens;
456
457 } catch (error) {
458 throw new Error(\`Token refresh failed: \${error.message}\`);
459 }
460 }
461
462 async revokeToken(tokenId, userId) {
463 try {
464 // Add to blacklist
465 this.tokenBlacklist.add(tokenId);
466
467 // Remove associated refresh tokens
468 for (const [jti, tokenData] of this.refreshTokens.entries()) {
469 if (tokenData.userId === userId && tokenData.accessTokenId === tokenId) {
470 this.refreshTokens.delete(jti);
471 }
472 }
473
474 // Log revocation
475 await this.logSecurityEvent(userId, 'TOKEN_REVOKED', { tokenId });
476
477 return true;
478 } catch (error) {
479 console.error('Token revocation error:', error);
480 return false;
481 }
482 }
483
484 async revokeAllUserTokens(userId) {
485 try {
486 let revokedCount = 0;
487
488 // Revoke all refresh tokens for user
489 for (const [jti, tokenData] of this.refreshTokens.entries()) {
490 if (tokenData.userId === userId) {
491 this.tokenBlacklist.add(tokenData.accessTokenId);
492 this.refreshTokens.delete(jti);
493 revokedCount++;
494 }
495 }
496
497 // Log mass revocation
498 await this.logSecurityEvent(userId, 'ALL_TOKENS_REVOKED', { count: revokedCount });
499
500 return revokedCount;
501 } catch (error) {
502 console.error('Mass token revocation error:', error);
503 return 0;
504 }
505 }
506
507 // Security utility methods
508 async hashPassword(password) {
509 // Validate password strength
510 if (!this.isPasswordStrong(password)) {
511 throw new Error('Password does not meet security requirements');
512 }
513
514 return await bcrypt.hash(password, this.config.password.saltRounds);
515 }
516
517 async verifyPassword(password, hash) {
518 return await bcrypt.compare(password, hash);
519 }
520
521 isPasswordStrong(password) {
522 const rules = this.config.password;
523
524 if (password.length < rules.minLength) return false;
525 if (rules.requireUppercase && !/[A-Z]/.test(password)) return false;
526 if (rules.requireLowercase && !/[a-z]/.test(password)) return false;
527 if (rules.requireNumbers && !/\\d/.test(password)) return false;
528 if (rules.requireSpecialChars && !/[!@#$%^&*()_+\\-=\\[\\]{};':"\\\\|,.<>\\/?]/.test(password)) return false;
529
530 return true;
531 }
532
533 isPasswordExpired(passwordChangedAt) {
534 if (!passwordChangedAt) return true;
535 const ageMs = Date.now() - new Date(passwordChangedAt).getTime();
536 return ageMs > this.config.password.maxAge;
537 }
538
539 calculateUserSecurityHash(user) {
540 const securityString = \`\${user.id}:\${user.passwordHash}:\${user.role}:\${user.isActive}\`;
541 return crypto.createHash('sha256').update(securityString).digest('hex');
542 }
543
544 sanitizeUser(user) {
545 const { passwordHash, passwordResetToken, emailVerificationToken, ...sanitized } = user;
546 return sanitized;
547 }
548
549 parseTimeToSeconds(timeString) {
550 const units = { s: 1, m: 60, h: 3600, d: 86400 };
551 const match = timeString.match(/^(\\d+)([smhd])$/);
552 if (!match) throw new Error('Invalid time format');
553 return parseInt(match[1]) * units[match[2]];
554 }
555
556 // Placeholder methods (implement based on your database/cache)
557 async findUserForAuthentication(email) { /* Implementation */ }
558 async findUserById(id) { /* Implementation */ }
559 async findUserByEmail(email) { /* Implementation */ }
560 async recordFailedAttempt(ip, email, reason) { /* Implementation */ }
561 async recordSuccessfulLogin(userId, req) { /* Implementation */ }
562 async incrementLoginAttempts(userId) { /* Implementation */ }
563 async resetLoginAttempts(userId) { /* Implementation */ }
564 async updateLastSeen(userId, req) { /* Implementation */ }
565 async detectSuspiciousActivity(userId, req) { /* Implementation */ }
566 async triggerSecurityAlert(userId, type, req) { /* Implementation */ }
567 async isRateLimited(ip, action) { /* Implementation */ }
568 async logSecurityEvent(userId, event, data) { /* Implementation */ }
569}
570
571module.exports = AuthenticationManager;Implement sophisticated rate limiting mechanisms to protect against abuse and ensure fair usage.
1const rateLimit = require('express-rate-limit');
2const RedisStore = require('rate-limit-redis');
3const Redis = require('ioredis');
4
5class AdvancedRateLimiter {
6 constructor() {
7 this.redis = new Redis({
8 host: process.env.REDIS_HOST || 'localhost',
9 port: process.env.REDIS_PORT || 6379,
10 db: process.env.REDIS_RATE_LIMIT_DB || 1
11 });
12
13 this.limiters = new Map();
14 this.setupDefaultLimiters();
15 }
16
17 setupDefaultLimiters() {
18 // Global API rate limiter
19 this.createLimiter('global', {
20 windowMs: 15 * 60 * 1000, // 15 minutes
21 max: 1000, // 1000 requests per window
22 standardHeaders: true,
23 legacyHeaders: false,
24 message: {
25 error: 'Too Many Requests',
26 message: 'Rate limit exceeded. Please try again later.',
27 retryAfter: '15 minutes'
28 },
29 skipSuccessfulRequests: false,
30 skipFailedRequests: false,
31 keyGenerator: (req) => {
32 // Use API key if available, otherwise IP
33 return req.user?.apiKeyId || req.ip;
34 }
35 });
36
37 // Authentication rate limiter
38 this.createLimiter('auth', {
39 windowMs: 15 * 60 * 1000, // 15 minutes
40 max: 5, // 5 attempts per window
41 standardHeaders: true,
42 legacyHeaders: false,
43 message: {
44 error: 'Too Many Authentication Attempts',
45 message: 'Too many login attempts. Please try again later.',
46 retryAfter: '15 minutes'
47 },
48 skipSuccessfulRequests: true, // Don't count successful requests
49 keyGenerator: (req) => {
50 // Rate limit by IP and email combination
51 const email = req.body?.email || '';
52 return \`auth:\${req.ip}:\${email}\`;
53 },
54 onLimitReached: async (req, res, options) => {
55 // Log security event
56 await this.logSecurityEvent(req, 'RATE_LIMIT_AUTH', {
57 ip: req.ip,
58 email: req.body?.email,
59 userAgent: req.get('User-Agent')
60 });
61 }
62 });
63
64 // API creation rate limiter (for expensive operations)
65 this.createLimiter('create', {
66 windowMs: 60 * 60 * 1000, // 1 hour
67 max: 100, // 100 creations per hour
68 message: {
69 error: 'Creation Rate Limit Exceeded',
70 message: 'Too many creation requests. Please wait before creating more resources.',
71 retryAfter: '1 hour'
72 },
73 keyGenerator: (req) => {
74 return req.user?.id || req.ip;
75 }
76 });
77
78 // Search rate limiter
79 this.createLimiter('search', {
80 windowMs: 60 * 1000, // 1 minute
81 max: 30, // 30 searches per minute
82 message: {
83 error: 'Search Rate Limit Exceeded',
84 message: 'Too many search requests. Please wait before searching again.',
85 retryAfter: '1 minute'
86 }
87 });
88
89 // Upload rate limiter
90 this.createLimiter('upload', {
91 windowMs: 60 * 60 * 1000, // 1 hour
92 max: 50, // 50 uploads per hour
93 message: {
94 error: 'Upload Rate Limit Exceeded',
95 message: 'Upload limit reached. Please wait before uploading more files.',
96 retryAfter: '1 hour'
97 }
98 });
99 }
100
101 createLimiter(name, options) {
102 const defaultOptions = {
103 store: new RedisStore({
104 prefix: \`rate_limit:\${name}:\`,
105 sendCommand: (...args) => this.redis.call(...args)
106 }),
107 standardHeaders: true,
108 legacyHeaders: false,
109 handler: this.createRateLimitHandler(name),
110 onLimitReached: this.createLimitReachedHandler(name)
111 };
112
113 const limiter = rateLimit({ ...defaultOptions, ...options });
114 this.limiters.set(name, limiter);
115 return limiter;
116 }
117
118 createRateLimitHandler(limiterName) {
119 return (req, res, next, options) => {
120 const retryAfter = Math.round(options.windowMs / 1000);
121
122 res.status(options.statusCode || 429).json({
123 error: 'Rate Limit Exceeded',
124 message: options.message?.message || \`Rate limit exceeded for \${limiterName}\`,
125 type: 'RATE_LIMIT_EXCEEDED',
126 limiter: limiterName,
127 retryAfter,
128 limit: options.max,
129 windowMs: options.windowMs,
130 headers: {
131 'X-RateLimit-Limit': options.max,
132 'X-RateLimit-Remaining': 0,
133 'X-RateLimit-Reset': new Date(Date.now() + options.windowMs).toISOString()
134 },
135 timestamp: new Date().toISOString()
136 });
137 };
138 }
139
140 createLimitReachedHandler(limiterName) {
141 return async (req, res, options) => {
142 // Log rate limit event
143 await this.logRateLimitEvent(req, limiterName, {
144 limit: options.max,
145 windowMs: options.windowMs,
146 ip: req.ip,
147 userAgent: req.get('User-Agent'),
148 userId: req.user?.id,
149 endpoint: req.path
150 });
151
152 // Check for potential abuse
153 await this.checkForAbuse(req, limiterName);
154 };
155 }
156
157 // Dynamic rate limiting based on user type and endpoint
158 createDynamicLimiter(options = {}) {
159 return async (req, res, next) => {
160 try {
161 // Determine rate limit based on user type
162 const limits = this.calculateDynamicLimits(req);
163
164 // Create key for this specific combination
165 const key = this.generateDynamicKey(req, limits);
166
167 // Check current usage
168 const current = await this.getCurrentUsage(key);
169
170 if (current >= limits.max) {
171 return this.sendRateLimitResponse(res, limits, current);
172 }
173
174 // Increment usage
175 await this.incrementUsage(key, limits.windowMs);
176
177 // Add rate limit headers
178 this.addRateLimitHeaders(res, limits, current + 1);
179
180 next();
181
182 } catch (error) {
183 console.error('Dynamic rate limiter error:', error);
184 next(); // Fail open for availability
185 }
186 };
187 }
188
189 calculateDynamicLimits(req) {
190 const base = {
191 windowMs: 15 * 60 * 1000, // 15 minutes
192 max: 1000
193 };
194
195 // Adjust based on user type
196 if (req.user) {
197 switch (req.user.role) {
198 case 'premium':
199 return { ...base, max: 5000 };
200 case 'admin':
201 return { ...base, max: 10000 };
202 case 'api':
203 return { ...base, max: req.user.rateLimit || 2000 };
204 default:
205 return { ...base, max: 2000 };
206 }
207 }
208
209 // Adjust based on endpoint type
210 if (req.path.includes('/search')) {
211 return { ...base, max: 100, windowMs: 60 * 1000 }; // 1 minute
212 }
213
214 if (req.method === 'POST' || req.method === 'PUT') {
215 return { ...base, max: 200 };
216 }
217
218 return base;
219 }
220
221 generateDynamicKey(req, limits) {
222 const identifier = req.user?.id || req.ip;
223 const endpoint = req.route?.path || req.path;
224 const method = req.method;
225
226 return \`dynamic:\${identifier}:\${method}:\${endpoint}:\${limits.windowMs}\`;
227 }
228
229 async getCurrentUsage(key) {
230 try {
231 const usage = await this.redis.get(key);
232 return parseInt(usage) || 0;
233 } catch (error) {
234 console.error('Error getting current usage:', error);
235 return 0;
236 }
237 }
238
239 async incrementUsage(key, windowMs) {
240 try {
241 const pipeline = this.redis.pipeline();
242 pipeline.incr(key);
243 pipeline.pexpire(key, windowMs);
244 await pipeline.exec();
245 } catch (error) {
246 console.error('Error incrementing usage:', error);
247 }
248 }
249
250 addRateLimitHeaders(res, limits, current) {
251 const remaining = Math.max(0, limits.max - current);
252 const resetTime = Date.now() + limits.windowMs;
253
254 res.set({
255 'X-RateLimit-Limit': limits.max,
256 'X-RateLimit-Remaining': remaining,
257 'X-RateLimit-Reset': Math.ceil(resetTime / 1000),
258 'X-RateLimit-Window': limits.windowMs
259 });
260 }
261
262 sendRateLimitResponse(res, limits, current) {
263 const retryAfter = Math.ceil(limits.windowMs / 1000);
264
265 res.status(429).json({
266 error: 'Rate Limit Exceeded',
267 message: 'Too many requests. Please try again later.',
268 retryAfter,
269 limit: limits.max,
270 current,
271 windowMs: limits.windowMs
272 });
273 }
274
275 // Sliding window rate limiter for more precise control
276 createSlidingWindowLimiter(options) {
277 return async (req, res, next) => {
278 try {
279 const {
280 max = 100,
281 windowMs = 60 * 1000,
282 keyGenerator = (req) => req.ip
283 } = options;
284
285 const key = keyGenerator(req);
286 const now = Date.now();
287 const windowStart = now - windowMs;
288
289 // Remove old entries and count current requests
290 const pipeline = this.redis.pipeline();
291 pipeline.zremrangebyscore(\`sliding:\${key}\`, 0, windowStart);
292 pipeline.zcard(\`sliding:\${key}\`);
293 pipeline.zadd(\`sliding:\${key}\`, now, \`\${now}-\${Math.random()}\`);
294 pipeline.expire(\`sliding:\${key}\`, Math.ceil(windowMs / 1000));
295
296 const results = await pipeline.exec();
297 const currentCount = results[1][1];
298
299 if (currentCount >= max) {
300 return res.status(429).json({
301 error: 'Rate Limit Exceeded',
302 message: 'Sliding window rate limit exceeded',
303 retryAfter: Math.ceil(windowMs / 1000)
304 });
305 }
306
307 res.set({
308 'X-RateLimit-Limit': max,
309 'X-RateLimit-Remaining': Math.max(0, max - currentCount - 1),
310 'X-RateLimit-Window': windowMs
311 });
312
313 next();
314
315 } catch (error) {
316 console.error('Sliding window rate limiter error:', error);
317 next(); // Fail open
318 }
319 };
320 }
321
322 // Token bucket rate limiter for burst handling
323 createTokenBucketLimiter(options) {
324 return async (req, res, next) => {
325 try {
326 const {
327 capacity = 100,
328 refillRate = 10, // tokens per second
329 keyGenerator = (req) => req.ip
330 } = options;
331
332 const key = \`bucket:\${keyGenerator(req)}\`;
333 const now = Date.now() / 1000;
334
335 // Get current bucket state
336 const bucketData = await this.redis.hmget(key, 'tokens', 'lastRefill');
337 let tokens = parseFloat(bucketData[0]) || capacity;
338 let lastRefill = parseFloat(bucketData[1]) || now;
339
340 // Calculate tokens to add based on time passed
341 const timePassed = now - lastRefill;
342 const tokensToAdd = timePassed * refillRate;
343 tokens = Math.min(capacity, tokens + tokensToAdd);
344
345 if (tokens < 1) {
346 const waitTime = (1 - tokens) / refillRate;
347 return res.status(429).json({
348 error: 'Rate Limit Exceeded',
349 message: 'Token bucket empty',
350 retryAfter: Math.ceil(waitTime)
351 });
352 }
353
354 // Consume one token
355 tokens -= 1;
356
357 // Update bucket state
358 await this.redis.hmset(key, 'tokens', tokens, 'lastRefill', now);
359 await this.redis.expire(key, Math.ceil(capacity / refillRate) + 60);
360
361 res.set({
362 'X-RateLimit-Tokens': Math.floor(tokens),
363 'X-RateLimit-Capacity': capacity,
364 'X-RateLimit-RefillRate': refillRate
365 });
366
367 next();
368
369 } catch (error) {
370 console.error('Token bucket rate limiter error:', error);
371 next(); // Fail open
372 }
373 };
374 }
375
376 // Distributed rate limiting across multiple servers
377 createDistributedLimiter(options) {
378 return async (req, res, next) => {
379 try {
380 const {
381 max = 1000,
382 windowMs = 15 * 60 * 1000,
383 keyGenerator = (req) => req.ip,
384 nodes = ['server1', 'server2', 'server3']
385 } = options;
386
387 const key = keyGenerator(req);
388 const nodeId = process.env.NODE_ID || 'default';
389 const quotaPerNode = Math.ceil(max / nodes.length);
390
391 // Check local quota first (fast path)
392 const localKey = \`local:\${nodeId}:\${key}\`;
393 const localUsage = await this.getCurrentUsage(localKey);
394
395 if (localUsage >= quotaPerNode) {
396 // Check global usage (slower path)
397 const globalKey = \`global:\${key}\`;
398 const globalUsage = await this.getGlobalUsage(globalKey, nodes);
399
400 if (globalUsage >= max) {
401 return res.status(429).json({
402 error: 'Distributed Rate Limit Exceeded',
403 message: 'Global rate limit exceeded across all nodes'
404 });
405 }
406 }
407
408 // Increment local counter
409 await this.incrementUsage(localKey, windowMs);
410
411 // Periodically sync with global counter
412 if (localUsage % 10 === 0) {
413 await this.syncGlobalUsage(\`global:\${key}\`, nodeId, localUsage, windowMs);
414 }
415
416 next();
417
418 } catch (error) {
419 console.error('Distributed rate limiter error:', error);
420 next(); // Fail open
421 }
422 };
423 }
424
425 async getGlobalUsage(globalKey, nodes) {
426 try {
427 const pipeline = this.redis.pipeline();
428
429 nodes.forEach(nodeId => {
430 pipeline.get(\`local:\${nodeId}:\${globalKey.replace('global:', '')}\`);
431 });
432
433 const results = await pipeline.exec();
434 return results.reduce((total, [err, value]) => {
435 return total + (parseInt(value) || 0);
436 }, 0);
437
438 } catch (error) {
439 console.error('Error getting global usage:', error);
440 return 0;
441 }
442 }
443
444 async syncGlobalUsage(globalKey, nodeId, usage, windowMs) {
445 try {
446 const nodeKey = \`\${globalKey}:\${nodeId}\`;
447 await this.redis.setex(nodeKey, Math.ceil(windowMs / 1000), usage);
448 } catch (error) {
449 console.error('Error syncing global usage:', error);
450 }
451 }
452
453 // Security monitoring
454 async logRateLimitEvent(req, limiterName, data) {
455 try {
456 const event = {
457 timestamp: new Date(),
458 type: 'RATE_LIMIT_EXCEEDED',
459 limiter: limiterName,
460 ip: req.ip,
461 userAgent: req.get('User-Agent'),
462 userId: req.user?.id,
463 endpoint: req.path,
464 method: req.method,
465 ...data
466 };
467
468 // Store in Redis for real-time monitoring
469 await this.redis.lpush('rate_limit_events', JSON.stringify(event));
470 await this.redis.ltrim('rate_limit_events', 0, 999); // Keep last 1000 events
471
472 console.warn('Rate limit exceeded:', event);
473
474 } catch (error) {
475 console.error('Error logging rate limit event:', error);
476 }
477 }
478
479 async checkForAbuse(req, limiterName) {
480 try {
481 const key = \`abuse:\${req.ip}\`;
482 const abuses = await this.redis.incr(key);
483 await this.redis.expire(key, 3600); // 1 hour window
484
485 // If too many rate limit violations, consider blocking
486 if (abuses > 10) {
487 await this.triggerAbuseResponse(req, abuses);
488 }
489
490 } catch (error) {
491 console.error('Error checking for abuse:', error);
492 }
493 }
494
495 async triggerAbuseResponse(req, abuseCount) {
496 try {
497 // Log security alert
498 const alert = {
499 type: 'POTENTIAL_ABUSE',
500 ip: req.ip,
501 userAgent: req.get('User-Agent'),
502 abuseCount,
503 timestamp: new Date()
504 };
505
506 await this.redis.lpush('security_alerts', JSON.stringify(alert));
507
508 // Could trigger additional security measures:
509 // - Temporary IP blocking
510 // - CAPTCHA requirements
511 // - Account suspension
512 // - Admin notifications
513
514 console.error('Potential abuse detected:', alert);
515
516 } catch (error) {
517 console.error('Error triggering abuse response:', error);
518 }
519 }
520
521 getLimiter(name) {
522 return this.limiters.get(name);
523 }
524
525 // Cleanup method
526 async cleanup() {
527 if (this.redis) {
528 await this.redis.quit();
529 }
530 }
531}
532
533module.exports = AdvancedRateLimiter;Implement comprehensive input validation and sanitization to prevent injection attacks and data corruption.
1const { body, param, query, validationResult } = require('express-validator');
2const DOMPurify = require('isomorphic-dompurify');
3const xss = require('xss');
4const validator = require('validator');
5
6class InputValidator {
7 constructor() {
8 this.sanitizers = new Map();
9 this.validators = new Map();
10 this.setupDefaultValidators();
11 this.setupDefaultSanitizers();
12 }
13
14 setupDefaultValidators() {
15 // User validation rules
16 this.validators.set('user', {
17 create: [
18 body('email')
19 .isEmail()
20 .normalizeEmail()
21 .withMessage('Valid email address is required')
22 .custom(async (email) => {
23 // Check for disposable email domains
24 if (this.isDisposableEmail(email)) {
25 throw new Error('Disposable email addresses are not allowed');
26 }
27 return true;
28 }),
29
30 body('password')
31 .isLength({ min: 8, max: 128 })
32 .withMessage('Password must be 8-128 characters long')
33 .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]/)
34 .withMessage('Password must contain uppercase, lowercase, number and special character')
35 .custom((password) => {
36 // Check against common passwords
37 if (this.isCommonPassword(password)) {
38 throw new Error('Password is too common');
39 }
40 return true;
41 }),
42
43 body('firstName')
44 .trim()
45 .isLength({ min: 1, max: 50 })
46 .withMessage('First name must be 1-50 characters')
47 .matches(/^[a-zA-Z\\s'-]+$/)
48 .withMessage('First name contains invalid characters')
49 .customSanitizer((value) => this.sanitizeName(value)),
50
51 body('lastName')
52 .trim()
53 .isLength({ min: 1, max: 50 })
54 .withMessage('Last name must be 1-50 characters')
55 .matches(/^[a-zA-Z\\s'-]+$/)
56 .withMessage('Last name contains invalid characters')
57 .customSanitizer((value) => this.sanitizeName(value)),
58
59 body('phoneNumber')
60 .optional()
61 .isMobilePhone()
62 .withMessage('Invalid phone number format')
63 .customSanitizer((value) => this.sanitizePhoneNumber(value)),
64
65 body('dateOfBirth')
66 .optional()
67 .isISO8601()
68 .toDate()
69 .withMessage('Invalid date format')
70 .custom((date) => {
71 const age = this.calculateAge(date);
72 if (age < 13 || age > 120) {
73 throw new Error('Age must be between 13 and 120 years');
74 }
75 return true;
76 }),
77
78 body('role')
79 .optional()
80 .isIn(['user', 'admin', 'manager', 'premium'])
81 .withMessage('Invalid role specified')
82 ],
83
84 update: [
85 body('firstName')
86 .optional()
87 .trim()
88 .isLength({ min: 1, max: 50 })
89 .withMessage('First name must be 1-50 characters')
90 .matches(/^[a-zA-Z\\s'-]+$/)
91 .withMessage('First name contains invalid characters'),
92
93 body('lastName')
94 .optional()
95 .trim()
96 .isLength({ min: 1, max: 50 })
97 .withMessage('Last name must be 1-50 characters')
98 .matches(/^[a-zA-Z\\s'-]+$/)
99 .withMessage('Last name contains invalid characters'),
100
101 body('avatar')
102 .optional()
103 .isURL({ protocols: ['http', 'https'], require_protocol: true })
104 .withMessage('Avatar must be a valid URL')
105 .custom((url) => {
106 // Validate image URL
107 if (!this.isValidImageUrl(url)) {
108 throw new Error('Avatar URL must point to a valid image');
109 }
110 return true;
111 })
112 ]
113 });
114
115 // Product validation rules
116 this.validators.set('product', {
117 create: [
118 body('name')
119 .trim()
120 .isLength({ min: 1, max: 200 })
121 .withMessage('Product name must be 1-200 characters')
122 .customSanitizer((value) => this.sanitizeHtml(value)),
123
124 body('description')
125 .optional()
126 .trim()
127 .isLength({ max: 2000 })
128 .withMessage('Description cannot exceed 2000 characters')
129 .customSanitizer((value) => this.sanitizeHtml(value, { allowedTags: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'] })),
130
131 body('shortDescription')
132 .optional()
133 .trim()
134 .isLength({ max: 500 })
135 .withMessage('Short description cannot exceed 500 characters')
136 .customSanitizer((value) => this.sanitizeHtml(value)),
137
138 body('price')
139 .isFloat({ min: 0 })
140 .withMessage('Price must be a positive number')
141 .customSanitizer((value) => parseFloat(parseFloat(value).toFixed(2))),
142
143 body('compareAtPrice')
144 .optional()
145 .isFloat({ min: 0 })
146 .withMessage('Compare at price must be a positive number')
147 .custom((compareAtPrice, { req }) => {
148 if (compareAtPrice && compareAtPrice <= req.body.price) {
149 throw new Error('Compare at price must be greater than price');
150 }
151 return true;
152 }),
153
154 body('sku')
155 .trim()
156 .isAlphanumeric('en-US', { ignore: '-_' })
157 .isLength({ min: 1, max: 50 })
158 .withMessage('SKU must be 1-50 alphanumeric characters (dashes and underscores allowed)')
159 .toUpperCase(),
160
161 body('categoryId')
162 .isUUID()
163 .withMessage('Category ID must be a valid UUID'),
164
165 body('tags')
166 .optional()
167 .isArray({ max: 10 })
168 .withMessage('Maximum 10 tags allowed')
169 .custom((tags) => {
170 if (tags && !tags.every(tag => typeof tag === 'string' && tag.length <= 30)) {
171 throw new Error('Each tag must be a string of max 30 characters');
172 }
173 return true;
174 })
175 ]
176 });
177
178 // Search validation rules
179 this.validators.set('search', {
180 products: [
181 query('q')
182 .optional()
183 .trim()
184 .isLength({ min: 1, max: 100 })
185 .withMessage('Search query must be 1-100 characters')
186 .customSanitizer((value) => this.sanitizeSearchQuery(value)),
187
188 query('category')
189 .optional()
190 .isUUID()
191 .withMessage('Category must be a valid UUID'),
192
193 query('priceMin')
194 .optional()
195 .isFloat({ min: 0 })
196 .withMessage('Minimum price must be a positive number')
197 .toFloat(),
198
199 query('priceMax')
200 .optional()
201 .isFloat({ min: 0 })
202 .withMessage('Maximum price must be a positive number')
203 .toFloat()
204 .custom((priceMax, { req }) => {
205 const priceMin = parseFloat(req.query.priceMin) || 0;
206 if (priceMax <= priceMin) {
207 throw new Error('Maximum price must be greater than minimum price');
208 }
209 return true;
210 }),
211
212 query('page')
213 .optional()
214 .isInt({ min: 1, max: 1000 })
215 .withMessage('Page must be between 1 and 1000')
216 .toInt(),
217
218 query('limit')
219 .optional()
220 .isInt({ min: 1, max: 100 })
221 .withMessage('Limit must be between 1 and 100')
222 .toInt(),
223
224 query('sort')
225 .optional()
226 .isIn(['name', 'price', 'createdAt', 'rating', 'popularity'])
227 .withMessage('Invalid sort field'),
228
229 query('order')
230 .optional()
231 .isIn(['asc', 'desc'])
232 .withMessage('Order must be asc or desc')
233 ]
234 });
235
236 // File upload validation rules
237 this.validators.set('upload', {
238 image: [
239 body('alt')
240 .optional()
241 .trim()
242 .isLength({ max: 200 })
243 .withMessage('Alt text cannot exceed 200 characters')
244 .customSanitizer((value) => this.sanitizeHtml(value)),
245
246 body('caption')
247 .optional()
248 .trim()
249 .isLength({ max: 500 })
250 .withMessage('Caption cannot exceed 500 characters')
251 .customSanitizer((value) => this.sanitizeHtml(value))
252 ]
253 });
254 }
255
256 setupDefaultSanitizers() {
257 this.sanitizers.set('html', (input, options = {}) => {
258 const defaultOptions = {
259 allowedTags: [],
260 allowedAttributes: {},
261 allowedIframeHostnames: []
262 };
263
264 const sanitizeOptions = { ...defaultOptions, ...options };
265
266 return DOMPurify.sanitize(input, sanitizeOptions);
267 });
268
269 this.sanitizers.set('xss', (input) => {
270 return xss(input, {
271 whiteList: {}, // No HTML tags allowed
272 stripIgnoreTag: true,
273 stripIgnoreTagBody: ['script']
274 });
275 });
276
277 this.sanitizers.set('sql', (input) => {
278 // Basic SQL injection prevention
279 return input
280 .replace(/['";\\\\]/g, '') // Remove quotes and backslashes
281 .replace(/\\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\\b/gi, '') // Remove SQL keywords
282 .trim();
283 });
284
285 this.sanitizers.set('filename', (input) => {
286 return input
287 .replace(/[^a-zA-Z0-9.-]/g, '_') // Replace non-alphanumeric chars except dots and dashes
288 .replace(/\\.{2,}/g, '.') // Replace multiple dots with single dot
289 .replace(/^[.-]+|[.-]+$/g, '') // Remove leading/trailing dots and dashes
290 .substring(0, 255); // Limit length
291 });
292 }
293
294 // Validation middleware factory
295 validate(type, operation = 'create') {
296 return async (req, res, next) => {
297 try {
298 // Get validation rules
299 const validationRules = this.validators.get(type)?.[operation];
300
301 if (!validationRules) {
302 return res.status(500).json({
303 error: 'Internal Server Error',
304 message: 'Validation rules not found'
305 });
306 }
307
308 // Run validations
309 await Promise.all(validationRules.map(validation => validation.run(req)));
310
311 // Check for validation errors
312 const errors = validationResult(req);
313
314 if (!errors.isEmpty()) {
315 const formattedErrors = this.formatValidationErrors(errors.array());
316
317 return res.status(422).json({
318 error: 'Validation Error',
319 message: 'Input validation failed',
320 details: formattedErrors,
321 timestamp: new Date().toISOString()
322 });
323 }
324
325 // Additional security checks
326 await this.performSecurityChecks(req);
327
328 next();
329
330 } catch (error) {
331 console.error('Validation middleware error:', error);
332 res.status(500).json({
333 error: 'Internal Server Error',
334 message: 'Validation processing failed'
335 });
336 }
337 };
338 }
339
340 // Advanced sanitization methods
341 sanitizeHtml(input, options = {}) {
342 if (!input || typeof input !== 'string') return input;
343
344 const defaultOptions = {
345 allowedTags: [],
346 allowedAttributes: {},
347 allowedIframeHostnames: []
348 };
349
350 return DOMPurify.sanitize(input, { ...defaultOptions, ...options });
351 }
352
353 sanitizeName(name) {
354 if (!name || typeof name !== 'string') return name;
355
356 return name
357 .trim()
358 .replace(/\\s+/g, ' ') // Replace multiple spaces with single space
359 .replace(/[^a-zA-Z\\s'-]/g, '') // Remove non-letter characters except spaces, apostrophes, hyphens
360 .substring(0, 50);
361 }
362
363 sanitizePhoneNumber(phone) {
364 if (!phone || typeof phone !== 'string') return phone;
365
366 return phone
367 .replace(/[^0-9+()-\\s]/g, '') // Keep only numbers, plus, parentheses, hyphens, spaces
368 .trim();
369 }
370
371 sanitizeSearchQuery(query) {
372 if (!query || typeof query !== 'string') return query;
373
374 return query
375 .trim()
376 .replace(/[<>'"]/g, '') // Remove potentially dangerous characters
377 .replace(/\\s+/g, ' ') // Normalize spaces
378 .substring(0, 100);
379 }
380
381 sanitizeFilePath(path) {
382 if (!path || typeof path !== 'string') return path;
383
384 return path
385 .replace(/\\.\\./g, '') // Remove directory traversal attempts
386 .replace(/[<>:"|?*]/g, '') // Remove invalid filename characters
387 .replace(/^[/\\\\]+/, '') // Remove leading slashes
388 .substring(0, 255);
389 }
390
391 // Security validation methods
392 isValidImageUrl(url) {
393 try {
394 const urlObj = new URL(url);
395 const allowedHosts = process.env.ALLOWED_IMAGE_HOSTS?.split(',') || [];
396 const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
397
398 // Check if host is allowed (if allowlist exists)
399 if (allowedHosts.length > 0 && !allowedHosts.includes(urlObj.hostname)) {
400 return false;
401 }
402
403 // Check file extension
404 const pathname = urlObj.pathname.toLowerCase();
405 return allowedExtensions.some(ext => pathname.endsWith(ext));
406
407 } catch {
408 return false;
409 }
410 }
411
412 isDisposableEmail(email) {
413 const disposableDomains = [
414 '10minutemail.com', 'tempmail.org', 'guerrillamail.com',
415 'mailinator.com', 'temp-mail.org', 'throwaway.email'
416 // Add more disposable email domains
417 ];
418
419 const domain = email.split('@')[1]?.toLowerCase();
420 return disposableDomains.includes(domain);
421 }
422
423 isCommonPassword(password) {
424 const commonPasswords = [
425 'password', '123456', '12345678', 'qwerty', 'abc123',
426 'password123', 'admin', 'letmein', 'welcome', 'monkey'
427 // Add more common passwords
428 ];
429
430 return commonPasswords.includes(password.toLowerCase());
431 }
432
433 calculateAge(birthDate) {
434 const today = new Date();
435 const birth = new Date(birthDate);
436 let age = today.getFullYear() - birth.getFullYear();
437 const monthDiff = today.getMonth() - birth.getMonth();
438
439 if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
440 age--;
441 }
442
443 return age;
444 }
445
446 // File validation methods
447 validateFileUpload(req, res, next) {
448 const file = req.file;
449
450 if (!file) {
451 return res.status(400).json({
452 error: 'Bad Request',
453 message: 'No file uploaded'
454 });
455 }
456
457 // Check file size (10MB limit)
458 if (file.size > 10 * 1024 * 1024) {
459 return res.status(413).json({
460 error: 'Payload Too Large',
461 message: 'File size exceeds 10MB limit'
462 });
463 }
464
465 // Check MIME type
466 const allowedMimeTypes = [
467 'image/jpeg', 'image/png', 'image/gif', 'image/webp',
468 'application/pdf', 'text/plain', 'text/csv'
469 ];
470
471 if (!allowedMimeTypes.includes(file.mimetype)) {
472 return res.status(400).json({
473 error: 'Bad Request',
474 message: 'File type not allowed'
475 });
476 }
477
478 // Sanitize filename
479 file.originalname = this.sanitizers.get('filename')(file.originalname);
480
481 // Validate file content (basic magic number check)
482 if (!this.validateFileContent(file)) {
483 return res.status(400).json({
484 error: 'Bad Request',
485 message: 'File content validation failed'
486 });
487 }
488
489 next();
490 }
491
492 validateFileContent(file) {
493 // Check magic numbers to verify file type matches extension
494 const buffer = file.buffer;
495 if (!buffer || buffer.length < 4) return false;
496
497 const magicNumbers = {
498 'image/jpeg': [0xFF, 0xD8, 0xFF],
499 'image/png': [0x89, 0x50, 0x4E, 0x47],
500 'image/gif': [0x47, 0x49, 0x46],
501 'application/pdf': [0x25, 0x50, 0x44, 0x46]
502 };
503
504 const fileMagicNumber = magicNumbers[file.mimetype];
505 if (!fileMagicNumber) return true; // Unknown type, allow
506
507 for (let i = 0; i < fileMagicNumber.length; i++) {
508 if (buffer[i] !== fileMagicNumber[i]) {
509 return false;
510 }
511 }
512
513 return true;
514 }
515
516 // Security checks
517 async performSecurityChecks(req) {
518 // Check for suspicious patterns
519 const suspiciousPatterns = [
520 /<script[^>]*>.*?<\\/script>/gi,
521 /javascript:/gi,
522 /on\\w+\\s*=/gi,
523 /\\b(eval|setTimeout|setInterval)\\s*\\(/gi,
524 /(union|select|insert|update|delete|drop)\\s+/gi
525 ];
526
527 const requestString = JSON.stringify(req.body);
528
529 for (const pattern of suspiciousPatterns) {
530 if (pattern.test(requestString)) {
531 throw new Error('Suspicious content detected');
532 }
533 }
534
535 // Check request size
536 const maxSize = 1024 * 1024; // 1MB
537 if (Buffer.byteLength(requestString, 'utf8') > maxSize) {
538 throw new Error('Request payload too large');
539 }
540 }
541
542 formatValidationErrors(errors) {
543 return errors.map(error => ({
544 field: error.path || error.param,
545 message: error.msg,
546 value: error.value,
547 location: error.location
548 }));
549 }
550
551 // Utility method to get validator
552 getValidator(type, operation) {
553 return this.validators.get(type)?.[operation] || [];
554 }
555
556 // Utility method to get sanitizer
557 getSanitizer(type) {
558 return this.sanitizers.get(type);
559 }
560}
561
562module.exports = InputValidator;Configure comprehensive CORS policies and security headers for production APIs.
1const helmet = require('helmet');
2const cors = require('cors');
3
4class SecurityHeadersManager {
5 constructor() {
6 this.corsConfig = this.createCorsConfig();
7 this.helmetConfig = this.createHelmetConfig();
8 }
9
10 createCorsConfig() {
11 return {
12 // Origin configuration
13 origin: (origin, callback) => {
14 // Allow requests with no origin (mobile apps, curl, etc.)
15 if (!origin) {
16 return callback(null, true);
17 }
18
19 const allowedOrigins = this.getAllowedOrigins();
20
21 if (this.isOriginAllowed(origin, allowedOrigins)) {
22 callback(null, true);
23 } else {
24 console.warn(\`CORS blocked origin: \${origin}\`);
25 callback(new Error('Not allowed by CORS policy'));
26 }
27 },
28
29 // Methods allowed
30 methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],
31
32 // Headers allowed in requests
33 allowedHeaders: [
34 'Content-Type',
35 'Authorization',
36 'X-Requested-With',
37 'X-API-Key',
38 'X-API-Version',
39 'X-Client-Version',
40 'X-Request-ID',
41 'Accept',
42 'Accept-Language',
43 'Accept-Encoding',
44 'Cache-Control',
45 'Connection',
46 'Host',
47 'Origin',
48 'Referer',
49 'User-Agent'
50 ],
51
52 // Headers exposed to the client
53 exposedHeaders: [
54 'X-Total-Count',
55 'X-Page-Count',
56 'X-Current-Page',
57 'X-Per-Page',
58 'X-Rate-Limit-Limit',
59 'X-Rate-Limit-Remaining',
60 'X-Rate-Limit-Reset',
61 'X-Response-Time',
62 'X-Request-ID',
63 'Link',
64 'Location',
65 'ETag',
66 'Last-Modified'
67 ],
68
69 // Allow credentials (cookies, authorization headers)
70 credentials: true,
71
72 // Preflight cache duration (in seconds)
73 maxAge: 86400, // 24 hours
74
75 // Handle preflight automatically
76 preflightContinue: false,
77 optionsSuccessStatus: 204
78 };
79 }
80
81 getAllowedOrigins() {
82 const envOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
83 const defaultOrigins = [];
84
85 // Environment-specific defaults
86 switch (process.env.NODE_ENV) {
87 case 'development':
88 defaultOrigins.push(
89 'http://localhost:3000',
90 'http://localhost:3001',
91 'http://localhost:8080',
92 'http://127.0.0.1:3000'
93 );
94 break;
95
96 case 'staging':
97 defaultOrigins.push(
98 'https://staging.example.com',
99 'https://staging-admin.example.com'
100 );
101 break;
102
103 case 'production':
104 defaultOrigins.push(
105 'https://example.com',
106 'https://www.example.com',
107 'https://admin.example.com',
108 'https://app.example.com'
109 );
110 break;
111 }
112
113 return [...new Set([...envOrigins, ...defaultOrigins])];
114 }
115
116 isOriginAllowed(origin, allowedOrigins) {
117 // Exact match
118 if (allowedOrigins.includes(origin)) {
119 return true;
120 }
121
122 // Wildcard subdomain matching (*.example.com)
123 for (const allowed of allowedOrigins) {
124 if (allowed.startsWith('*.')) {
125 const domain = allowed.substring(2);
126 const originUrl = new URL(origin);
127
128 if (originUrl.hostname.endsWith(domain)) {
129 return true;
130 }
131 }
132 }
133
134 // Development mode - allow localhost with any port
135 if (process.env.NODE_ENV === 'development') {
136 try {
137 const originUrl = new URL(origin);
138 if (originUrl.hostname === 'localhost' || originUrl.hostname === '127.0.0.1') {
139 return true;
140 }
141 } catch {
142 // Invalid URL, reject
143 }
144 }
145
146 return false;
147 }
148
149 createHelmetConfig() {
150 return {
151 // Content Security Policy
152 contentSecurityPolicy: {
153 directives: {
154 defaultSrc: ["'self'"],
155 scriptSrc: [
156 "'self'",
157 "'unsafe-inline'", // Required for some admin interfaces
158 'https://cdn.jsdelivr.net',
159 'https://unpkg.com'
160 ],
161 styleSrc: [
162 "'self'",
163 "'unsafe-inline'",
164 'https://fonts.googleapis.com',
165 'https://cdn.jsdelivr.net'
166 ],
167 fontSrc: [
168 "'self'",
169 'https://fonts.gstatic.com',
170 'data:'
171 ],
172 imgSrc: [
173 "'self'",
174 'data:',
175 'https:',
176 'blob:'
177 ],
178 connectSrc: [
179 "'self'",
180 'https://api.example.com',
181 'wss://api.example.com'
182 ],
183 mediaSrc: ["'self'", 'https:', 'data:'],
184 objectSrc: ["'none'"],
185 baseUri: ["'self'"],
186 formAction: ["'self'"],
187 frameAncestors: ["'none'"],
188 upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null
189 },
190 reportOnly: process.env.NODE_ENV === 'development'
191 },
192
193 // HTTP Strict Transport Security
194 hsts: {
195 maxAge: 31536000, // 1 year
196 includeSubDomains: true,
197 preload: true
198 },
199
200 // X-Frame-Options
201 frameguard: {
202 action: 'deny'
203 },
204
205 // X-Content-Type-Options
206 noSniff: true,
207
208 // X-XSS-Protection
209 xssFilter: true,
210
211 // Referrer Policy
212 referrerPolicy: {
213 policy: ['strict-origin-when-cross-origin']
214 },
215
216 // X-Permitted-Cross-Domain-Policies
217 permittedCrossDomainPolicies: false,
218
219 // X-Download-Options
220 ieNoOpen: true,
221
222 // X-DNS-Prefetch-Control
223 dnsPrefetchControl: {
224 allow: false
225 },
226
227 // Expect-CT
228 expectCt: {
229 maxAge: 30,
230 enforce: true,
231 reportUri: 'https://example.com/ct-report'
232 },
233
234 // Feature Policy / Permissions Policy
235 permissionsPolicy: {
236 features: {
237 camera: ['self'],
238 microphone: ['self'],
239 geolocation: ['self'],
240 payment: ['self'],
241 fullscreen: ['self'],
242 accelerometer: [],
243 gyroscope: [],
244 magnetometer: [],
245 usb: [],
246 bluetooth: []
247 }
248 }
249 };
250 }
251
252 // Middleware factory
253 createSecurityMiddleware() {
254 return [
255 // Custom security headers
256 this.customSecurityHeaders(),
257
258 // Helmet middleware
259 helmet(this.helmetConfig),
260
261 // CORS middleware
262 cors(this.corsConfig),
263
264 // Additional security checks
265 this.additionalSecurityChecks()
266 ];
267 }
268
269 customSecurityHeaders() {
270 return (req, res, next) => {
271 // API-specific headers
272 res.set({
273 'X-API-Version': process.env.API_VERSION || '1.0.0',
274 'X-Server-ID': process.env.SERVER_ID || 'unknown',
275 'X-Request-ID': req.id || this.generateRequestId(),
276 'X-Powered-By': 'Secure Node.js API', // Override default
277 'X-Robots-Tag': 'noindex, nofollow', // Prevent API indexing
278 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
279 'Pragma': 'no-cache',
280 'Expires': '0'
281 });
282
283 // Remove server signature
284 res.removeHeader('X-Powered-By');
285
286 // Add timing header (for performance monitoring)
287 const startTime = Date.now();
288 res.on('finish', () => {
289 res.set('X-Response-Time', \`\${Date.now() - startTime}ms\`);
290 });
291
292 next();
293 };
294 }
295
296 additionalSecurityChecks() {
297 return (req, res, next) => {
298 // Block suspicious user agents
299 const userAgent = req.get('User-Agent') || '';
300 if (this.isSuspiciousUserAgent(userAgent)) {
301 return res.status(403).json({
302 error: 'Forbidden',
303 message: 'Access denied'
304 });
305 }
306
307 // Check for suspicious headers
308 if (this.hasSuspiciousHeaders(req)) {
309 return res.status(400).json({
310 error: 'Bad Request',
311 message: 'Invalid request headers'
312 });
313 }
314
315 // Rate limit by IP and User-Agent combination
316 const fingerprint = this.generateFingerprint(req);
317 req.fingerprint = fingerprint;
318
319 next();
320 };
321 }
322
323 isSuspiciousUserAgent(userAgent) {
324 const suspiciousPatterns = [
325 /bot/i,
326 /crawler/i,
327 /spider/i,
328 /scraper/i,
329 /scanner/i,
330 /nuclei/i,
331 /nmap/i,
332 /sqlmap/i,
333 /nikto/i
334 ];
335
336 // Allow legitimate bots in development
337 if (process.env.NODE_ENV === 'development') {
338 return false;
339 }
340
341 return suspiciousPatterns.some(pattern => pattern.test(userAgent));
342 }
343
344 hasSuspiciousHeaders(req) {
345 const suspiciousHeaders = [
346 'x-forwarded-for',
347 'x-originating-ip',
348 'x-remote-ip',
349 'x-cluster-client-ip'
350 ];
351
352 // Check for header injection attempts
353 for (const header of suspiciousHeaders) {
354 const value = req.get(header);
355 if (value && (value.includes('\\n') || value.includes('\\r'))) {
356 return true;
357 }
358 }
359
360 return false;
361 }
362
363 generateFingerprint(req) {
364 const components = [
365 req.ip,
366 req.get('User-Agent') || '',
367 req.get('Accept-Language') || '',
368 req.get('Accept-Encoding') || ''
369 ];
370
371 const crypto = require('crypto');
372 return crypto
373 .createHash('sha256')
374 .update(components.join('|'))
375 .digest('hex')
376 .substring(0, 16);
377 }
378
379 generateRequestId() {
380 const crypto = require('crypto');
381 return crypto.randomUUID();
382 }
383
384 // Environment-specific configurations
385 getDevelopmentConfig() {
386 return {
387 ...this.corsConfig,
388 origin: true, // Allow all origins in development
389 credentials: true
390 };
391 }
392
393 getProductionConfig() {
394 return {
395 ...this.corsConfig,
396 // Stricter settings for production
397 maxAge: 86400,
398 credentials: true,
399 optionsSuccessStatus: 200
400 };
401 }
402
403 // Dynamic CORS configuration
404 createDynamicCors() {
405 return (req, res, next) => {
406 const origin = req.get('Origin');
407
408 // API endpoints vs web endpoints might have different policies
409 let corsOptions = { ...this.corsConfig };
410
411 if (req.path.startsWith('/api/')) {
412 // API-specific CORS rules
413 corsOptions.credentials = req.path.startsWith('/api/auth/');
414 corsOptions.maxAge = 3600; // Shorter cache for API
415 } else {
416 // Web interface CORS rules
417 corsOptions.credentials = true;
418 corsOptions.maxAge = 86400;
419 }
420
421 // Apply environment-specific overrides
422 if (process.env.NODE_ENV === 'development') {
423 corsOptions = this.getDevelopmentConfig();
424 } else if (process.env.NODE_ENV === 'production') {
425 corsOptions = this.getProductionConfig();
426 }
427
428 cors(corsOptions)(req, res, next);
429 };
430 }
431
432 // Security monitoring
433 logSecurityEvent(req, type, details) {
434 const event = {
435 timestamp: new Date().toISOString(),
436 type,
437 ip: req.ip,
438 userAgent: req.get('User-Agent'),
439 origin: req.get('Origin'),
440 referer: req.get('Referer'),
441 path: req.path,
442 method: req.method,
443 fingerprint: req.fingerprint,
444 details
445 };
446
447 console.warn('Security event:', event);
448
449 // In production, send to security monitoring system
450 // await this.sendToSecurityMonitoring(event);
451 }
452}
453
454module.exports = SecurityHeadersManager;Learn about Practice Exercises.
Exercise 1: Secure API Implementation
Build a complete secure API with multi-strategy authentication, comprehensive rate limiting, and input validation for a banking application.
Exercise 2: Security Vulnerability Assessment
Conduct a security audit of an existing API, identify vulnerabilities, and implement comprehensive security measures and monitoring.
Exercise 3: Security Incident Response
Design and implement a security incident response system with automated threat detection, alerting, and mitigation strategies.
Authentication:
Implement multi-strategy authentication with proper token management
Rate Limiting:
Design sophisticated rate limiting for different user types and endpoints
Input Validation:
Apply comprehensive validation and sanitization to prevent injection attacks
Security Headers:
Configure CORS and security headers for production environments
Monitoring:
Implement security event logging and incident response procedures
Defense in Depth:
Apply multiple layers of security controls for comprehensive protection
You've mastered API security best practices! You can now build and secure production-grade APIs with comprehensive authentication, authorization, input validation, and security monitoring systems.
Your progress is saved in this browser, on this device only.