776 lines
26 KiB
JavaScript
776 lines
26 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { body, validationResult } = require('express-validator');
|
|
const { authLimiter } = require('../middleware/rateLimiter');
|
|
const { authenticate } = require('../middleware/auth');
|
|
const { db } = require('../database/db');
|
|
const logger = require('../utils/logger');
|
|
const { validatePassword, calculatePasswordStrength } = require('../utils/passwordPolicy');
|
|
const SecurityAuditLogger = require('../utils/securityAudit');
|
|
const {
|
|
enforceAccountLockout,
|
|
recordFailedLogin,
|
|
clearFailedAttempts,
|
|
updatePasswordExpiry,
|
|
createSession,
|
|
checkPasswordExpiry
|
|
} = require('../middleware/securityEnhancements');
|
|
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'change_this_in_production';
|
|
const JWT_EXPIRES_IN = '7d';
|
|
|
|
// Register - Controlled by DISABLE_SIGNUPS environment variable
|
|
router.post('/register',
|
|
[
|
|
body('username').trim().isLength({ min: 3, max: 50 }).isAlphanumeric(),
|
|
body('email').isEmail().normalizeEmail(),
|
|
body('password').notEmpty()
|
|
],
|
|
async (req, res) => {
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
|
|
// Check if signups are disabled (default: true)
|
|
const disableSignups = process.env.DISABLE_SIGNUPS !== 'false';
|
|
|
|
if (disableSignups) {
|
|
await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'blocked', {
|
|
ip,
|
|
userAgent,
|
|
reason: 'Registration disabled'
|
|
});
|
|
return res.status(403).json({
|
|
error: 'Registration is disabled. Contact an administrator to create your account.'
|
|
});
|
|
}
|
|
|
|
// If signups are enabled, proceed with registration (fallback for future flexibility)
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, email, password } = req.body;
|
|
|
|
try {
|
|
// Validate password against policy
|
|
const passwordValidation = validatePassword(password, username, email);
|
|
if (!passwordValidation.valid) {
|
|
await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'failed', {
|
|
ip,
|
|
userAgent,
|
|
username,
|
|
reason: passwordValidation.errors.join(', ')
|
|
});
|
|
return res.status(400).json({
|
|
error: 'Password does not meet requirements',
|
|
details: passwordValidation.errors,
|
|
strength: calculatePasswordStrength(password)
|
|
});
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
const now = new Date().toISOString();
|
|
|
|
db.run(
|
|
'INSERT INTO users (username, email, password, role, must_change_password, password_changed_at, password_expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[username, email, hashedPassword, 'user', 0, now, null],
|
|
async function(err) {
|
|
if (err) {
|
|
if (err.message.includes('UNIQUE')) {
|
|
await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'failed', {
|
|
ip,
|
|
userAgent,
|
|
username,
|
|
reason: 'Duplicate username or email'
|
|
});
|
|
return res.status(400).json({ error: 'Username or email already exists' });
|
|
}
|
|
logger.error('Registration error:', err);
|
|
return res.status(500).json({ error: 'Registration failed' });
|
|
}
|
|
|
|
const userId = this.lastID;
|
|
|
|
// Update password expiry
|
|
await updatePasswordExpiry(userId);
|
|
|
|
// Log successful registration
|
|
await SecurityAuditLogger.logAuthEvent(userId, 'registration', 'success', {
|
|
ip,
|
|
userAgent,
|
|
username
|
|
});
|
|
|
|
const token = jwt.sign(
|
|
{ userId, role: 'user' },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRES_IN }
|
|
);
|
|
|
|
// CWE-778: Log token issuance
|
|
await SecurityAuditLogger.logTokenIssuance(userId, 'JWT', {
|
|
ip,
|
|
userAgent,
|
|
expiresIn: JWT_EXPIRES_IN,
|
|
purpose: 'registration'
|
|
});
|
|
|
|
// Create session
|
|
await createSession(userId, token, req);
|
|
|
|
// Set secure HTTP-only cookie
|
|
res.cookie('auth_token', token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
});
|
|
|
|
res.status(201).json({
|
|
message: 'Registration successful',
|
|
token,
|
|
user: {
|
|
id: userId,
|
|
username,
|
|
email,
|
|
role: 'user'
|
|
}
|
|
});
|
|
}
|
|
);
|
|
} catch (error) {
|
|
logger.error('Registration error:', error);
|
|
await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'error', {
|
|
ip,
|
|
userAgent,
|
|
error: error.message
|
|
});
|
|
res.status(500).json({ error: 'Registration failed' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Login with strict rate limiting and account lockout
|
|
router.post('/login', authLimiter, enforceAccountLockout,
|
|
[
|
|
body('username').trim().notEmpty(),
|
|
body('password').notEmpty()
|
|
],
|
|
async (req, res) => {
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { username, password } = req.body;
|
|
|
|
try {
|
|
db.get(
|
|
'SELECT * FROM users WHERE username = ? OR email = ?',
|
|
[username, username],
|
|
async (err, user) => {
|
|
if (err) {
|
|
logger.error('Login error:', err);
|
|
return res.status(500).json({ error: 'Login failed' });
|
|
}
|
|
|
|
if (!user) {
|
|
await recordFailedLogin(username, ip, userAgent);
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
// Check if user is active
|
|
if (!user.is_active) {
|
|
await SecurityAuditLogger.logLoginFailure(username, 'Account disabled', { ip, userAgent });
|
|
return res.status(403).json({ error: 'Account is disabled. Contact an administrator.' });
|
|
}
|
|
|
|
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
if (!isValidPassword) {
|
|
await recordFailedLogin(username, ip, userAgent);
|
|
return res.status(401).json({ error: 'Invalid credentials' });
|
|
}
|
|
|
|
// Clear failed attempts on successful password check
|
|
await clearFailedAttempts(user.id);
|
|
|
|
// Check password expiry
|
|
const expiryStatus = await checkPasswordExpiry(user.id);
|
|
if (expiryStatus.expired) {
|
|
await SecurityAuditLogger.logLoginFailure(username, 'Password expired', { ip, userAgent });
|
|
return res.status(403).json({
|
|
error: expiryStatus.message,
|
|
passwordExpired: true,
|
|
requirePasswordChange: true
|
|
});
|
|
}
|
|
|
|
// Check if 2FA is enabled
|
|
if (user.two_factor_enabled) {
|
|
// Create temporary token for 2FA verification
|
|
const tempToken = jwt.sign(
|
|
{ userId: user.id, temp: true, purpose: '2fa' },
|
|
JWT_SECRET,
|
|
{ expiresIn: '10m' }
|
|
);
|
|
|
|
// CWE-778: Log temp token issuance for 2FA
|
|
await SecurityAuditLogger.logTokenIssuance(user.id, 'TEMP_2FA', {
|
|
ip,
|
|
userAgent,
|
|
expiresIn: '10m',
|
|
purpose: '2fa'
|
|
});
|
|
|
|
await SecurityAuditLogger.logAuthEvent(user.id, '2fa_required', 'pending', { ip, userAgent });
|
|
|
|
return res.json({
|
|
require2FA: true,
|
|
tempToken,
|
|
userId: user.id,
|
|
passwordWarning: expiryStatus.warning ? expiryStatus.message : null
|
|
});
|
|
}
|
|
|
|
const token = jwt.sign(
|
|
{ userId: user.id, role: user.role },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRES_IN }
|
|
);
|
|
|
|
// CWE-778: Log token issuance
|
|
await SecurityAuditLogger.logTokenIssuance(user.id, 'JWT', {
|
|
ip,
|
|
userAgent,
|
|
expiresIn: JWT_EXPIRES_IN,
|
|
purpose: 'login'
|
|
});
|
|
|
|
// Update last login
|
|
db.run(
|
|
'UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?',
|
|
[new Date().toISOString(), ip, user.id]
|
|
);
|
|
|
|
// Create session
|
|
await createSession(user.id, token, req);
|
|
|
|
// Log successful login
|
|
await SecurityAuditLogger.logLoginSuccess(user.id, { ip, userAgent });
|
|
|
|
// Set secure HTTP-only cookie
|
|
res.cookie('auth_token', token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
});
|
|
|
|
res.json({
|
|
message: 'Login successful',
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
must_change_password: user.must_change_password === 1
|
|
},
|
|
passwordWarning: expiryStatus.warning ? expiryStatus.message : null
|
|
});
|
|
}
|
|
);
|
|
} catch (error) {
|
|
logger.error('Login error:', error);
|
|
await SecurityAuditLogger.logAuthEvent(null, 'login_attempt', 'error', {
|
|
ip,
|
|
userAgent,
|
|
error: error.message
|
|
});
|
|
res.status(500).json({ error: 'Login failed' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Verify 2FA and complete login
|
|
router.post('/verify-2fa', authLimiter,
|
|
[
|
|
body('tempToken').notEmpty(),
|
|
body('code').notEmpty()
|
|
],
|
|
async (req, res) => {
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
|
|
const errors = validationResult(req);
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({ errors: errors.array() });
|
|
}
|
|
|
|
const { tempToken, code } = req.body;
|
|
|
|
try {
|
|
// Verify temp token
|
|
const decoded = jwt.verify(tempToken, JWT_SECRET);
|
|
|
|
if (!decoded.temp || decoded.purpose !== '2fa') {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
|
|
const speakeasy = require('speakeasy');
|
|
|
|
db.get(
|
|
'SELECT * FROM users WHERE id = ?',
|
|
[decoded.userId],
|
|
async (err, user) => {
|
|
if (err || !user) {
|
|
logger.error('2FA verify - user not found:', err);
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
|
|
if (!user.two_factor_enabled) {
|
|
return res.status(400).json({ error: '2FA not enabled for this user' });
|
|
}
|
|
|
|
// Check if it's a backup code
|
|
db.get(
|
|
'SELECT id FROM two_factor_backup_codes WHERE user_id = ? AND code = ? AND used = 0',
|
|
[user.id, code.toUpperCase()],
|
|
async (err, backupCode) => {
|
|
if (backupCode) {
|
|
// Mark backup code as used
|
|
db.run(
|
|
'UPDATE two_factor_backup_codes SET used = 1, used_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[backupCode.id]
|
|
);
|
|
|
|
logger.info(`Backup code used for user ${user.id}`);
|
|
|
|
// Log 2FA success with backup code
|
|
await SecurityAuditLogger.log2FAEvent(user.id, 'backup_code_used', 'success', { ip, userAgent });
|
|
|
|
// Generate full token
|
|
const token = jwt.sign(
|
|
{ userId: user.id, role: user.role },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRES_IN }
|
|
);
|
|
|
|
// CWE-778: Log token issuance after 2FA backup code
|
|
await SecurityAuditLogger.logTokenIssuance(user.id, 'JWT', {
|
|
ip,
|
|
userAgent,
|
|
expiresIn: JWT_EXPIRES_IN,
|
|
purpose: '2fa_backup_verification'
|
|
});
|
|
|
|
// Update last login
|
|
db.run(
|
|
'UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?',
|
|
[new Date().toISOString(), ip, user.id]
|
|
);
|
|
|
|
// Create session
|
|
await createSession(user.id, token, req);
|
|
|
|
// Log successful login
|
|
await SecurityAuditLogger.logLoginSuccess(user.id, { ip, userAgent, method: '2fa_backup' });
|
|
|
|
// Set secure HTTP-only cookie
|
|
res.cookie('auth_token', token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
});
|
|
|
|
return res.json({
|
|
message: 'Login successful with backup code',
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
must_change_password: user.must_change_password === 1
|
|
}
|
|
});
|
|
}
|
|
|
|
// Verify TOTP code
|
|
const verified = speakeasy.totp.verify({
|
|
secret: user.two_factor_secret,
|
|
encoding: 'base32',
|
|
token: code,
|
|
window: 2
|
|
});
|
|
|
|
if (!verified) {
|
|
await SecurityAuditLogger.log2FAEvent(user.id, 'totp_verification', 'failed', {
|
|
ip,
|
|
userAgent,
|
|
reason: 'Invalid code'
|
|
});
|
|
return res.status(400).json({ error: 'Invalid 2FA code' });
|
|
}
|
|
|
|
// Log 2FA success
|
|
await SecurityAuditLogger.log2FAEvent(user.id, 'totp_verification', 'success', { ip, userAgent });
|
|
|
|
// Generate full token
|
|
const token = jwt.sign(
|
|
{ userId: user.id, role: user.role },
|
|
JWT_SECRET,
|
|
{ expiresIn: JWT_EXPIRES_IN }
|
|
);
|
|
|
|
// CWE-778: Log token issuance after TOTP 2FA
|
|
await SecurityAuditLogger.logTokenIssuance(user.id, 'JWT', {
|
|
ip,
|
|
userAgent,
|
|
expiresIn: JWT_EXPIRES_IN,
|
|
purpose: '2fa_totp_verification'
|
|
});
|
|
|
|
// Update last login
|
|
db.run(
|
|
'UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?',
|
|
[new Date().toISOString(), ip, user.id]
|
|
);
|
|
|
|
// Create session
|
|
await createSession(user.id, token, req);
|
|
|
|
// Log successful login
|
|
await SecurityAuditLogger.logLoginSuccess(user.id, { ip, userAgent, method: '2fa_totp' });
|
|
|
|
// Set secure HTTP-only cookie
|
|
res.cookie('auth_token', token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict',
|
|
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
});
|
|
|
|
res.json({
|
|
message: 'Login successful',
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
must_change_password: user.must_change_password === 1
|
|
}
|
|
});
|
|
}
|
|
);
|
|
}
|
|
);
|
|
} catch (error) {
|
|
logger.error('2FA verify error:', error);
|
|
if (error.name === 'TokenExpiredError') {
|
|
return res.status(401).json({ error: '2FA session expired. Please login again' });
|
|
}
|
|
res.status(500).json({ error: 'Failed to verify 2FA' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Change password with enhanced security
|
|
router.post('/change-password',
|
|
[
|
|
body('currentPassword').notEmpty(),
|
|
body('newPassword').notEmpty()
|
|
],
|
|
async (req, res) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'No token provided' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
const { currentPassword, newPassword } = req.body;
|
|
|
|
db.get(
|
|
'SELECT * FROM users WHERE id = ?',
|
|
[decoded.userId],
|
|
async (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
|
|
const isValidPassword = await bcrypt.compare(currentPassword, user.password);
|
|
if (!isValidPassword) {
|
|
await SecurityAuditLogger.logPasswordChange(user.id, 'failed', {
|
|
ip,
|
|
userAgent,
|
|
reason: 'Incorrect current password'
|
|
});
|
|
return res.status(401).json({ error: 'Current password is incorrect' });
|
|
}
|
|
|
|
// Validate new password
|
|
const passwordValidation = validatePassword(newPassword, user.username, user.email);
|
|
if (!passwordValidation.valid) {
|
|
await SecurityAuditLogger.logPasswordChange(user.id, 'failed', {
|
|
ip,
|
|
userAgent,
|
|
reason: passwordValidation.errors.join(', ')
|
|
});
|
|
return res.status(400).json({
|
|
error: 'New password does not meet requirements',
|
|
details: passwordValidation.errors,
|
|
strength: calculatePasswordStrength(newPassword)
|
|
});
|
|
}
|
|
|
|
// Check password history (prevent reuse of last 5 passwords)
|
|
db.all(
|
|
'SELECT password_hash FROM password_history WHERE user_id = ? ORDER BY changed_at DESC LIMIT 5',
|
|
[user.id],
|
|
async (err, history) => {
|
|
if (err) {
|
|
logger.error('Password history check error:', err);
|
|
return res.status(500).json({ error: 'Failed to change password' });
|
|
}
|
|
|
|
// Check if new password matches any recent password
|
|
for (const record of history || []) {
|
|
const matches = await bcrypt.compare(newPassword, record.password_hash);
|
|
if (matches) {
|
|
await SecurityAuditLogger.logPasswordChange(user.id, 'failed', {
|
|
ip,
|
|
userAgent,
|
|
reason: 'Password reused from history'
|
|
});
|
|
return res.status(400).json({
|
|
error: 'Cannot reuse any of your last 5 passwords'
|
|
});
|
|
}
|
|
}
|
|
|
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
|
|
// Save old password to history
|
|
db.run(
|
|
'INSERT INTO password_history (user_id, password_hash) VALUES (?, ?)',
|
|
[user.id, user.password]
|
|
);
|
|
|
|
// Update password
|
|
db.run(
|
|
'UPDATE users SET password = ?, must_change_password = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[hashedPassword, user.id],
|
|
async (err) => {
|
|
if (err) {
|
|
logger.error('Password change error:', err);
|
|
return res.status(500).json({ error: 'Failed to change password' });
|
|
}
|
|
|
|
// Update password expiry
|
|
await updatePasswordExpiry(user.id);
|
|
|
|
// CWE-778: Revoke all tokens on password change (security best practice)
|
|
await SecurityAuditLogger.logTokenRevocation(user.id, 'password_change', {
|
|
ip,
|
|
userAgent
|
|
});
|
|
|
|
// Log successful password change
|
|
await SecurityAuditLogger.logPasswordChange(user.id, 'success', { ip, userAgent });
|
|
|
|
res.json({ message: 'Password changed successfully' });
|
|
}
|
|
);
|
|
}
|
|
);
|
|
}
|
|
);
|
|
} catch (error) {
|
|
logger.error('Password change error:', error);
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
}
|
|
);
|
|
|
|
// Verify token
|
|
router.get('/verify', async (req, res) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'No token provided' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
db.get(
|
|
'SELECT id, username, email, role, must_change_password, is_active FROM users WHERE id = ?',
|
|
[decoded.userId],
|
|
async (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
|
|
if (!user.is_active) {
|
|
return res.status(403).json({ error: 'Account is disabled' });
|
|
}
|
|
|
|
// Check password expiry for warning
|
|
const expiryStatus = await checkPasswordExpiry(user.id);
|
|
|
|
res.json({
|
|
valid: true,
|
|
user: {
|
|
...user,
|
|
must_change_password: user.must_change_password === 1
|
|
},
|
|
passwordWarning: expiryStatus.warning ? expiryStatus.message : null,
|
|
daysUntilExpiry: expiryStatus.daysRemaining || null
|
|
});
|
|
}
|
|
);
|
|
} catch (error) {
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
});
|
|
|
|
// Check password strength
|
|
router.post('/check-password-strength',
|
|
[body('password').notEmpty()],
|
|
(req, res) => {
|
|
const { password, username, email } = req.body;
|
|
|
|
const validation = validatePassword(password, username, email);
|
|
const strength = calculatePasswordStrength(password);
|
|
|
|
res.json({
|
|
valid: validation.valid,
|
|
errors: validation.errors,
|
|
strength: {
|
|
score: strength.score,
|
|
level: strength.level,
|
|
feedback: strength.feedback
|
|
}
|
|
});
|
|
}
|
|
);
|
|
|
|
// Get security status for current user
|
|
router.get('/security-status', async (req, res) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ error: 'No token provided' });
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_SECRET);
|
|
|
|
db.get(
|
|
'SELECT id, username, two_factor_enabled, password_changed_at, password_expires_at, last_login_at, last_login_ip, failed_login_attempts FROM users WHERE id = ?',
|
|
[decoded.userId],
|
|
async (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
|
|
// Get active sessions count
|
|
const sessions = await new Promise((resolve, reject) => {
|
|
db.all(
|
|
'SELECT COUNT(*) as count, MAX(last_activity) as last_activity FROM active_sessions WHERE user_id = ? AND expires_at > ?',
|
|
[user.id, new Date().toISOString()],
|
|
(err, rows) => err ? reject(err) : resolve(rows[0])
|
|
);
|
|
});
|
|
|
|
// Get recent security events
|
|
let recentEvents = [];
|
|
try {
|
|
recentEvents = await SecurityAuditLogger.getUserSecurityEvents(user.id, 10);
|
|
} catch (eventErr) {
|
|
logger.error('Error fetching security events:', eventErr);
|
|
}
|
|
|
|
// Check password expiry
|
|
const expiryStatus = await checkPasswordExpiry(user.id);
|
|
|
|
res.json({
|
|
twoFactorEnabled: user.two_factor_enabled === 1,
|
|
passwordAge: user.password_changed_at ?
|
|
Math.floor((Date.now() - new Date(user.password_changed_at)) / (24 * 60 * 60 * 1000)) : null,
|
|
passwordExpiry: expiryStatus,
|
|
lastLogin: {
|
|
timestamp: user.last_login_at,
|
|
ip: user.last_login_ip
|
|
},
|
|
activeSessions: sessions.count || 0,
|
|
lastActivity: sessions.last_activity,
|
|
failedLoginAttempts: user.failed_login_attempts || 0,
|
|
recentEvents: Array.isArray(recentEvents) ? recentEvents.map(e => ({
|
|
type: e.event_type,
|
|
status: e.status,
|
|
timestamp: e.created_at
|
|
})) : []
|
|
});
|
|
}
|
|
);
|
|
} catch (error) {
|
|
logger.error('Security status error:', error);
|
|
res.status(401).json({ error: 'Invalid token' });
|
|
}
|
|
});
|
|
|
|
// Logout endpoint - invalidate session and clear cookie
|
|
router.post('/logout', authenticate, async (req, res) => {
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
|
const userAgent = req.headers['user-agent'];
|
|
|
|
try {
|
|
// Delete the session from database
|
|
await new Promise((resolve, reject) => {
|
|
db.run(
|
|
'DELETE FROM active_sessions WHERE session_token = ?',
|
|
[token],
|
|
(err) => err ? reject(err) : resolve()
|
|
);
|
|
});
|
|
|
|
// CWE-778: Log token revocation on logout
|
|
await SecurityAuditLogger.logTokenRevocation(req.user.userId, 'user_logout', {
|
|
ip,
|
|
userAgent
|
|
});
|
|
|
|
// Log logout event
|
|
await SecurityAuditLogger.logAuthEvent(req.user.userId, 'logout', 'success', {
|
|
ip,
|
|
userAgent
|
|
});
|
|
|
|
// Clear the HTTP-only cookie
|
|
res.clearCookie('auth_token', {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === 'production',
|
|
sameSite: 'strict'
|
|
});
|
|
|
|
res.json({ message: 'Logout successful' });
|
|
} catch (error) {
|
|
logger.error('Logout error:', error);
|
|
res.status(500).json({ error: 'Logout failed' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|