Files
workflow-app/backend/middleware/auth.js
Kühn 6be1791c62 DEV1.0: Initial commit - Workflow Portal with security fixes
- Backend: Express.js + PostgreSQL/SQLite with LDAP/AD integration
- Frontend: React 18 + Vite + TailwindCSS/DaisyUI
- Security fixes applied (2026-07 + 2026-08):
  - LDAP injection prevention, CSRF protection, HttpOnly cookies
  - Session hashing (SHA-256), account lockout, rate limiting
  - Input validation (zod), file upload security, CSP/HSTS headers
  - V3: express-rate-limit updated (ip-address SSRF fix)
  - V4: postcss updated (nanoid DoS fix)
  - V5: Rate-limit on /health endpoint
  - V6: Session rotation on login (session fixation prevention)
  - V9: Task values array limit (DoS prevention)
  - V10: Frontend XSS audit completed
- Docker: Multi-stage build, non-root user, PostgreSQL + backup service
2026-08-24 09:45:28 +02:00

170 lines
6.8 KiB
JavaScript

/**
* 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';
function setCSRFCookie(res) {
const csrfToken = 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
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];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF-Token ungültig oder fehlend.' });
}
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;
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
};