// Authentication middleware import { authConfig } from '../config/auth.js'; /** * Require authentication for a route * Sets req.user if authenticated, returns 401 if not */ export function requireAuth(req, res, next) { console.log(`[requireAuth] ${req.method} ${req.path} - session ID: ${req.session?.id || 'NONE'}, has user: ${!!req.session?.user}`); // Skip auth if disabled if (!authConfig.app.authEnabled) { req.user = { id: 'anonymous', email: 'anonymous@local', groups: ['agent-admin'] }; return next(); } // Check session if (!req.session || !req.session.user) { console.log(`[requireAuth] Unauthorized - no session or user`); return res.status(401).json({ error: 'Unauthorized', message: 'Authentication required', loginUrl: '/auth/login', }); } // Attach user to request req.user = req.session.user; next(); } /** * Require specific group membership * Must be used after requireAuth */ export function requireGroup(allowedGroups) { return (req, res, next) => { // Skip if auth disabled if (!authConfig.app.authEnabled) { return next(); } if (!req.user) { return res.status(401).json({ error: 'Unauthorized', message: 'Authentication required', }); } const userGroups = req.user.groups || []; const hasGroup = allowedGroups.some(g => userGroups.includes(g)); if (!hasGroup) { return res.status(403).json({ error: 'Forbidden', message: `Required group: ${allowedGroups.join(' or ')}`, userGroups, }); } next(); }; } /** * Optional authentication - attach user if present but don't require it */ export function optionalAuth(req, res, next) { if (!authConfig.app.authEnabled) { req.user = null; return next(); } if (req.session && req.session.user) { req.user = req.session.user; } else { req.user = null; } next(); } /** * Check if user is admin */ export function isAdmin(user) { if (!user || !user.groups) return false; return user.groups.includes(authConfig.groups.admin); } /** * Authenticate WebSocket upgrade request * Returns user object or null */ export async function authenticateWebSocket(req, sessionStore) { // Skip auth if disabled if (!authConfig.app.authEnabled) { return { id: 'anonymous', email: 'anonymous@local', groups: ['agent-admin'] }; } // Parse session ID from cookies const cookies = parseCookies(req.headers.cookie || ''); const sessionCookie = cookies[authConfig.session.name]; if (!sessionCookie) { console.log('[WS Auth] No session cookie found'); return null; } // Session cookie format: s:sessionId.signature // We need to extract the session ID const sessionId = decodeSessionCookie(sessionCookie, authConfig.session.secret); if (!sessionId) { console.log('[WS Auth] Invalid session cookie'); return null; } // Load session from store return new Promise((resolve) => { sessionStore.get(sessionId, (err, session) => { if (err || !session || !session.user) { console.log('[WS Auth] Session not found or invalid'); resolve(null); return; } console.log(`[WS Auth] Authenticated user: ${session.user.email}`); resolve(session.user); }); }); } /** * Parse cookies from header string */ function parseCookies(cookieHeader) { const cookies = {}; if (!cookieHeader) return cookies; cookieHeader.split(';').forEach(cookie => { const [name, ...rest] = cookie.trim().split('='); cookies[name] = rest.join('='); }); return cookies; } /** * Decode express-session cookie * Cookie format: s:sessionId.signature (URL encoded) */ function decodeSessionCookie(cookie, secret) { try { // URL decode const decoded = decodeURIComponent(cookie); // Check for signed cookie prefix if (!decoded.startsWith('s:')) { return null; } // Extract session ID (before the signature) const withoutPrefix = decoded.substring(2); const dotIndex = withoutPrefix.lastIndexOf('.'); if (dotIndex === -1) { return null; } return withoutPrefix.substring(0, dotIndex); } catch (e) { return null; } } export default { requireAuth, requireGroup, optionalAuth, isAdmin, authenticateWebSocket, };