import unirest from 'unirest'; import { randomBytes, createHmac } from 'crypto'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import sanitizeHtml from 'sanitize-html'; import helmet from 'helmet'; import csurf from 'csurf'; // Environment variable validation const requiredEnvVars = [ 'ADMIN_SECRET_KEY', 'AUTH_ENDPOINT', 'AUTH_PASSWORD', 'AUTO_LOGIN_LINK_PREFIX', 'AUTO_LOGIN_REDIRECT_URL', 'LINK_ID_BYTES', 'LINK_EXPIRY_SECONDS', 'TEMP_LINKS_CLEANUP_INTERVAL_MS' ]; for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.log(`Missing required environment variable: ${envVar}`); process.exit(1); } } // Initialize security middleware const csrfProtection = csurf({ cookie: { secure: true, httpOnly: true, sameSite: 'strict' } }); const rateLimiter = new RateLimiterMemory({ points: 10, duration: 60 // 10 requests per minute }); const temporaryLinks = new Map(); // Secure cleanup interval const cleanupInterval = Math.max(60000, parseInt(process.env.TEMP_LINKS_CLEANUP_INTERVAL_MS, 10)); setInterval(() => { const now = Date.now(); for (const [linkId, linkData] of temporaryLinks.entries()) { if (linkData.expiresAt < now) { temporaryLinks.delete(linkId); console.log(`Cleaned up expired link: ${linkId}`); } } }, cleanupInterval); // Input sanitization and validation const sanitizeInput = (input) => { if (typeof input !== 'string') { console.log(`Invalid input type: expected string, got ${typeof input}`); return null; } // Allow alphanumeric characters and underscores if (!/^[a-zA-Z0-9_]+$/.test(input)) { console.log(`Invalid input format: ${input}`); return null; } return sanitizeHtml(input); }; // Generate secure token const generateSecureToken = (bytes) => { const buffer = randomBytes(Math.max(16, parseInt(bytes, 10))); const hmac = createHmac('sha256', process.env.ADMIN_SECRET_KEY); return hmac.update(buffer).digest('hex'); }; // Check if IP is local (for development) const isLocalIp = (ip) => { return ( ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.') ); }; // Get real client IP from Cloudflare or fallback const getRealIp = (req) => { // Prioritize CF-Connecting-IP for Cloudflare const cfConnectingIp = req.headers['cf-connecting-ip']; if (cfConnectingIp) { return cfConnectingIp; } // Fallback to X-Forwarded-For const forwardedFor = req.headers['x-forwarded-for']; if (forwardedFor) { // Take the first IP in the chain (original client IP) return forwardedFor.split(',')[0].trim(); } // Fallback to req.ip return req.ip; }; // Generate nonce for CSP const generateNonce = () => { return randomBytes(16).toString('base64'); }; export async function generateLoginLink(req, res) { // Apply security headers helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"] } }, xFrameOptions: { action: 'deny' } })(req, res, async () => { try { // Rate limiting with real IP await rateLimiter.consume(getRealIp(req)); // CSRF protection csrfProtection(req, res, async () => { const { secretKey, username } = req.body; // Validate inputs if (!sanitizeInput(secretKey) || secretKey !== process.env.ADMIN_SECRET_KEY) { console.log(`Invalid secret key attempt from IP: ${getRealIp(req)}`); return res.status(401).json({ error: 'Unauthorized' }); } const sanitizedUsername = sanitizeInput(username); if (!sanitizedUsername) { console.log(`Invalid username attempt from IP: ${getRealIp(req)}, username: ${username}`); return res.status(400).json({ error: 'Invalid username' }); } // Secure API request const tokenResponse = await unirest .post(process.env.AUTH_ENDPOINT) .headers({ 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Request-ID': generateSecureToken(16) }) .send({ username: sanitizedUsername, password: process.env.AUTH_PASSWORD }) .timeout(5000); if (!tokenResponse.body.token) { console.log(`Failed to generate API key for username: ${sanitizedUsername}`); return res.status(500).json({ error: 'Authentication service error' }); } const apiKey = tokenResponse.body.token; const linkId = generateSecureToken(process.env.LINK_ID_BYTES); const loginLink = `${process.env.AUTO_LOGIN_LINK_PREFIX}${linkId}`; // Store link data with real IP temporaryLinks.set(linkId, { apiKey, username: sanitizedUsername, expiresAt: Date.now() + Math.min(3600000, parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000), ip: getRealIp(req), userAgent: req.get('User-Agent') || 'Discord-Bot-Request' }); // Secure timeout setTimeout(() => { temporaryLinks.delete(linkId); console.log(`Expired link removed: ${linkId}`); }, Math.min(3600000, parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000)); console.log(`Generated login link for username: ${sanitizedUsername} from IP: ${getRealIp(req)}, userAgent: ${req.get('User-Agent') || 'Discord-Bot-Request'}`); res.json({ loginLink }); }); } catch (error) { console.log(`Error generating login link: ${error.message}`); res.status(500).json({ error: 'Server error' }); } }); } export function handleAutoLogin(req, res) { // Generate nonce for CSP const nonce = generateNonce(); // Apply security headers with nonce helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'", `'nonce-${nonce}'`] } }, xFrameOptions: { action: 'deny' } })(req, res, () => { const { linkId } = req.params; const sanitizedLinkId = sanitizeInput(linkId); const linkData = temporaryLinks.get(sanitizedLinkId); if (!linkData || linkData.expiresAt < Date.now()) { temporaryLinks.delete(sanitizedLinkId); console.log(`Expired or invalid login attempt for link: ${sanitizedLinkId} from IP: ${getRealIp(req)}`); return res.send(`