/** * TASKMATE - Auth Middleware * ========================== * JWT-basierte Authentifizierung */ const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const logger = require('../utils/logger'); const { getDb } = require('../database'); const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET || JWT_SECRET.length < 32) { throw new Error('JWT_SECRET muss in .env gesetzt und mindestens 32 Zeichen lang sein!'); } const ACCESS_TOKEN_EXPIRY = 15; // Minuten (kürzer für mehr Sicherheit) const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60; // 7 Tage in Minuten const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten /** * JWT Access-Token generieren (kurze Lebensdauer) */ function generateAccessToken(user) { // Permissions parsen falls als String gespeichert let permissions = user.permissions || []; if (typeof permissions === 'string') { try { permissions = JSON.parse(permissions); } catch (e) { permissions = []; } } return jwt.sign( { id: user.id, username: user.username, displayName: user.display_name, color: user.color, role: user.role || 'user', permissions: permissions, type: 'access' }, JWT_SECRET, { expiresIn: `${ACCESS_TOKEN_EXPIRY}m` } ); } /** * Refresh-Token generieren (lange Lebensdauer) */ function generateRefreshToken(userId, ipAddress, userAgent) { const db = getDb(); const token = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY * 60 * 1000); // Token in Datenbank speichern db.prepare(` INSERT INTO refresh_tokens (user_id, token, expires_at, ip_address, user_agent) VALUES (?, ?, ?, ?, ?) `).run(userId, token, expiresAt.toISOString(), ipAddress, userAgent); return token; } /** * Legacy generateToken für Rückwärtskompatibilität */ function generateToken(user) { return generateAccessToken(user); } /** * JWT-Token verifizieren */ function verifyToken(token) { try { return jwt.verify(token, JWT_SECRET); } catch (error) { // Nur bei unerwarteten Fehlern loggen (nicht bei normalen Ablauf/Ungültig-Fällen) if (error.name !== 'TokenExpiredError' && error.name !== 'JsonWebTokenError') { logger.error(`[AUTH] Unerwarteter Token-Fehler: ${error.name} - ${error.message}`); } return null; } } /** * Express Middleware: Token aus Header, Cookie oder Query-Parameter prüfen */ function authenticateToken(req, res, next) { // Token aus Authorization Header, Cookie oder Query-Parameter (für img src etc.) let token = null; const authHeader = req.headers['authorization']; if (authHeader && authHeader.startsWith('Bearer ')) { token = authHeader.substring(7); } else if (req.cookies && req.cookies.token) { token = req.cookies.token; } else if (req.query && req.query.token) { // Token aus Query-Parameter (für Ressourcen die in img/video tags geladen werden) token = req.query.token; } if (!token) { return res.status(401).json({ error: 'Nicht authentifiziert' }); } const user = verifyToken(token); if (!user) { return res.status(401).json({ error: 'Token ungültig oder abgelaufen' }); } // User-Info an Request anhängen req.user = user; // Token-Refresh: Wenn Token bald ablaeuft, neuen ausstellen const tokenExp = user.exp * 1000; // exp ist in Sekunden const now = Date.now(); const refreshThreshold = 5 * 60 * 1000; // 5 Minuten vor Ablauf if (tokenExp - now < refreshThreshold) { const newToken = generateToken({ id: user.id, username: user.username, display_name: user.displayName, color: user.color, role: user.role, permissions: user.permissions }); res.setHeader('X-New-Token', newToken); } next(); } /** * Middleware: Nur Admins erlauben */ function requireAdmin(req, res, next) { if (!req.user) { return res.status(401).json({ error: 'Nicht authentifiziert' }); } if (req.user.role !== 'admin') { return res.status(403).json({ error: 'Keine Admin-Berechtigung' }); } next(); } /** * Middleware: Nur regulaere User erlauben (blockiert Admins) */ function requireRegularUser(req, res, next) { if (!req.user) { return res.status(401).json({ error: 'Nicht authentifiziert' }); } if (req.user.role === 'admin') { return res.status(403).json({ error: 'Admin hat keinen Zugang zur regulaeren App' }); } next(); } /** * Middleware-Factory: Bestimmte Berechtigung pruefen */ function checkPermission(permission) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Nicht authentifiziert' }); } const permissions = req.user.permissions || []; if (!permissions.includes(permission)) { return res.status(403).json({ error: `Berechtigung "${permission}" fehlt` }); } next(); }; } /** * Socket.io Middleware: Token prüfen */ function authenticateSocket(socket, next) { const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.replace('Bearer ', ''); if (!token) { return next(new Error('Nicht authentifiziert')); } const user = verifyToken(token); if (!user) { return next(new Error('Token ungültig oder abgelaufen')); } // User-Info an Socket anhängen socket.user = user; next(); } /** * CSRF-Token generieren (für Forms) */ function generateCsrfToken() { const { randomBytes } = require('crypto'); return randomBytes(32).toString('hex'); } /** * Refresh-Token validieren und neuen Access-Token generieren */ async function refreshAccessToken(refreshToken, ipAddress, userAgent) { const db = getDb(); // Token in Datenbank suchen const tokenRecord = db.prepare(` SELECT rt.*, u.* FROM refresh_tokens rt JOIN users u ON rt.user_id = u.id WHERE rt.token = ? AND rt.expires_at > datetime('now') `).get(refreshToken); if (!tokenRecord) { throw new Error('Ungültiger oder abgelaufener Refresh-Token'); } // Token als benutzt markieren db.prepare(` UPDATE refresh_tokens SET last_used = CURRENT_TIMESTAMP WHERE id = ? `).run(tokenRecord.id); // Neuen Access-Token generieren const user = { id: tokenRecord.user_id, username: tokenRecord.username, display_name: tokenRecord.display_name, color: tokenRecord.color, role: tokenRecord.role, permissions: tokenRecord.permissions }; return generateAccessToken(user); } /** * Alle Refresh-Tokens eines Benutzers löschen (Logout auf allen Geräten) */ function revokeAllRefreshTokens(userId) { const db = getDb(); db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').run(userId); } /** * Abgelaufene Refresh-Tokens aufräumen */ function cleanupExpiredTokens() { const db = getDb(); const result = db.prepare(` DELETE FROM refresh_tokens WHERE expires_at < datetime('now') `).run(); if (result.changes > 0) { logger.info(`Bereinigt: ${result.changes} abgelaufene Refresh-Tokens`); } } // Cleanup alle 6 Stunden setInterval(cleanupExpiredTokens, 6 * 60 * 60 * 1000); module.exports = { generateToken, generateAccessToken, generateRefreshToken, refreshAccessToken, revokeAllRefreshTokens, verifyToken, authenticateToken, authenticateSocket, generateCsrfToken, requireAdmin, requireRegularUser, checkPermission, JWT_SECRET, SESSION_TIMEOUT };