streamflow/backend/middleware/securityEnhancements.js

350 lines
9.7 KiB
JavaScript
Raw Normal View History

/**
* 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
};