Files
hope-events/backend/src/middleware/authMiddleware.js
T
joshuaandClaude Opus 5.5 2b453b00e6 Add read-only admin impersonation
Admins (with 2FA) can view the site as a non-admin user for up to 30
minutes, with a required reason and a "Viewing as" banner. It is
strictly read-only: a guard mounted before every route refuses any
write made with an impersonation token, protect/optionalAuth check the
session is live, and GET handlers skip the price-refresh writes. Start,
end, reason and every page/API call (including blocked attempts) are
audited; optional email to the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CP62L9JxHomA36aYP1ekCe
2026-10-19 18:25:02 +02:00

161 lines
5.4 KiB
JavaScript

const rateLimit = require("express-rate-limit");
const jwt = require('jsonwebtoken');
const prisma = require('../config/db');
const { mustEnrolTwoFactor } = require('../utils/twoFactor');
// The setup wizard's token may finish setup without 2FA for this long.
const SETUP_TOKEN_GRACE_SECONDS = 2 * 60 * 60;
// Protect routes - verify token
const protect = async (req, res, next) => {
let token;
// Check if token exists in headers
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
try {
// Get token from header
token = req.headers.authorization.split(' ')[1];
// Verify token — pin the algorithm so a token signed with an
// unexpected/attacker-chosen algorithm is never accepted.
const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
// Get user from the token (exclude password)
req.user = await prisma.user.findUnique({
where: { id: decoded.id },
select: {
id: true,
name: true,
email: true,
role: true,
isActive: true,
createdAt: true,
updatedAt: true,
phoneNumber: true,
tokenVersion: true,
twoFactorEnabled: true
}
});
if (!req.user) {
res.status(401);
return next(new Error('User not found'));
}
if (!req.user.isActive) {
res.status(401);
return next(new Error('User account is deactivated'));
}
// An admin viewing as this user: only while the session is live, and read-only.
if (decoded.imp) {
const { liveSession } = require('../utils/impersonation');
const session = await liveSession(String(decoded.imp));
if (!session || session.targetUserId !== req.user.id) {
return res.status(401).json({ message: 'The impersonation session has ended.', code: 'IMPERSONATION_ENDED' });
}
req.impersonation = session;
req.readOnly = true;
}
// Revocation check — tokenVersion in JWT must match DB
// Old tokens without tokenVersion are treated as version 0
const tokenVer = decoded.tokenVersion ?? 0;
if (tokenVer !== req.user.tokenVersion) {
res.status(401);
return next(new Error('Session has been revoked. Please log in again.'));
}
// Admins and supervisors must use 2FA. A session that predates the
// requirement (or the user's promotion) is sent back to log in, where they
// enrol. The setup wizard's own token gets a short grace period.
const inSetupGrace = decoded.setup === true && Date.now() / 1000 - (decoded.iat || 0) < SETUP_TOKEN_GRACE_SECONDS;
// (An impersonating admin has already passed 2FA themselves.)
if (!inSetupGrace && !req.impersonation && (await mustEnrolTwoFactor(req.user))) {
return res.status(401).json({
message: 'Two-factor authentication is required for your role. Please log in again to set it up.',
code: 'TWO_FACTOR_REQUIRED',
});
}
next();
} catch (error) {
console.error(error);
res.status(401);
return next(new Error('Not authorized, token failed'));
}
} else {
res.status(401);
return next(new Error('Not authorized, no token'));
}
};
// Admin only middleware
const admin = (req, res, next) => {
if (req.user && req.user.role === 'admin') {
next();
} else {
res.status(403);
return next(new Error('Not authorized as an admin'));
}
};
// Staff or higher middleware
const staff = (req, res, next) => {
if (req.user && (req.user.role === 'admin' || req.user.role === 'supervisor' || req.user.role === 'staff')) {
next();
} else {
res.status(403);
return next(new Error('Not authorized as staff'));
}
};
// Supervisor or higher middleware
const supervisor = (req, res, next) => {
if (req.user && (req.user.role === 'admin' || req.user.role === 'supervisor')) {
next();
} else {
res.status(403);
return next(new Error('Not authorized as a supervisor'));
}
};
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 15,
message: "Too many login attempts. Try again later.",
});
// Optional auth — populates req.user if a valid token is present, but never rejects the request
const optionalAuth = async (req, res, next) => {
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer')) {
return next();
}
try {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
const user = await prisma.user.findUnique({
where: { id: decoded.id },
select: { id: true, name: true, email: true, role: true, isActive: true, createdAt: true, updatedAt: true, phoneNumber: true, tokenVersion: true }
});
if (user && user.isActive && (decoded.tokenVersion ?? 0) === user.tokenVersion) {
if (decoded.imp) {
const { liveSession } = require('../utils/impersonation');
const session = await liveSession(String(decoded.imp));
req.readOnly = true;
if (session && session.targetUserId === user.id) {
req.user = user;
req.impersonation = session;
}
} else {
req.user = user;
}
}
} catch {
// Token invalid or expired — proceed without user
}
next();
};
module.exports = { protect, admin, staff, supervisor, loginLimiter, optionalAuth };