/** * Auth routes module. * * Punkt 5: Register returns correct status ('inaktiv'). * Punkt 4: Session tokens are hashed (SHA-256) before storage. * Punkt 6: Uses better-sqlite3 synchronous API. */ const express = require('express'); const bcrypt = require('bcryptjs'); const db = require('../db'); const { auditLog } = require('../auditLog'); const { authMiddleware, adminMiddleware, createSession, deleteSession, invalidateUserSessions, isAccountLocked, recordFailedLogin, recordSuccessfulLogin, setAuthCookie, clearAuthCookie, setCSRFCookie } = require('../middleware/auth'); const { isLDAPConfigured, authenticateLDAP } = require('../ldapSync'); const { loginLimiter } = require('../middleware/rateLimit'); const { validate, registerSchema, loginSchema } = require('../middleware/validation'); const router = express.Router(); // Register router.post('/register', validate(registerSchema), async (req, res) => { const { email, password, name } = req.validatedBody; try { const hash = bcrypt.hashSync(password, 10); // VULN-FIX: Force role to 'user' - never trust client-supplied role on register const info = await db.prepare('INSERT INTO users (email, password, name, role, status, source) VALUES (?, ?, ?, \'user\', \'inaktiv\', \'local\')').run(email, hash, name); const userId = info.lastInsertRowid; auditLog(null, 'register', 'user', userId, `New registration: ${email}`); // P4: Set CSRF cookie for the new session const csrfToken = setCSRFCookie(res); // Return correct status 'inaktiv' (Punkt 5 fix) res.status(201).json({ id: userId, email, name, role: 'user', status: 'inaktiv', source: 'local', csrfToken, message: 'Registrierung erfolgreich. Ein Administrator muss dein Konto freischalten.' }); } catch (err) { if (err.message && err.message.includes('UNIQUE constraint')) { return res.status(409).json({ error: 'E-Mail bereits vergeben.' }); } res.status(500).json({ error: 'Interner Serverfehler.' }); } }); // Login router.post('/login', loginLimiter, validate(loginSchema), async (req, res) => { const { email, password } = req.validatedBody; const row = await db.prepare('SELECT id, email, name, role, status, source, username, password FROM users WHERE LOWER(email) = LOWER(?) OR LOWER(username) = LOWER(?)').get(email, email); // Punkt 12: Account-Lockout check if (row && await isAccountLocked(row.id)) { return res.status(423).json({ error: 'Konto gesperrt wegen zu vieler fehlgeschlagener Anmeldeversuche. Bitte später erneut versuchen.' }); } // If user not found locally, try LDAP auth if (!row) { if (isLDAPConfigured()) { try { const ldapResult = await authenticateLDAP(email, password); const adRow = await db.prepare('SELECT id, email, name, role, status, source, username FROM users WHERE LOWER(username) = LOWER(?)').get(ldapResult.username); if (!adRow) return res.status(404).json({ error: 'Nutzer im System nicht gefunden. Bitte warte auf die naechste Synchronisation.' }); if (adRow.status === 'inaktiv') return res.status(403).json({ error: 'Dein Konto ist deaktiviert.' }); await recordSuccessfulLogin(adRow.id); // V6: Pass old token for session rotation (prevents session fixation) const oldToken = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', ''); const rawToken = await createSession(adRow.id, oldToken); setAuthCookie(res, rawToken); // Punkt 8: HttpOnly-Cookie const csrfToken = setCSRFCookie(res); // P4: CSRF cookie auditLog(adRow.id, 'login', 'user', adRow.id, 'AD login'); // Bug 6: Don't expose token in response body (cookie-only auth) res.json({ ...adRow, csrfToken }); } catch (ldapErr) { return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' }); } } else { return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' }); } return; } if (row.status === 'inaktiv') { return res.status(403).json({ error: 'Dein Konto ist deaktiviert. Bitte wende dich an einen Administrator.' }); } if (row.source === 'ad') { if (!isLDAPConfigured()) { return res.status(403).json({ error: 'AD-Anmeldung nicht konfiguriert.' }); } try { await authenticateLDAP(row.username || row.email.split('@')[0], password); await recordSuccessfulLogin(row.id); // V6: Pass old token for session rotation (prevents session fixation) const oldTokenAD = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', ''); const rawToken = await createSession(row.id, oldTokenAD); setAuthCookie(res, rawToken); // Punkt 8: HttpOnly-Cookie const csrfToken = setCSRFCookie(res); // P4: CSRF cookie auditLog(row.id, 'login', 'user', row.id, 'AD login'); const { password: _, ...safeRow } = row; // Bug 6: Don't expose token in response body (cookie-only auth) res.json({ ...safeRow, csrfToken }); } catch (ldapErr) { await recordFailedLogin(row.id); return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' }); } } else { // Local user - check password with bcrypt (auto-upgrade from plaintext) let passwordMatch = false; if (row.password.startsWith('$2a$') || row.password.startsWith('$2b$')) { passwordMatch = bcrypt.compareSync(password, row.password); } else { // Legacy plaintext comparison - auto-upgrade to bcrypt passwordMatch = row.password === password; if (passwordMatch) { // P8: Log plaintext login for security monitoring (auto-upgrade follows) auditLog(row.id, 'plaintext_login_upgraded', 'user', row.id, 'Legacy plaintext password upgraded to bcrypt'); console.warn('[SECURITY] User', row.email, 'logged in with plaintext password - upgrading to bcrypt.'); const hash = bcrypt.hashSync(password, 12); // Punkt 10: 12 rounds per OWASP await db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hash, row.id); } } if (!passwordMatch) { await recordFailedLogin(row.id); return res.status(401).json({ error: 'Ungueltige Anmeldedaten.' }); } await recordSuccessfulLogin(row.id); // V6: Pass old token for session rotation (prevents session fixation) const oldTokenLocal = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', ''); const rawToken = await createSession(row.id, oldTokenLocal); setAuthCookie(res, rawToken); // Punkt 8: HttpOnly-Cookie const csrfToken = setCSRFCookie(res); // P4: CSRF cookie auditLog(row.id, 'login', 'user', row.id, 'Local login'); const { password: _, ...safeRow } = row; // Bug 6: Don't expose token in response body (cookie-only auth) res.json({ ...safeRow, csrfToken }); } }); // Logout router.post('/logout', async (req, res) => { const rawToken = req.cookies?.workflow_token || req.headers.authorization?.replace('Bearer ', ''); await deleteSession(rawToken); clearAuthCookie(res); // Punkt 8: Clear HttpOnly-Cookie res.json({ message: 'Abgemeldet.' }); }); // Check session router.get('/me', authMiddleware, (req, res) => { res.json(req.user); }); module.exports = router;