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'; import winston from 'winston'; import validator from 'validator'; // Initialize logger const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'security.log' }) ] }); // 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]) { logger.error(`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); logger.info(`Cleaned up expired link: ${linkId}`); } } }, cleanupInterval); // Input sanitization and validation const sanitizeInput = (input) => { if (typeof input !== 'string') { logger.warn(`Invalid input type: expected string, got ${typeof input}`); return null; } // Allow alphanumeric characters and underscores if (!/^[a-zA-Z0-9_]+$/.test(input)) { logger.warn(`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.') ); }; // 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 await rateLimiter.consume(req.ip); // CSRF protection csrfProtection(req, res, async () => { const { secretKey, username } = req.body; // Validate inputs if (!sanitizeInput(secretKey) || secretKey !== process.env.ADMIN_SECRET_KEY) { logger.warn(`Invalid secret key attempt from IP: ${req.ip}`); return res.status(401).json({ error: 'Unauthorized' }); } const sanitizedUsername = sanitizeInput(username); if (!sanitizedUsername) { logger.warn(`Invalid username attempt from IP: ${req.ip}, 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) { logger.error(`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 additional security metadata temporaryLinks.set(linkId, { apiKey, username: sanitizedUsername, expiresAt: Date.now() + Math.min(3600000, parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000), ip: req.ip, userAgent: req.get('User-Agent') || 'Unknown' }); // Secure timeout setTimeout(() => { temporaryLinks.delete(linkId); logger.info(`Expired link removed: ${linkId}`); }, Math.min(3600000, parseInt(process.env.LINK_EXPIRY_SECONDS, 10) * 1000)); logger.info(`Generated login link for username: ${sanitizedUsername} from IP: ${req.ip}, userAgent: ${req.get('User-Agent') || 'Unknown'}`); res.json({ loginLink }); }); } catch (error) { logger.error(`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); logger.warn(`Expired or invalid login attempt for link: ${sanitizedLinkId} from IP: ${req.ip}`); return res.send(`

Login Expired

Redirecting...

`); } // Verify client consistency const isIpMatch = linkData.ip === req.ip; const isUserAgentMatch = linkData.userAgent === (req.get('User-Agent') || 'Unknown'); const isLocal = isLocalIp(req.ip) && isLocalIp(linkData.ip); const strictUserAgentCheck = process.env.STRICT_USER_AGENT_CHECK === 'true'; if (strictUserAgentCheck && !isUserAgentMatch && !isLocal) { temporaryLinks.delete(sanitizedLinkId); logger.warn( `Suspicious login attempt for link: ${sanitizedLinkId} from IP: ${req.ip}, ` + `expected IP: ${linkData.ip}, isLocal: ${isLocal}, ` + `userAgentMatch: ${isUserAgentMatch}, ` + `expectedUserAgent: ${linkData.userAgent}, ` + `actualUserAgent: ${req.get('User-Agent') || 'Unknown'}` ); return res.status(403).json({ error: 'Invalid session' }); } if (!isUserAgentMatch) { logger.info( `Non-critical user-agent mismatch for link: ${sanitizedLinkId} from IP: ${req.ip}, ` + `expectedUserAgent: ${linkData.userAgent}, ` + `actualUserAgent: ${req.get('User-Agent') || 'Unknown'}` ); } temporaryLinks.delete(sanitizedLinkId); logger.info(`Successful auto-login for username: ${linkData.username} from IP: ${req.ip}, userAgent: ${req.get('User-Agent') || 'Unknown'}`); // Secure API key storage with additional client-side security and debugging res.send(` Secure Auto Login

Securely logging in...

`); }); }