Files
TaskMate/backend/middleware/auth.js
2026-01-04 00:24:11 +00:00

290 Zeilen
7.3 KiB
JavaScript

/**
* 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
};