streamflow/backend/routes/twoFactor.js

332 lines
9.3 KiB
JavaScript
Raw Normal View History

const express = require('express');
const router = express.Router();
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const crypto = require('crypto');
const { authenticate } = require('../middleware/auth');
const { modifyLimiter, authLimiter, readLimiter } = require('../middleware/rateLimiter');
const { db } = require('../database/db');
const logger = require('../utils/logger');
const { promisify } = require('util');
const dbRun = promisify(db.run.bind(db));
const dbGet = promisify(db.get.bind(db));
const dbAll = promisify(db.all.bind(db));
// Generate 2FA secret and QR code
router.post('/setup', authenticate, modifyLimiter, async (req, res) => {
try {
const userId = req.user.userId;
// Check if user already has 2FA enabled
const user = await dbGet('SELECT two_factor_enabled FROM users WHERE id = ?', [userId]);
if (user.two_factor_enabled) {
return res.status(400).json({ error: '2FA is already enabled' });
}
// Generate secret with StreamFlow branding
const secret = speakeasy.generateSecret({
name: `StreamFlow IPTV:${req.user.username || 'User'}`,
issuer: 'StreamFlow'
});
// Generate QR code with custom options for better readability
const qrCodeDataURL = await QRCode.toDataURL(secret.otpauth_url, {
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
},
errorCorrectionLevel: 'H'
});
// Store secret temporarily (not enabled yet)
await dbRun(
'UPDATE users SET two_factor_secret = ? WHERE id = ?',
[secret.base32, userId]
);
res.json({
secret: secret.base32,
qrCode: qrCodeDataURL,
manualEntryKey: secret.base32
});
} catch (error) {
logger.error('2FA setup error:', error);
res.status(500).json({ error: 'Failed to setup 2FA' });
}
});
// Verify and enable 2FA
router.post('/enable', authenticate, authLimiter, async (req, res) => {
try {
const { token } = req.body;
const userId = req.user.userId;
if (!token) {
return res.status(400).json({ error: 'Verification token required' });
}
// Get user's secret
const user = await dbGet(
'SELECT two_factor_secret FROM users WHERE id = ?',
[userId]
);
if (!user.two_factor_secret) {
return res.status(400).json({ error: '2FA not set up. Call /setup first' });
}
// Verify token
const verified = speakeasy.totp.verify({
secret: user.two_factor_secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps before/after
});
if (!verified) {
return res.status(400).json({ error: 'Invalid verification code' });
}
// Generate backup codes
const backupCodes = [];
for (let i = 0; i < 10; i++) {
const code = crypto.randomBytes(4).toString('hex').toUpperCase();
backupCodes.push(code);
await dbRun(
'INSERT INTO two_factor_backup_codes (user_id, code) VALUES (?, ?)',
[userId, code]
);
}
// Enable 2FA
await dbRun(
'UPDATE users SET two_factor_enabled = 1 WHERE id = ?',
[userId]
);
logger.info(`2FA enabled for user ${userId}`);
res.json({
success: true,
message: '2FA enabled successfully',
backupCodes: backupCodes
});
} catch (error) {
logger.error('2FA enable error:', error);
res.status(500).json({ error: 'Failed to enable 2FA' });
}
});
// Disable 2FA
router.post('/disable', authenticate, authLimiter, async (req, res) => {
try {
const { password, token } = req.body;
const userId = req.user.userId;
if (!password) {
return res.status(400).json({ error: 'Password required to disable 2FA' });
}
// Verify password
const bcrypt = require('bcrypt');
const user = await dbGet('SELECT password, two_factor_secret FROM users WHERE id = ?', [userId]);
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid password' });
}
// Verify 2FA token
const verified = speakeasy.totp.verify({
secret: user.two_factor_secret,
encoding: 'base32',
token: token,
window: 2
});
if (!verified) {
return res.status(400).json({ error: 'Invalid 2FA code' });
}
// Disable 2FA and remove secret
await dbRun(
'UPDATE users SET two_factor_enabled = 0, two_factor_secret = NULL WHERE id = ?',
[userId]
);
// Delete all backup codes
await dbRun('DELETE FROM two_factor_backup_codes WHERE user_id = ?', [userId]);
logger.info(`2FA disabled for user ${userId}`);
res.json({ success: true, message: '2FA disabled successfully' });
} catch (error) {
logger.error('2FA disable error:', error);
res.status(500).json({ error: 'Failed to disable 2FA' });
}
});
// Verify 2FA token (for login)
router.post('/verify', authLimiter, async (req, res) => {
try {
const { userId, token } = req.body;
if (!userId || !token) {
return res.status(400).json({ error: 'User ID and token required' });
}
// Get user's secret
const user = await dbGet(
'SELECT two_factor_secret, two_factor_enabled FROM users WHERE id = ?',
[userId]
);
if (!user || !user.two_factor_enabled) {
return res.status(400).json({ error: '2FA not enabled for this user' });
}
// Check if it's a backup code
const backupCode = await dbGet(
'SELECT id FROM two_factor_backup_codes WHERE user_id = ? AND code = ? AND used = 0',
[userId, token.toUpperCase()]
);
if (backupCode) {
// Mark backup code as used
await dbRun(
'UPDATE two_factor_backup_codes SET used = 1, used_at = CURRENT_TIMESTAMP WHERE id = ?',
[backupCode.id]
);
logger.info(`Backup code used for user ${userId}`);
return res.json({ valid: true, method: 'backup_code' });
}
// Verify TOTP token
const verified = speakeasy.totp.verify({
secret: user.two_factor_secret,
encoding: 'base32',
token: token,
window: 2
});
if (verified) {
return res.json({ valid: true, method: 'totp' });
} else {
return res.status(400).json({ error: 'Invalid 2FA code' });
}
} catch (error) {
logger.error('2FA verify error:', error);
res.status(500).json({ error: 'Failed to verify 2FA code' });
}
});
// Get backup codes
router.get('/backup-codes', authenticate, readLimiter, async (req, res) => {
try {
const userId = req.user.userId;
const codes = await dbAll(
'SELECT code, used, used_at, created_at FROM two_factor_backup_codes WHERE user_id = ? ORDER BY created_at DESC',
[userId]
);
res.json(codes);
} catch (error) {
logger.error('Get backup codes error:', error);
res.status(500).json({ error: 'Failed to retrieve backup codes' });
}
});
// Regenerate backup codes
router.post('/backup-codes/regenerate', authenticate, modifyLimiter, async (req, res) => {
try {
const { password, token } = req.body;
const userId = req.user.userId;
if (!password || !token) {
return res.status(400).json({ error: 'Password and 2FA token required' });
}
// Verify password
const bcrypt = require('bcrypt');
const user = await dbGet('SELECT password, two_factor_secret FROM users WHERE id = ?', [userId]);
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid password' });
}
// Verify 2FA token
const verified = speakeasy.totp.verify({
secret: user.two_factor_secret,
encoding: 'base32',
token: token,
window: 2
});
if (!verified) {
return res.status(400).json({ error: 'Invalid 2FA code' });
}
// Delete old backup codes
await dbRun('DELETE FROM two_factor_backup_codes WHERE user_id = ?', [userId]);
// Generate new backup codes
const backupCodes = [];
for (let i = 0; i < 10; i++) {
const code = crypto.randomBytes(4).toString('hex').toUpperCase();
backupCodes.push(code);
await dbRun(
'INSERT INTO two_factor_backup_codes (user_id, code) VALUES (?, ?)',
[userId, code]
);
}
logger.info(`Backup codes regenerated for user ${userId}`);
res.json({
success: true,
message: 'Backup codes regenerated',
backupCodes: backupCodes
});
} catch (error) {
logger.error('Backup codes regenerate error:', error);
res.status(500).json({ error: 'Failed to regenerate backup codes' });
}
});
// Check 2FA status
router.get('/status', authenticate, readLimiter, async (req, res) => {
try {
const userId = req.user.userId;
const user = await dbGet(
'SELECT two_factor_enabled FROM users WHERE id = ?',
[userId]
);
const backupCodesCount = await dbGet(
'SELECT COUNT(*) as total, SUM(CASE WHEN used = 0 THEN 1 ELSE 0 END) as unused FROM two_factor_backup_codes WHERE user_id = ?',
[userId]
);
res.json({
enabled: !!user.two_factor_enabled,
backupCodesTotal: backupCodesCount.total || 0,
backupCodesUnused: backupCodesCount.unused || 0
});
} catch (error) {
logger.error('2FA status error:', error);
res.status(500).json({ error: 'Failed to get 2FA status' });
}
});
module.exports = router;