Skip to content

Commit ab9caf6

Browse files
authored
Merge pull request #2090 from rteas/refactor-permission-management
Refactor permission management
2 parents c59853c + 7c70d00 commit ab9caf6

30 files changed

+889
-580
lines changed

backend/app.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
// app.js - Entry point for our application
33

44
// Load in all of our node modules. Their uses are explained below as they are called.
@@ -63,6 +63,10 @@ const { createRecurringEvents } = require('./workers/createRecurringEvents');
6363
createRecurringEvents(cron, fetch);
6464
// const runSlackBot = require("./workers/slackbot")(fetch);
6565

66+
// Run cleanup expired refresh token(s) on startup
67+
const { cleanupExpiredTokens } = require('./workers/tokenCleanup');
68+
cleanupExpiredTokens();
69+
6670
// MIDDLEWARE
6771
const errorhandler = require('./middleware/errorhandler.middleware');
6872

backend/config/auth.config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
/*eslint-disable */
21
module.exports = {
3-
SECRET:
4-
'c0d7d0716e4cecffe9dcc77ff90476d98f5aace08ea40f5516bd982b06401021191f0f24cd6759f7d8ca41b64f68d0b3ad19417453bddfd1dbe8fcb197245079',
2+
TOKEN_SECRET: process.env.TOKEN_SECRET || 'placeholder_secret_key_for_development_only',
53
CUSTOM_REQUEST_HEADER: process.env.CUSTOM_REQUEST_HEADER,
6-
TOKEN_EXPIRATION_SEC: 900,
4+
// 15 minutes as a string for JWT expiration
5+
ACCESS_TOKEN_EXPIRATION: '15m',
6+
// 30 days in milliseconds for refresh token expiration
7+
REFRESH_TOKEN_EXPIRATION_MS: 30 * 24 * 60 * 60 * 1000,
78
};
8-
/* eslint-enable */

backend/controllers/user.controller.js

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ const { ObjectId } = require('mongodb');
44
const EmailController = require('./email.controller');
55
const { CONFIG_AUTH } = require('../config');
66

7-
const { User, Project } = require('../models');
7+
const { User, Project, RefreshToken } = require('../models');
8+
const {
9+
generateRefreshToken,
10+
getClientIp,
11+
hashToken,
12+
generateAccessToken,
13+
} = require('../middleware/auth.middleware');
814

915
const expectedHeader = process.env.CUSTOM_REQUEST_HEADER;
1016

@@ -193,18 +199,7 @@ UserController.delete = async function (req, res) {
193199
}
194200
};
195201

