feat: enhance security configuration and SSE management with robust token validation and client tracking

- Refactored `.env.example` with comprehensive security and configuration parameters
- Added new `security.config.ts` for centralized security configuration management
- Improved middleware with enhanced authentication, request validation, and error handling
- Updated SSE routes and manager with advanced client tracking, rate limiting, and connection management
- Implemented more granular token validation with IP-based rate limiting and connection tracking
- Added detailed error responses and improved logging for security-related events
This commit is contained in:
jango-blockchained
2025-02-03 22:02:12 +01:00
parent 840927998e
commit a814c427e9
8 changed files with 605 additions and 227 deletions

View File

@@ -1,6 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { HASS_CONFIG, RATE_LIMIT_CONFIG } from '../config/index.js';
import rateLimit from 'express-rate-limit';
import { TokenManager } from '../security/index.js';
// Rate limiter middleware
export const rateLimiter = rateLimit({
@@ -24,34 +25,79 @@ export const wsRateLimiter = rateLimit({
}
});
// Security headers middleware
export const securityHeaders = (_req: Request, res: Response, next: NextFunction) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Authentication middleware
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '') || '';
const clientIp = req.ip || req.socket.remoteAddress || '';
const validationResult = TokenManager.validateToken(token, clientIp);
if (!validationResult.valid) {
return res.status(401).json({
success: false,
message: 'Unauthorized',
error: validationResult.error || 'Invalid token',
timestamp: new Date().toISOString()
});
}
next();
};
// Request validation middleware
// Enhanced security headers middleware
export const securityHeaders = (_req: Request, res: Response, next: NextFunction) => {
// Set strict security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss: https:;");
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
};
// Enhanced request validation middleware
export const validateRequest = (req: Request, res: Response, next: NextFunction) => {
// Skip validation for health check endpoints
if (req.path === '/health' || req.path === '/mcp') {
return next();
}
// Validate content type for POST/PUT/PATCH requests
if (['POST', 'PUT', 'PATCH'].includes(req.method) && !req.is('application/json')) {
return res.status(415).json({
success: false,
message: 'Content-Type must be application/json'
message: 'Unsupported Media Type',
error: 'Content-Type must be application/json',
timestamp: new Date().toISOString()
});
}
// Validate request body size
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
if (contentLength > 1024 * 1024) { // 1MB limit
const maxSize = 1024 * 1024; // 1MB limit
if (contentLength > maxSize) {
return res.status(413).json({
success: false,
message: 'Request body too large'
message: 'Payload Too Large',
error: `Request body must not exceed ${maxSize} bytes`,
timestamp: new Date().toISOString()
});
}
// Validate request body structure
if (req.method !== 'GET' && req.body) {
if (typeof req.body !== 'object' || Array.isArray(req.body)) {
return res.status(400).json({
success: false,
message: 'Bad Request',
error: 'Invalid request body structure',
timestamp: new Date().toISOString()
});
}
}
next();
};
@@ -84,21 +130,7 @@ export const sanitizeInput = (req: Request, _res: Response, next: NextFunction)
next();
};
// Authentication middleware
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token || token !== HASS_CONFIG.TOKEN) {
return res.status(401).json({
success: false,
message: 'Unauthorized - Invalid token'
});
}
next();
};
// Error handling middleware
// Enhanced error handling middleware
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('Error:', err);
@@ -106,8 +138,9 @@ export const errorHandler = (err: Error, _req: Request, res: Response, _next: Ne
if (err.name === 'ValidationError') {
return res.status(400).json({
success: false,
message: 'Validation error',
details: err.message
message: 'Validation Error',
error: err.message,
timestamp: new Date().toISOString()
});
}
@@ -115,15 +148,26 @@ export const errorHandler = (err: Error, _req: Request, res: Response, _next: Ne
return res.status(401).json({
success: false,
message: 'Unauthorized',
details: err.message
error: err.message,
timestamp: new Date().toISOString()
});
}
if (err.name === 'ForbiddenError') {
return res.status(403).json({
success: false,
message: 'Forbidden',
error: err.message,
timestamp: new Date().toISOString()
});
}
// Default error response
res.status(500).json({
success: false,
message: 'Internal server error',
details: process.env.NODE_ENV === 'development' ? err.message : undefined
message: 'Internal Server Error',
error: process.env.NODE_ENV === 'development' ? err.message : 'An unexpected error occurred',
timestamp: new Date().toISOString()
});
};