screentinker/server/middleware/auth.js
ScreenTinker afbe113acf Security audit remediation: auth, IDOR, XSS, hardening
- Device WebSocket authentication: devices get a device_token on
  registration, must present it on reconnect. All WS events require
  prior auth. Timing-safe token comparison.
- IDOR fixes: ownership checks on schedules (device, week), layouts
  (all CRUD, zones, duplicate, device assign), video-walls (content,
  device-config).
- XSS prevention: shared esc() helper in utils.js, fixed 13 innerHTML
  injection points across 9 frontend files.
- OAuth hardening: no longer silently overwrites auth_provider on
  accounts with local passwords (returns 409).
- JWT pinned to HS256 for sign and verify.
- Password policy: change endpoint now requires 8 chars (was 6).
- HSTS header enabled (max-age 1 year, includeSubDomains).
- Stripe webhook rejects unsigned payloads when no secret configured.
- Screenshot size validation (max 2MB base64).
- Rate limiting on exports, imports, content operations.
- Content file serving checks playlist_items instead of old assignments.
- Content ownership verified in device-groups assign-content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 22:48:07 -05:00

68 lines
2.2 KiB
JavaScript

const jwt = require('jsonwebtoken');
const config = require('../config');
const { db } = require('../db/database');
function generateToken(user) {
return jwt.sign(
{ id: user.id, email: user.email, role: user.role },
config.jwtSecret,
{ algorithm: 'HS256', expiresIn: config.jwtExpiry }
);
}
function verifyToken(token) {
return jwt.verify(token, config.jwtSecret, { algorithms: ['HS256'] });
}
// Express middleware - requires valid JWT
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const token = authHeader.split(' ')[1];
const decoded = verifyToken(token);
const user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id);
if (!user) return res.status(401).json({ error: 'User not found' });
req.user = user;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Optional auth - sets req.user if token present, continues either way
function optionalAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
try {
const token = authHeader.split(' ')[1];
const decoded = verifyToken(token);
req.user = db.prepare('SELECT id, email, name, role, auth_provider, avatar_url, plan_id FROM users WHERE id = ?').get(decoded.id);
} catch (err) {
// Token invalid, continue without user
}
}
next();
}
// Require admin role (admin or superadmin)
function requireAdmin(req, res, next) {
if (!req.user || !['admin', 'superadmin'].includes(req.user.role)) {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// Require superadmin role (platform owner only)
function requireSuperAdmin(req, res, next) {
if (!req.user || req.user.role !== 'superadmin') {
return res.status(403).json({ error: 'Platform admin access required' });
}
next();
}
module.exports = { generateToken, verifyToken, requireAuth, optionalAuth, requireAdmin, requireSuperAdmin };