Fix jwt-token
This commit is contained in:
@@ -17,6 +17,7 @@ const { initializeHealthService } = require('./routes/deviceHealth');
|
||||
const seedDatabase = require('./seedDatabase');
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
const { apiDebugMiddleware } = require('./utils/apiDebugLogger');
|
||||
const IPRestrictionMiddleware = require('./middleware/ip-restriction');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -79,6 +80,10 @@ if (process.env.API_DEBUG === 'true') {
|
||||
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// IP Restriction middleware (before routes)
|
||||
const ipRestriction = new IPRestrictionMiddleware();
|
||||
app.use((req, res, next) => ipRestriction.checkIPRestriction(req, res, next));
|
||||
|
||||
// Make io available to routes
|
||||
app.use((req, res, next) => {
|
||||
req.io = io;
|
||||
|
||||
206
server/middleware/ip-restriction.js
Normal file
206
server/middleware/ip-restriction.js
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* IP Restriction Middleware
|
||||
* Checks if the client IP is allowed access based on tenant configuration
|
||||
*/
|
||||
|
||||
const { Tenant } = require('../models');
|
||||
const MultiTenantAuth = require('./multi-tenant-auth');
|
||||
|
||||
class IPRestrictionMiddleware {
|
||||
constructor() {
|
||||
this.multiAuth = new MultiTenantAuth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP address matches any pattern in the whitelist
|
||||
* @param {string} clientIP - The client IP address
|
||||
* @param {Array} whitelist - Array of IP addresses and CIDR blocks
|
||||
* @returns {boolean} - True if IP is allowed
|
||||
*/
|
||||
isIPAllowed(clientIP, whitelist) {
|
||||
if (!whitelist || !Array.isArray(whitelist) || whitelist.length === 0) {
|
||||
return true; // No restrictions
|
||||
}
|
||||
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIP = clientIP.replace(/^::ffff:/, '');
|
||||
|
||||
for (const allowedIP of whitelist) {
|
||||
if (this.matchesPattern(normalizedIP, allowedIP.trim())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP matches a pattern (IP address or CIDR block)
|
||||
* @param {string} ip - The IP to check
|
||||
* @param {string} pattern - IP address or CIDR block
|
||||
* @returns {boolean} - True if IP matches pattern
|
||||
*/
|
||||
matchesPattern(ip, pattern) {
|
||||
// Exact IP match
|
||||
if (ip === pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CIDR block matching
|
||||
if (pattern.includes('/')) {
|
||||
return this.isIPInCIDR(ip, pattern);
|
||||
}
|
||||
|
||||
// Wildcard matching (e.g., 192.168.1.*)
|
||||
if (pattern.includes('*')) {
|
||||
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '\\d+') + '$');
|
||||
return regex.test(ip);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP is within a CIDR block
|
||||
* @param {string} ip - The IP to check
|
||||
* @param {string} cidr - CIDR block (e.g., "192.168.1.0/24")
|
||||
* @returns {boolean} - True if IP is in CIDR block
|
||||
*/
|
||||
isIPInCIDR(ip, cidr) {
|
||||
try {
|
||||
const [subnet, prefixLength] = cidr.split('/');
|
||||
const prefix = parseInt(prefixLength, 10);
|
||||
|
||||
if (prefix < 0 || prefix > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ipNum = this.ipToNumber(ip);
|
||||
const subnetNum = this.ipToNumber(subnet);
|
||||
const mask = (-1 << (32 - prefix)) >>> 0;
|
||||
|
||||
return (ipNum & mask) === (subnetNum & mask);
|
||||
} catch (error) {
|
||||
console.error('Error checking CIDR:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert IP address to number
|
||||
* @param {string} ip - IP address
|
||||
* @returns {number} - IP as number
|
||||
*/
|
||||
ipToNumber(ip) {
|
||||
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP from request, considering proxy headers
|
||||
* @param {Object} req - Express request object
|
||||
* @returns {string} - Client IP address
|
||||
*/
|
||||
getClientIP(req) {
|
||||
// Check various headers for real IP (in order of preference)
|
||||
const possibleHeaders = [
|
||||
'x-forwarded-for',
|
||||
'x-real-ip',
|
||||
'x-client-ip',
|
||||
'cf-connecting-ip', // Cloudflare
|
||||
'x-cluster-client-ip',
|
||||
'x-forwarded',
|
||||
'forwarded-for',
|
||||
'forwarded'
|
||||
];
|
||||
|
||||
for (const header of possibleHeaders) {
|
||||
const ip = req.headers[header];
|
||||
if (ip) {
|
||||
// x-forwarded-for can contain multiple IPs, get the first one
|
||||
const firstIP = ip.split(',')[0].trim();
|
||||
if (this.isValidIP(firstIP)) {
|
||||
return firstIP;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to connection IP
|
||||
return req.connection.remoteAddress || req.socket.remoteAddress || req.ip || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic IP validation
|
||||
* @param {string} ip - IP address to validate
|
||||
* @returns {boolean} - True if valid IP
|
||||
*/
|
||||
isValidIP(ip) {
|
||||
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Express middleware to check IP restrictions
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
* @param {Function} next - Next middleware function
|
||||
*/
|
||||
async checkIPRestriction(req, res, next) {
|
||||
try {
|
||||
// Skip IP checking for health checks and internal requests
|
||||
if (req.path === '/health' || req.path === '/api/health') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Determine tenant
|
||||
const tenantId = await this.multiAuth.determineTenant(req);
|
||||
if (!tenantId) {
|
||||
// No tenant found, continue without IP checking
|
||||
return next();
|
||||
}
|
||||
|
||||
// Get tenant configuration
|
||||
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||
if (!tenant) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if IP restrictions are enabled
|
||||
if (!tenant.ip_restriction_enabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Get client IP
|
||||
const clientIP = this.getClientIP(req);
|
||||
|
||||
// Check if IP is allowed
|
||||
const isAllowed = this.isIPAllowed(clientIP, tenant.ip_whitelist);
|
||||
|
||||
if (!isAllowed) {
|
||||
console.log(`🚫 IP Access Denied: ${clientIP} attempted to access tenant "${tenantId}"`);
|
||||
|
||||
// Log the access attempt for security auditing
|
||||
console.log(`[SECURITY AUDIT] ${new Date().toISOString()} - IP ${clientIP} denied access to tenant ${tenantId} - User-Agent: ${req.headers['user-agent']}`);
|
||||
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this service.',
|
||||
code: 'IP_RESTRICTED',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// IP is allowed, continue
|
||||
console.log(`✅ IP Access Allowed: ${clientIP} accessing tenant "${tenantId}"`);
|
||||
next();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in IP restriction middleware:', error);
|
||||
// In case of error, allow access but log the issue
|
||||
next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IPRestrictionMiddleware;
|
||||
@@ -82,6 +82,25 @@ module.exports = (sequelize) => {
|
||||
comment: 'Tenant feature limits and enabled features'
|
||||
},
|
||||
|
||||
// Security Configuration
|
||||
ip_whitelist: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: 'Array of allowed IP addresses/CIDR blocks for this tenant'
|
||||
},
|
||||
ip_restriction_enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: 'Whether IP restrictions are enabled for this tenant'
|
||||
},
|
||||
ip_restriction_message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: 'Access denied. Your IP address is not authorized to access this tenant.',
|
||||
comment: 'Custom message shown when IP access is denied'
|
||||
},
|
||||
|
||||
// Contact Information
|
||||
admin_email: {
|
||||
type: DataTypes.STRING,
|
||||
|
||||
Reference in New Issue
Block a user