/** * LOTUSGUILD TERMINAL DESIGN SYSTEM — Node.js / Express Middleware * * Provides: * - requireAuth Authelia SSO header validation * - requireAdmin Admin group enforcement * - csrfMiddleware CSRF token generation + validation * - cspNonce CSP nonce injection (for use with helmet) * - injectLocals Template locals (user, nonce, config) * * Usage in server.js / app.js: * * const { requireAuth, requireAdmin, cspNonce, injectLocals } = require('./middleware'); * * // Apply globally * app.use(cspNonce); * app.use(requireAuth); * app.use(injectLocals); * * // Apply per-route * app.get('/admin', requireAdmin, (req, res) => res.render('admin')); */ 'use strict'; const crypto = require('crypto'); /* ---------------------------------------------------------------- Auth — Authelia SSO header parsing ---------------------------------------------------------------- */ /** * Parse Authelia remote headers from the request. * @param {import('express').Request} req * @returns {{ username, name, email, groups, isAdmin }} */ function parseUser(req) { const groupsRaw = req.headers['remote-groups'] || ''; const groups = groupsRaw.split(',').map(s => s.trim()).filter(Boolean); return { username: req.headers['remote-user'] || '', name: req.headers['remote-name'] || '', email: req.headers['remote-email'] || '', groups, isAdmin: groups.includes('admin'), }; } /** * Middleware: require authenticated Authelia user. * Allowed groups are read from process.env.ALLOWED_GROUPS (comma-separated), * defaulting to 'admin'. */ function requireAuth(req, res, next) { const user = parseUser(req); if (!user.username) { return res.status(401).json({ error: 'Not authenticated — access via Authelia SSO' }); } const allowed = (process.env.ALLOWED_GROUPS || 'admin').split(',').map(s => s.trim()); if (!user.groups.some(g => allowed.includes(g))) { return res.status(403).json({ error: `Access denied — ${user.username} is not in an allowed group (${allowed.join(', ')})` }); } req.user = user; next(); } /** * Middleware: require 'admin' group specifically. * Must be used after requireAuth (or inline it calls parseUser again — cheap). */ function requireAdmin(req, res, next) { const user = req.user || parseUser(req); if (!user.isAdmin) { return res.status(403).json({ error: 'Admin access required' }); } req.user = user; next(); } /* ---------------------------------------------------------------- CSRF — double-submit cookie pattern ---------------------------------------------------------------- */ /** * Middleware: generate and validate CSRF tokens. * * GET/HEAD/OPTIONS → generate a token, store in session, expose on res.locals. * POST/PUT/PATCH/DELETE → validate X-CSRF-Token header against session token. * * Requires express-session to be configured before this middleware. */ function csrfMiddleware(req, res, next) { const safeMethods = ['GET', 'HEAD', 'OPTIONS']; if (safeMethods.includes(req.method)) { // Generate token if not already set if (!req.session.csrfToken) { req.session.csrfToken = crypto.randomBytes(32).toString('hex'); } res.locals.csrfToken = req.session.csrfToken; return next(); } // Validate on mutating methods const token = req.headers['x-csrf-token'] || req.body?.csrf_token || ''; const expected = req.session.csrfToken || ''; // Constant-time comparison to prevent timing attacks if (!expected || !timingSafeEqual(token, expected)) { return res.status(403).json({ error: 'CSRF token invalid or missing' }); } res.locals.csrfToken = expected; next(); } function timingSafeEqual(a, b) { if (typeof a !== 'string' || typeof b !== 'string') return false; if (a.length !== b.length) return false; try { return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); } catch (_) { return false; } } /* ---------------------------------------------------------------- CSP Nonce — generate per-request nonce for Content-Security-Policy ---------------------------------------------------------------- */ /** * Middleware: generate a CSP nonce and attach to res.locals. * Use with helmet's contentSecurityPolicy: * * app.use(helmet({ * contentSecurityPolicy: { * directives: { * scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`], * } * } * })); * * // In EJS templates: