/** * Auth middleware module (async). * * Session tokens are hashed with SHA-256 for security (Punkt 4). * All DB calls are async (Punkt 4: PostgreSQL compatibility). */ const crypto = require('crypto'); const db = require('../db'); const { auditLog } = require('../auditLog'); // P6: Cookie config - defined early for use in CSRF and auth cookies const isProduction = process.env.NODE_ENV === 'production'; const COOKIE_NAME = 'workflow_token'; function hashToken(token) { return crypto.createHash('sha256').update(token).digest('hex'); } // P4: CSRF protection (Double-Submit-Cookie pattern) const CSRF_COOKIE_NAME = 'workflow_csrf'; const CSRF_HEADER_NAME = 'x-csrf-token'; // CSRF token is derived deterministically from the session token hash (HMAC). // Advantage: cookie and header can never drift apart (no more stale-token 403s), // works across tabs, page reloads and re-logins. The cookie is self-healed by // authMiddleware on every request if it is missing or out of sync. const CSRF_SECRET = process.env.SESSION_SECRET || 'workflow-portal-csrf-v1'; function deriveCSRFToken(tokenHash) { return crypto.createHmac('sha256', CSRF_SECRET).update(tokenHash).digest('hex'); } function setCSRFCookie(res, tokenHash) { // If a session token hash is provided, derive the CSRF token from it (deterministic). // Otherwise (e.g. register, no session yet) fall back to a random token. const csrfToken = tokenHash ? deriveCSRFToken(tokenHash) : crypto.randomBytes(32).toString('hex'); res.cookie(CSRF_COOKIE_NAME, csrfToken, { httpOnly: false, // Must be readable by JS to send back in header secure: isProduction, sameSite: isProduction ? 'strict' : 'lax', maxAge: 24 * 60 * 60 * 1000, // 24h (re-set by authMiddleware on every request) path: '/', }); return csrfToken; } function csrfMiddleware(req, res, next) { // Only check state-changing methods const stateChanging = ['POST', 'PUT', 'PATCH', 'DELETE']; if (!stateChanging.includes(req.method)) return next(); const cookieToken = req.cookies?.[CSRF_COOKIE_NAME]; const headerToken = req.headers[CSRF_HEADER_NAME]; // Accept if header matches cookie (classic double-submit) OR if the header // matches the token derived from the current session (self-healing path). let valid = cookieToken && headerToken && cookieToken === headerToken; if (!valid && req.tokenHash && headerToken) { valid = headerToken === deriveCSRFToken(req.tokenHash); } if (!valid) { return res.status(403).json({ error: 'CSRF-Token ungültig oder fehlend.' }); } // Self-heal: ensure the cookie always carries the correct token if (req.tokenHash && cookieToken !== deriveCSRFToken(req.tokenHash)) { res.cookie(CSRF_COOKIE_NAME, deriveCSRFToken(req.tokenHash), { httpOnly: false, secure: isProduction, sameSite: isProduction ? 'strict' : 'lax', maxAge: 24 * 60 * 60 * 1000, path: '/', }); } next(); } async function authMiddleware(req, res, next) { // Punkt 8: Token from HttpOnly-Cookie OR Authorization header const rawToken = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', ''); if (!rawToken) return res.status(401).json({ error: 'Nicht authentifiziert.' }); const tokenHash = hashToken(rawToken); const session = await db.prepare('SELECT s.id, s.user_id, s.expires_at, u.email, u.name, u.role, u.status, u.source, u.username FROM sessions s JOIN users u ON s.user_id = u.id WHERE s.token = ?').get(tokenHash); if (!session) return res.status(401).json({ error: 'Sitzung abgelaufen. Bitte erneut anmelden.' }); if (session.status === 'inaktiv') { await db.prepare('DELETE FROM sessions WHERE token = ?').run(tokenHash); return res.status(401).json({ error: 'Konto deaktiviert.' }); } if (session.expires_at && new Date(session.expires_at) < new Date()) { await db.prepare('DELETE FROM sessions WHERE token = ?').run(tokenHash); return res.status(401).json({ error: 'Sitzung abgelaufen. Bitte erneut anmelden.' }); } req.user = { id: session.user_id, email: session.email, name: session.name, role: session.role, status: session.status, source: session.source, username: session.username }; req.tokenHash = tokenHash; // Self-healing CSRF: on safe requests (GET/HEAD/OPTIONS), re-issue the CSRF cookie // derived from the current session so it can never go stale or out of sync. if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { res.cookie(CSRF_COOKIE_NAME, deriveCSRFToken(tokenHash), { httpOnly: false, secure: isProduction, sameSite: isProduction ? 'strict' : 'lax', maxAge: 24 * 60 * 60 * 1000, path: '/', }); } next(); } function adminMiddleware(req, res, next) { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin-Rechte erforderlich.' }); next(); } async function createSession(userId, oldRawToken) { // V6: Session-Rotation - invalidate old session on new login (prevents session fixation) if (oldRawToken) { const oldHash = hashToken(oldRawToken); await db.prepare('DELETE FROM sessions WHERE token = ?').run(oldHash); } const rawToken = crypto.randomBytes(32).toString('hex'); const tokenHash = hashToken(rawToken); const ttlHours = parseInt(process.env.SESSION_TTL_HOURS) || 168; const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000).toISOString(); // Punkt 9: Session-Limitierung - max sessions per user const maxSessions = parseInt(process.env.SESSION_MAX_PER_USER) || 5; const existingSessions = await db.prepare('SELECT id FROM sessions WHERE user_id = ? ORDER BY created_at ASC').all(userId); if (existingSessions.length >= maxSessions) { const toDelete = existingSessions.slice(0, existingSessions.length - maxSessions + 1); const deleteIds = toDelete.map(s => s.id).filter(id => Number.isInteger(id)); if (deleteIds.length > 0) { const placeholders = deleteIds.map(() => '?').join(','); await db.prepare(`DELETE FROM sessions WHERE id IN (${placeholders})`).run(...deleteIds); } } await db.prepare('INSERT INTO sessions (user_id, token, expires_at) VALUES (?, ?, ?)').run(userId, tokenHash, expiresAt); return rawToken; } async function deleteSession(rawToken) { if (!rawToken) return; const tokenHash = hashToken(rawToken); const session = await db.prepare('SELECT user_id FROM sessions WHERE token = ?').get(tokenHash); if (session) { auditLog(session.user_id, 'logout', 'user', session.user_id, null); } await db.prepare('DELETE FROM sessions WHERE token = ?').run(tokenHash); } async function invalidateUserSessions(userId) { await db.prepare('DELETE FROM sessions WHERE user_id = ?').run(userId); } // Punkt 12: Account-Lockout functions const MAX_ATTEMPTS = parseInt(process.env.LOGIN_MAX_ATTEMPTS) || 5; const LOCKOUT_MINUTES = parseInt(process.env.LOGIN_LOCKOUT_MINUTES) || 15; async function isAccountLocked(userId) { const user = await db.prepare('SELECT locked_until FROM users WHERE id = ?').get(userId); if (!user || !user.locked_until) return false; if (new Date(user.locked_until) > new Date()) return true; await db.prepare('UPDATE users SET failed_login_attempts = 0, locked_until = NULL WHERE id = ?').run(userId); return false; } async function recordFailedLogin(userId) { if (!userId) return; const user = await db.prepare('SELECT failed_login_attempts FROM users WHERE id = ?').get(userId); if (!user) return; const attempts = (user.failed_login_attempts || 0) + 1; if (attempts >= MAX_ATTEMPTS) { const lockedUntil = new Date(Date.now() + LOCKOUT_MINUTES * 60 * 1000).toISOString(); await db.prepare('UPDATE users SET failed_login_attempts = ?, locked_until = ? WHERE id = ?').run(attempts, lockedUntil, userId); } else { await db.prepare('UPDATE users SET failed_login_attempts = ? WHERE id = ?').run(attempts, userId); } } async function recordSuccessfulLogin(userId) { if (!userId) return; await db.prepare('UPDATE users SET failed_login_attempts = 0, locked_until = NULL WHERE id = ?').run(userId); } // Punkt 8: Cookie helpers function setAuthCookie(res, token) { const ttlHours = parseInt(process.env.SESSION_TTL_HOURS) || 168; res.cookie(COOKIE_NAME, token, { httpOnly: true, secure: isProduction, sameSite: isProduction ? 'strict' : 'lax', maxAge: ttlHours * 60 * 60 * 1000, path: '/', }); } function clearAuthCookie(res) { res.clearCookie(COOKIE_NAME, { path: '/' }); } module.exports = { authMiddleware, adminMiddleware, createSession, deleteSession, invalidateUserSessions, hashToken, isAccountLocked, recordFailedLogin, recordSuccessfulLogin, setAuthCookie, clearAuthCookie, COOKIE_NAME, setCSRFCookie, csrfMiddleware, CSRF_COOKIE_NAME, CSRF_HEADER_NAME };