Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
349
backend/middleware/securityEnhancements.js
Normal file
349
backend/middleware/securityEnhancements.js
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
/**
|
||||
* Enhanced Security Middleware
|
||||
* Implements account lockout, password expiry, and session management
|
||||
*/
|
||||
|
||||
const { db } = require('../database/db');
|
||||
const { ACCOUNT_LOCKOUT, PASSWORD_EXPIRY, SESSION_POLICY } = require('../utils/passwordPolicy');
|
||||
const SecurityAuditLogger = require('../utils/securityAudit');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Check if account is locked
|
||||
*/
|
||||
async function checkAccountLockout(userId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT locked_until FROM users WHERE id = ?',
|
||||
[userId],
|
||||
(err, user) => {
|
||||
if (err) return reject(err);
|
||||
if (!user) return resolve({ locked: false });
|
||||
|
||||
if (user.locked_until) {
|
||||
const lockoutEnd = new Date(user.locked_until);
|
||||
const now = new Date();
|
||||
|
||||
if (now < lockoutEnd) {
|
||||
const remainingMinutes = Math.ceil((lockoutEnd - now) / 60000);
|
||||
return resolve({
|
||||
locked: true,
|
||||
remainingMinutes,
|
||||
message: `Account locked. Try again in ${remainingMinutes} minutes.`
|
||||
});
|
||||
} else {
|
||||
// Lockout expired, clear it
|
||||
db.run('UPDATE users SET locked_until = NULL, failed_login_attempts = 0 WHERE id = ?', [userId]);
|
||||
return resolve({ locked: false });
|
||||
}
|
||||
}
|
||||
|
||||
resolve({ locked: false });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Record failed login attempt
|
||||
*/
|
||||
async function recordFailedLogin(username, ip, userAgent) {
|
||||
try {
|
||||
// Log to audit
|
||||
await SecurityAuditLogger.logLoginFailure(username, 'Invalid credentials', { ip, userAgent });
|
||||
|
||||
// Get user ID
|
||||
const user = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT id, failed_login_attempts FROM users WHERE username = ? OR email = ?', [username, username], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const failedAttempts = (user.failed_login_attempts || 0) + 1;
|
||||
|
||||
// Update failed attempts
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE users SET failed_login_attempts = ?, last_failed_login = ? WHERE id = ?',
|
||||
[failedAttempts, new Date().toISOString(), user.id],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
|
||||
// Check if lockout threshold reached
|
||||
if (failedAttempts >= ACCOUNT_LOCKOUT.maxFailedAttempts) {
|
||||
const lockUntil = new Date(Date.now() + ACCOUNT_LOCKOUT.lockoutDuration).toISOString();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE users SET locked_until = ? WHERE id = ?',
|
||||
[lockUntil, user.id],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
|
||||
await SecurityAuditLogger.logAccountLockout(user.id, {
|
||||
ip,
|
||||
userAgent,
|
||||
failedAttempts
|
||||
});
|
||||
|
||||
logger.warn(`Account locked for user ${username} after ${failedAttempts} failed attempts`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error recording failed login:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear failed login attempts on successful login
|
||||
*/
|
||||
async function clearFailedAttempts(userId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE users SET failed_login_attempts = 0, last_failed_login = NULL WHERE id = ?',
|
||||
[userId],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if password has expired
|
||||
*/
|
||||
async function checkPasswordExpiry(userId) {
|
||||
if (!PASSWORD_EXPIRY.enabled) {
|
||||
return { expired: false, warning: false };
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT password_changed_at, password_expires_at FROM users WHERE id = ?',
|
||||
[userId],
|
||||
(err, user) => {
|
||||
if (err) return reject(err);
|
||||
if (!user) return resolve({ expired: false, warning: false });
|
||||
|
||||
const now = new Date();
|
||||
let expiryDate;
|
||||
|
||||
if (user.password_expires_at) {
|
||||
expiryDate = new Date(user.password_expires_at);
|
||||
} else if (user.password_changed_at) {
|
||||
expiryDate = new Date(user.password_changed_at);
|
||||
expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays);
|
||||
} else {
|
||||
// No password change date, set expiry from now
|
||||
expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays);
|
||||
}
|
||||
|
||||
const daysUntilExpiry = Math.ceil((expiryDate - now) / (24 * 60 * 60 * 1000));
|
||||
|
||||
if (daysUntilExpiry <= 0) {
|
||||
return resolve({
|
||||
expired: true,
|
||||
warning: false,
|
||||
message: 'Your password has expired. Please change it to continue.',
|
||||
gracePeriodDays: PASSWORD_EXPIRY.gracePeriodDays
|
||||
});
|
||||
}
|
||||
|
||||
if (daysUntilExpiry <= PASSWORD_EXPIRY.warningDays) {
|
||||
return resolve({
|
||||
expired: false,
|
||||
warning: true,
|
||||
daysRemaining: daysUntilExpiry,
|
||||
message: `Your password will expire in ${daysUntilExpiry} days. Please change it soon.`
|
||||
});
|
||||
}
|
||||
|
||||
resolve({ expired: false, warning: false });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update password expiry date
|
||||
*/
|
||||
async function updatePasswordExpiry(userId) {
|
||||
if (!PASSWORD_EXPIRY.enabled) return;
|
||||
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(now);
|
||||
expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE users SET password_changed_at = ?, password_expires_at = ? WHERE id = ?',
|
||||
[now.toISOString(), expiryDate.toISOString(), userId],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware: Check account lockout before authentication
|
||||
*/
|
||||
const enforceAccountLockout = async (req, res, next) => {
|
||||
const { username } = req.body;
|
||||
|
||||
if (!username) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
// Get user
|
||||
const user = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT id FROM users WHERE username = ? OR email = ?', [username, username], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return next(); // User doesn't exist, let auth handle it
|
||||
}
|
||||
|
||||
// Check lockout
|
||||
const lockoutStatus = await checkAccountLockout(user.id);
|
||||
|
||||
if (lockoutStatus.locked) {
|
||||
return res.status(423).json({
|
||||
error: lockoutStatus.message,
|
||||
remainingMinutes: lockoutStatus.remainingMinutes,
|
||||
locked: true
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Account lockout check error:', error);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware: Check password expiry after authentication
|
||||
*/
|
||||
const enforcePasswordExpiry = async (req, res, next) => {
|
||||
if (!req.user || !req.user.userId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const expiryStatus = await checkPasswordExpiry(req.user.userId);
|
||||
|
||||
if (expiryStatus.expired) {
|
||||
return res.status(403).json({
|
||||
error: expiryStatus.message,
|
||||
passwordExpired: true,
|
||||
gracePeriodDays: expiryStatus.gracePeriodDays,
|
||||
requirePasswordChange: true
|
||||
});
|
||||
}
|
||||
|
||||
if (expiryStatus.warning) {
|
||||
// Add warning header but allow request
|
||||
res.setHeader('X-Password-Expiry-Warning', expiryStatus.message);
|
||||
res.setHeader('X-Password-Days-Remaining', expiryStatus.daysRemaining.toString());
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error('Password expiry check error:', error);
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Manage active sessions
|
||||
*/
|
||||
async function createSession(userId, token, req) {
|
||||
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
const userAgent = req.headers['user-agent'];
|
||||
const expiresAt = new Date(Date.now() + SESSION_POLICY.absoluteTimeout);
|
||||
|
||||
// Check concurrent sessions
|
||||
const activeSessions = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
'SELECT COUNT(*) as count FROM active_sessions WHERE user_id = ? AND expires_at > ?',
|
||||
[userId, new Date().toISOString()],
|
||||
(err, rows) => err ? reject(err) : resolve(rows[0].count)
|
||||
);
|
||||
});
|
||||
|
||||
if (activeSessions >= SESSION_POLICY.maxConcurrentSessions) {
|
||||
// Remove oldest session
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'DELETE FROM active_sessions WHERE id IN (SELECT id FROM active_sessions WHERE user_id = ? ORDER BY last_activity ASC LIMIT 1)',
|
||||
[userId],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Create new session
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO active_sessions (user_id, session_token, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?)',
|
||||
[userId, token, ip, userAgent, expiresAt.toISOString()],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session activity
|
||||
*/
|
||||
async function updateSessionActivity(token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'UPDATE active_sessions SET last_activity = ? WHERE session_token = ?',
|
||||
[new Date().toISOString(), token],
|
||||
(err) => err ? reject(err) : resolve()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired sessions
|
||||
*/
|
||||
async function cleanupExpiredSessions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(
|
||||
'DELETE FROM active_sessions WHERE expires_at < ?',
|
||||
[new Date().toISOString()],
|
||||
function(err) {
|
||||
if (err) reject(err);
|
||||
else {
|
||||
if (this.changes > 0) {
|
||||
logger.info(`Cleaned up ${this.changes} expired sessions`);
|
||||
}
|
||||
resolve(this.changes);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Run session cleanup every hour
|
||||
setInterval(cleanupExpiredSessions, 60 * 60 * 1000);
|
||||
|
||||
module.exports = {
|
||||
checkAccountLockout,
|
||||
recordFailedLogin,
|
||||
clearFailedAttempts,
|
||||
checkPasswordExpiry,
|
||||
updatePasswordExpiry,
|
||||
enforceAccountLockout,
|
||||
enforcePasswordExpiry,
|
||||
createSession,
|
||||
updateSessionActivity,
|
||||
cleanupExpiredSessions
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue