Files
hope-events/backend/src/middleware/authMiddleware.js
T
joshuaandClaude Opus 5.5 c3012cb3f6 Require two-factor authentication for admins and supervisors
Admins and supervisors without 2FA are sent a code at their next login,
and verifying it turns 2FA on. They can change the channel but not turn
it off. Older sessions without 2FA are rejected with TWO_FACTOR_REQUIRED,
and promoting a user ends their sessions so they enrol at next login.
The requirement is skipped when no email or WhatsApp channel can deliver
a code, and TWO_FACTOR_REQUIRED_ROLES=false switches it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:00:12 +02:00

139 lines
4.5 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'));
}
// 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;
if (!inSetupGrace && (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) {
req.user = user;
}
} catch {
// Token invalid or expired — proceed without user
}
next();
};
module.exports = { protect, admin, staff, supervisor, loginLimiter, optionalAuth };