290 Zeilen
7.3 KiB
JavaScript
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
|
|
};
|