196-
function generateAccessToken(user, auth_origin) {
197-
// expires after half and hour (1800 seconds = 30 minutes)
198-
return jwt.sign(
199-
{ id: user.id, role: user.accessLevel, auth_origin: auth_origin },
200-
CONFIG_AUTH.SECRET,
201-
{
202-
expiresIn: `${CONFIG_AUTH.TOKEN_EXPIRATION_SEC}s`,
203-
},
204-
);
205-
}
206-
207-
UserController.createUser = async function (req, res) {
202+
UserController.createUser = function (req, res) {
208203
const { firstName, lastName, email } = req.body;
209204
const { origin } = req.headers;
210205

@@ -266,21 +261,57 @@ UserController.verifySignIn = async function (req, res) {
266261
try {
267262
const payload = jwt.verify(token, CONFIG_AUTH.SECRET);
268263
const user = await User.findById(payload.id);
269-
res.cookie('token', token, { httpOnly: true });
270-
return res.send(user);
264+
const refreshToken = generateRefreshToken();
265+
const accessToken = generateAccessToken(user, payload.auth_origin);
266+
const ipAddress = getClientIp(req);
267+
268+
await RefreshToken.create({
269+
userId: user._id,
270+
hash: hashToken(refreshToken),
271+
deviceInfo: {
272+
deviceType: req.headers['user-agent'],
273+
ipAddress: ipAddress,
274+
},
275+
});
276+
277+
res.cookie('token', accessToken, { httpOnly: true });
278+
res.cookie('refresh_token', refreshToken, { httpOnly: true });
279+
280+
return res.send({
281+
user: user,
282+
expiresAt: accessToken.exp * 1000, // Convert JWT exp (seconds) to milliseconds
283+
});
271284
} catch (err) {
272285
console.error(err);
273286
return res.status(403);
274287
}
275288
};
276289

277290
UserController.verifyMe = async function (req, res) {
278-
const user = await User.findById(req.userId);
279-
return res.status(200).send(user);
291+
return res.status(200).send(req.user);
280292
};
281293

282294
UserController.logout = async function (req, res) {
283-
return res.clearCookie('token').status(200).send('Successfully logged out.');
295+
try {
296+
await RefreshToken.deleteOne({ _id: req.refreshToken._id });
297+
return res.clearCookie('token').status(200).send('Successfully logged out.');
298+
} catch (err) {
299+
console.error(err);
300+
return res.status(500).send('Error occurred while logging out.');
301+
}
302+
};
303+
304+
UserController.refreshAccessToken = async function (req, res) {
305+
const accessToken = generateAccessToken(req.user, req.auth_origin);
306+
const decoded = jwt.decode(accessToken);
307+
308+
return res
309+
.cookie('token', accessToken, { httpOnly: true })
310+
.status(200)
311+
.json({
312+
user: req.user,
313+
expiresAt: decoded.exp * 1000, // Convert JWT exp (seconds) to milliseconds
314+
});
284315
};
285316

286317
// Update user's managedProjects

backend/middleware/auth.middleware.js

Lines changed: 154 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,158 @@
11
const jwt = require('jsonwebtoken');
22
const { CONFIG_AUTH } = require('../config');
33

4-
function verifyToken(req, res, next) {
5-
// Allow users to set token
6-
// eslint-disable-next-line dot-notation
7-
let token = req.headers['x-access-token'] || req.headers['authorization'];
8-
if (token.startsWith('Bearer ')) {
9-
// Remove Bearer from string
10-
token = token.slice(7, token.length);
4+
const { RefreshToken, User } = require('../models');
5+
const crypto = require('crypto');
6+
const AuthUtils = require('../../shared/authorizationUtils');
7+
8+
const SECRET = CONFIG_AUTH.TOKEN_SECRET;
9+
10+
// Utility functions
11+
12+
function generateAccessToken(user, auth_origin) {
13+
return jwt.sign(
14+
{
15+
id: user._id,
16+
email: user.email,
17+
role: user.accessLevel,
18+
accessLevel: user.accessLevel,
19+
auth_origin: auth_origin,
20+
},
21+
SECRET,
22+
{ expiresIn: CONFIG_AUTH.ACCESS_TOKEN_EXPIRATION },
23+
);
24+
}
25+
26+
function generateRefreshToken() {
27+
return crypto.randomBytes(32).toString('hex');
28+
}
29+
30+
function hashToken(token) {
31+
return crypto.createHash('sha256').update(token).digest('hex');
32+
}
33+
34+
function getClientIp(req) {
35+
// Check X-Forwarded-For header (most common)
36+
const forwarded = req.headers['x-forwarded-for'];
37+
if (forwarded) {
38+
// Takes the first IP if there are multiple
39+
return forwarded.split(',')[0].trim();
1140
}
12-
if (!token) {
13-
return res.sendStatus(403);
41+
42+
// Check other common headers
43+
return (
44+
req.headers['x-real-ip'] || req.connection.remoteAddress || req.socket.remoteAddress || req.ip
45+
);
46+
}
47+
48+
async function authenticateAccessToken(req, res, next) {
49+
try {
50+
// Extract token from Authorization header
51+
let accessToken =
52+
req.cookies.token || req.headers['x-access-token'] || req.headers['authorization'];
53+
54+
if (!accessToken) {
55+
return res.status(401).json({ error: 'Access token required' });
56+
}
57+
58+
if (accessToken.startsWith('Bearer ')) {
59+
accessToken = accessToken.slice(7, accessToken.length);
60+
}
61+
62+
const decoded = jwt.verify(accessToken, SECRET);
63+
// Attach user info to request
64+
req.user = decoded;
65+
66+
next();
67+
} catch (error) {
68+
if (error.name === 'TokenExpiredError') {
69+
return res.status(401).json({ error: 'Token expired' });
70+
}
71+
72+
if (error.name === 'JsonWebTokenError') {
73+
return res.status(401).json({ error: 'Invalid token' });
74+
}
75+
76+
return res.status(401).json({ error: 'Authentication failed' });
1477
}
78+
}
79+
80+
// shorthand for authenticateAccessToken
81+
const authUser = authenticateAccessToken;
1582

83+
async function authenticateRefreshToken(req, res, next) {
1684
try {
17-
const decoded = jwt.verify(token, CONFIG_AUTH.SECRET);
18-
res.cookie('token', token, { httpOnly: true });
19-
req.userId = decoded.id;
20-
return next();
21-
} catch (err) {
22-
return res.sendStatus(401);
85+
const refreshToken = req.cookies?.refresh_token;
86+
87+
if (!refreshToken) {
88+
return res.status(401).json({ error: 'Refresh token required' });
89+
}
90+
91+
const tokenHash = hashToken(refreshToken);
92+
93+
const tokenDoc = await RefreshToken.findOne({
94+
hash: tokenHash,
95+
expiresAt: { $gt: new Date() },
96+
});
97+
98+
if (!tokenDoc) {
99+
return res.status(401).json({ error: 'Invalid or expired refresh token' });
100+
}
101+
102+
const user = await User.findById(tokenDoc.userId);
103+
if (!user) {
104+
return res.status(401).json({ error: 'User not found for this token' });
105+
}
106+
107+
// Attach user & refresh token to request for downstream handlers
108+
req.user = user;
109+
req.refreshToken = tokenDoc;
110+
111+
next();
112+
} catch (error) {
113+
console.error('Refresh token validation error:', error);
114+
return res.status(401).json({ error: 'Authentication failed' });
23115
}
24116
}
25117

118+
function requireRole(...roles) {
119+
return (req, res, next) => {
120+
if (!req.user) {
121+
return res.status(401).json({ error: 'Authentication required' });
122+
}
123+
124+
if (!AuthUtils.hasAnyRole(req.user, roles)) {
125+
return res.status(403).json({
126+
error: 'Insufficient permissions',
127+
required_role: roles,
128+
your_role: req.user.accessLevel,
129+
});
130+
}
131+
132+
next();
133+
};
134+
}
135+
136+
function requireMinimumRole(role) {
137+
return (req, res, next) => {
138+
if (!req.user) {
139+
return res.status(401).json({ error: 'Authentication required' });
140+
}
141+
142+
const user = req.user;
143+
if (!AuthUtils.hasMinimumRole(user, role)) {
144+
return res.status(403).json({
145+
error: 'Insufficient permissions',
146+
required_minimum_role: role,
147+
your_role: req.user.accessLevel,
148+
});
149+
}
150+
next();
151+
};
152+
}
153+
26154
function verifyCookie(req, res, next) {
27-
jwt.verify(req.cookies.token, CONFIG_AUTH.SECRET, (err, decoded) => {
155+
jwt.verify(req.cookies.token, SECRET, (err, decoded) => {
28156
if (err) {
29157
return res.sendStatus(401);
30158
}
@@ -35,8 +163,15 @@ function verifyCookie(req, res, next) {
35163
});
36164
}
37165

38-
const AuthUtil = {
39-
verifyToken,
166+
module.exports = {
167+
authenticateAccessToken,
168+
authUser,
169+
authenticateRefreshToken,
170+
requireRole,
171+
requireMinimumRole,
172+
generateAccessToken,
173+
generateRefreshToken,
174+
getClientIp,
175+
hashToken,
40176
verifyCookie,
41177
};
42-
module.exports = AuthUtil;

backend/middleware/index.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
const AuthUtil = require('./auth.middleware');
1+
const Auth = require('./auth.middleware');
22
const verifyUser = require('./user.middleware');
3-
const verifyToken = require('./token.middleware');
43

54
module.exports = {
6-
AuthUtil,
5+
Auth,
76
verifyUser,
8-
verifyToken,
97
};

backend/middleware/token.middleware.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

backend/models/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ const { Question } = require('./question.model');
66
const { RecurringEvent } = require('./recurringEvent.model');
77
const { Role } = require('./role.model');
88
const { User } = require('./user.model');
9+
const { RefreshToken } = require('./refreshToken.model');
910

10-
const mongoose = require("mongoose");
11+
const mongoose = require('mongoose');
1112
mongoose.Promise = global.Promise;
1213

1314
module.exports = {
@@ -19,4 +20,5 @@ module.exports = {
1920
RecurringEvent,
2021
Role,
2122
User,
23+
RefreshToken,
2224
};

0 commit comments

Comments
 (0)