Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
776
backend/routes/auth.js
Normal file
776
backend/routes/auth.js
Normal file
|
|
@ -0,0 +1,776 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue