@dax-crafta/auth 1.3.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +8 -5
- package/src/index.js +217 -160
- package/src/middlewares/auth.middleware.js +84 -24
- package/src/middlewares/validation.middleware.js +77 -0
- package/src/models/audit-log.js +14 -8
- package/src/models/role.js +30 -28
- package/src/models/user.js +168 -32
- package/src/services/auth.service.js +320 -0
- package/src/utils/email.js +98 -22
- package/src/utils/mfa.js +17 -3
- package/src/utils/password-policy.js +21 -17
- package/src/services/auth.services.js +0 -161
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A powerful, flexible, and secure authentication system for Node.js applications. Built with enterprise-grade security features while maintaining developer-friendly simplicity.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@dax-crafta/auth)
|
|
6
|
-
[](https://github.com/daxp472/crafta/
|
|
6
|
+
[](https://github.com/daxp472/crafta/blob/main/LICENSE)
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dax-crafta/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A powerful, flexible, and secure authentication plugin for the Crafta framework. Supports JWT, social login, 2FA, RBAC, audit logging, and enterprise-grade security features.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"private": false,
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"bcryptjs": "^2.4.3",
|
|
9
|
+
"cookie-parser": "^1.4.7",
|
|
10
|
+
"csurf": "^1.11.0",
|
|
11
|
+
"express-brute": "^1.0.1",
|
|
9
12
|
"express-rate-limit": "^7.1.4",
|
|
10
13
|
"google-auth-library": "^9.2.0",
|
|
14
|
+
"helmet": "^8.1.0",
|
|
11
15
|
"jsonwebtoken": "^9.0.2",
|
|
12
16
|
"mongoose": "^7.6.3",
|
|
13
17
|
"nodemailer": "^6.9.7",
|
|
@@ -16,12 +20,11 @@
|
|
|
16
20
|
"passport-facebook": "^3.0.0",
|
|
17
21
|
"passport-github2": "^0.1.12",
|
|
18
22
|
"passport-google-oauth20": "^2.0.0",
|
|
23
|
+
"qrcode": "^1.5.3",
|
|
24
|
+
"speakeasy": "^2.0.0",
|
|
19
25
|
"validator": "^13.11.0",
|
|
20
|
-
"zxcvbn": "^4.4.2",
|
|
21
26
|
"winston": "^3.11.0",
|
|
22
|
-
"
|
|
23
|
-
"qrcode": "^1.5.3",
|
|
24
|
-
"speakeasy": "^2.0.0"
|
|
27
|
+
"zxcvbn": "^4.4.2"
|
|
25
28
|
},
|
|
26
29
|
"peerDependencies": {
|
|
27
30
|
"crafta": "^1.1.4"
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
const express = require('express');
|
|
1
2
|
const passport = require('passport');
|
|
3
|
+
const cookieParser = require('cookie-parser');
|
|
4
|
+
const csrf = require('csurf');
|
|
5
|
+
const helmet = require("helmet");
|
|
6
|
+
|
|
2
7
|
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
|
3
|
-
const FacebookStrategy = require('passport-facebook').Strategy;
|
|
4
|
-
const GitHubStrategy = require('passport-github2').Strategy;
|
|
5
8
|
const AuthService = require('./services/auth.service');
|
|
6
9
|
const RoleService = require('./services/role.service');
|
|
7
10
|
const AuditService = require('./services/audit.service');
|
|
8
11
|
const MFAService = require('./utils/mfa');
|
|
9
12
|
const PasswordPolicy = require('./utils/password-policy');
|
|
10
13
|
const createAuthMiddleware = require('./middlewares/auth.middleware');
|
|
14
|
+
const validators = require('./middlewares/validation.middleware');
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
const defaultConfig = {
|
|
13
18
|
strategy: 'jwt',
|
|
@@ -40,12 +45,26 @@ const defaultConfig = {
|
|
|
40
45
|
google: null,
|
|
41
46
|
facebook: null,
|
|
42
47
|
github: null
|
|
43
|
-
}
|
|
48
|
+
},
|
|
49
|
+
env: {
|
|
50
|
+
JWT_SECRET: process.env.JWT_SECRET
|
|
51
|
+
},
|
|
52
|
+
accessTokenExpiry: '1h',
|
|
53
|
+
refreshTokenDays: 7
|
|
44
54
|
};
|
|
45
55
|
|
|
56
|
+
// minimal ApiError re-export if needed
|
|
57
|
+
class ApiError extends Error {
|
|
58
|
+
constructor(message, status = 400) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.status = status;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function auth(config = {}) {
|
|
47
65
|
const finalConfig = { ...defaultConfig, ...config };
|
|
48
|
-
|
|
66
|
+
finalConfig.env = { ...(defaultConfig.env || {}), ...(config.env || process.env) };
|
|
67
|
+
|
|
49
68
|
if (!finalConfig.env?.JWT_SECRET) {
|
|
50
69
|
throw new Error('JWT_SECRET is required in environment configuration');
|
|
51
70
|
}
|
|
@@ -54,214 +73,252 @@ function auth(config = {}) {
|
|
|
54
73
|
throw new Error('SMTP configuration is required for email verification');
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
// Initialize services
|
|
58
76
|
const authService = new AuthService(finalConfig);
|
|
59
77
|
const roleService = new RoleService();
|
|
60
78
|
const auditService = new AuditService();
|
|
61
79
|
const mfaService = new MFAService();
|
|
62
80
|
const passwordPolicy = new PasswordPolicy(finalConfig.passwordPolicy);
|
|
63
|
-
const { rateLimiter, verifyToken, checkRole } = createAuthMiddleware(finalConfig);
|
|
81
|
+
const { rateLimiter, limiterFor, verifyToken, checkRole, checkOwnershipOrAdmin } = createAuthMiddleware(finalConfig);
|
|
64
82
|
|
|
65
|
-
//
|
|
66
|
-
if (finalConfig.social.google) {
|
|
83
|
+
// setup passport strategies as earlier if provided...
|
|
84
|
+
if (finalConfig.social && finalConfig.social.google) {
|
|
67
85
|
passport.use(new GoogleStrategy(finalConfig.social.google,
|
|
68
86
|
async (accessToken, refreshToken, profile, done) => {
|
|
69
87
|
try {
|
|
70
88
|
const user = await authService.handleSocialLogin('google', profile);
|
|
71
89
|
done(null, user);
|
|
72
|
-
} catch (
|
|
73
|
-
done(
|
|
90
|
+
} catch (err) {
|
|
91
|
+
done(err);
|
|
74
92
|
}
|
|
75
93
|
}
|
|
76
94
|
));
|
|
77
95
|
}
|
|
78
96
|
|
|
79
|
-
return function(app) {
|
|
97
|
+
return function (app) {
|
|
98
|
+
app.use(cookieParser());
|
|
99
|
+
// CSRF: enable only if config.enableCSRF === true
|
|
100
|
+
if (finalConfig.enableCSRF) {
|
|
101
|
+
// use cookie-based CSRF token and expect header 'x-csrf-token' or cookie
|
|
102
|
+
const csrfProtection = csrf({ cookie: true });
|
|
103
|
+
app.use(csrfProtection);
|
|
104
|
+
|
|
105
|
+
// expose token endpoint to clients (they must fetch this and send it back on state-changing requests)
|
|
106
|
+
app.get('/csrf-token', (req, res) => {
|
|
107
|
+
res.json({ csrfToken: req.csrfToken() });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
app.use(passport.initialize());
|
|
81
112
|
app.use(rateLimiter);
|
|
113
|
+
app.use(helmet({
|
|
114
|
+
contentSecurityPolicy: false, // optional to ease frontend dev
|
|
115
|
+
crossOriginEmbedderPolicy: false,
|
|
116
|
+
}));
|
|
82
117
|
|
|
83
|
-
|
|
118
|
+
|
|
119
|
+
// attach services
|
|
84
120
|
app.authService = authService;
|
|
85
121
|
app.roleService = roleService;
|
|
86
122
|
app.auditService = auditService;
|
|
87
123
|
app.mfaService = mfaService;
|
|
88
124
|
app.passwordPolicy = passwordPolicy;
|
|
89
125
|
|
|
90
|
-
//
|
|
91
|
-
app.post(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
126
|
+
// register
|
|
127
|
+
app.post(
|
|
128
|
+
finalConfig.routes.register,
|
|
129
|
+
limiterFor('register'),
|
|
130
|
+
validators.registerValidator,
|
|
131
|
+
validators.handleValidation,
|
|
132
|
+
async (req, res, next) => {
|
|
133
|
+
try {
|
|
134
|
+
const { isValid, errors } = passwordPolicy.validate(req.body.password, {
|
|
135
|
+
email: req.body.email,
|
|
136
|
+
name: req.body.name
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!isValid) {
|
|
140
|
+
return res.status(400).json({ success: false, error: 'Password policy violation', details: errors });
|
|
141
|
+
}
|
|
97
142
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
const user = await authService.register(req.body);
|
|
144
|
+
await auditService.logActivity({
|
|
145
|
+
userId: user._id,
|
|
146
|
+
action: 'register',
|
|
147
|
+
ipAddress: req.ip,
|
|
148
|
+
userAgent: req.headers['user-agent'],
|
|
149
|
+
status: 'success'
|
|
102
150
|
});
|
|
151
|
+
|
|
152
|
+
res.status(201).json({ success: true, message: 'Registration successful' });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
next(err);
|
|
103
155
|
}
|
|
156
|
+
}
|
|
157
|
+
);
|
|
104
158
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
159
|
+
// login
|
|
160
|
+
app.post(
|
|
161
|
+
finalConfig.routes.login,
|
|
162
|
+
limiterFor('login'),
|
|
163
|
+
validators.loginValidator,
|
|
164
|
+
validators.handleValidation,
|
|
165
|
+
async (req, res, next) => {
|
|
166
|
+
try {
|
|
167
|
+
const deviceInfo = { browser: req.headers['user-agent'], ip: req.ip };
|
|
168
|
+
const result = await authService.login(req.body.email, req.body.password, deviceInfo);
|
|
113
169
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
170
|
+
await auditService.logActivity({
|
|
171
|
+
userId: result.user?._id,
|
|
172
|
+
action: 'login',
|
|
173
|
+
ipAddress: req.ip,
|
|
174
|
+
userAgent: req.headers['user-agent'],
|
|
175
|
+
status: 'success'
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
res.json({ success: true, ...result });
|
|
179
|
+
} catch (err) {
|
|
180
|
+
await auditService.logActivity({
|
|
181
|
+
action: 'login',
|
|
182
|
+
ipAddress: req.ip,
|
|
183
|
+
userAgent: req.headers['user-agent'],
|
|
184
|
+
status: 'failure',
|
|
185
|
+
details: { error: err.message }
|
|
186
|
+
});
|
|
187
|
+
next(err);
|
|
188
|
+
}
|
|
123
189
|
}
|
|
124
|
-
|
|
190
|
+
);
|
|
125
191
|
|
|
126
|
-
//
|
|
127
|
-
app.post(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
action: 'login',
|
|
143
|
-
ipAddress: req.ip,
|
|
144
|
-
userAgent: req.headers['user-agent'],
|
|
145
|
-
status: 'success'
|
|
146
|
-
});
|
|
192
|
+
// 2FA verification (rate-limited)
|
|
193
|
+
app.post(
|
|
194
|
+
finalConfig.routes.twoFactor,
|
|
195
|
+
limiterFor('2fa'),
|
|
196
|
+
validators.twoFAValidator,
|
|
197
|
+
validators.handleValidation,
|
|
198
|
+
async (req, res, next) => {
|
|
199
|
+
try {
|
|
200
|
+
const result = await authService.verify2FA(req.body.userId, req.body.code);
|
|
201
|
+
await auditService.logActivity({ userId: result.user._id, action: '2fa_verify', status: 'success' });
|
|
202
|
+
res.json({ success: true, ...result });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
next(err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
);
|
|
147
208
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
209
|
+
// forgot password (rate-limited)
|
|
210
|
+
app.post(
|
|
211
|
+
finalConfig.routes.forgotPassword,
|
|
212
|
+
limiterFor('forgotPassword'),
|
|
213
|
+
validators.forgotPasswordValidator,
|
|
214
|
+
validators.handleValidation,
|
|
215
|
+
async (req, res, next) => {
|
|
216
|
+
try {
|
|
217
|
+
await authService.forgotPassword(req.body.email);
|
|
218
|
+
res.json({ success: true, message: 'Password reset email sent if user exists' });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
next(err);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
);
|
|
160
224
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
// reset password
|
|
226
|
+
app.post(
|
|
227
|
+
finalConfig.routes.resetPassword,
|
|
228
|
+
validators.resetPasswordValidator,
|
|
229
|
+
validators.handleValidation,
|
|
230
|
+
async (req, res, next) => {
|
|
231
|
+
try {
|
|
232
|
+
await authService.resetPassword(req.body.token, req.body.newPassword);
|
|
233
|
+
await auditService.logActivity({ action: 'password_reset', status: 'success' });
|
|
234
|
+
res.json({ success: true, message: 'Password reset successful' });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
next(err);
|
|
237
|
+
}
|
|
165
238
|
}
|
|
166
|
-
|
|
239
|
+
);
|
|
167
240
|
|
|
168
|
-
//
|
|
169
|
-
app.post(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
241
|
+
// refresh token
|
|
242
|
+
app.post(
|
|
243
|
+
finalConfig.routes.refreshToken,
|
|
244
|
+
validators.refreshTokenValidator,
|
|
245
|
+
validators.handleValidation,
|
|
246
|
+
async (req, res, next) => {
|
|
247
|
+
try {
|
|
248
|
+
const result = await authService.refreshToken(req.body.refreshToken);
|
|
249
|
+
res.json({ success: true, ...result });
|
|
250
|
+
} catch (err) {
|
|
251
|
+
next(err);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
);
|
|
181
255
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
256
|
+
// update profile (prevent IDOR: only owner or admin)
|
|
257
|
+
app.put(
|
|
258
|
+
finalConfig.routes.profile,
|
|
259
|
+
verifyToken,
|
|
260
|
+
checkOwnershipOrAdmin((req) => req.user.id), // target is the calling user; if you allow admin to pass an id param, adapt accordingly
|
|
261
|
+
validators.profileUpdateValidator,
|
|
262
|
+
validators.handleValidation,
|
|
263
|
+
async (req, res, next) => {
|
|
264
|
+
try {
|
|
265
|
+
const user = await authService.updateProfile(req.user.id, req.body);
|
|
266
|
+
await auditService.logActivity({ userId: user._id, action: 'profile_update', status: 'success' });
|
|
267
|
+
res.json({ success: true, user });
|
|
268
|
+
} catch (err) {
|
|
269
|
+
next(err);
|
|
270
|
+
}
|
|
191
271
|
}
|
|
192
|
-
|
|
272
|
+
);
|
|
193
273
|
|
|
194
|
-
//
|
|
195
|
-
app.post('/roles', verifyToken, checkRole(['admin']), async (req, res) => {
|
|
274
|
+
// roles (admin only)
|
|
275
|
+
app.post('/roles', verifyToken, checkRole(['admin']), async (req, res, next) => {
|
|
196
276
|
try {
|
|
197
277
|
const role = await roleService.createRole(req.body);
|
|
198
278
|
res.status(201).json({ success: true, role });
|
|
199
|
-
} catch (
|
|
200
|
-
|
|
201
|
-
success: false,
|
|
202
|
-
error: error.message
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Update profile with audit logging
|
|
208
|
-
app.put(finalConfig.routes.profile, verifyToken, async (req, res) => {
|
|
209
|
-
try {
|
|
210
|
-
const user = await authService.updateProfile(req.user.id, req.body);
|
|
211
|
-
await auditService.logActivity({
|
|
212
|
-
userId: user._id,
|
|
213
|
-
action: 'profile_update',
|
|
214
|
-
status: 'success'
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
res.json({
|
|
218
|
-
success: true,
|
|
219
|
-
user
|
|
220
|
-
});
|
|
221
|
-
} catch (error) {
|
|
222
|
-
res.status(400).json({
|
|
223
|
-
success: false,
|
|
224
|
-
error: error.message
|
|
225
|
-
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
next(err);
|
|
226
281
|
}
|
|
227
282
|
});
|
|
228
283
|
|
|
229
|
-
//
|
|
230
|
-
if (finalConfig.social.google) {
|
|
231
|
-
app.get('/auth/google',
|
|
232
|
-
passport.authenticate('google', { scope: ['profile', 'email'] })
|
|
233
|
-
);
|
|
284
|
+
// social google callback example (kept same)
|
|
285
|
+
if (finalConfig.social && finalConfig.social.google) {
|
|
286
|
+
app.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
|
|
234
287
|
|
|
235
288
|
app.get('/auth/google/callback',
|
|
236
289
|
passport.authenticate('google', { session: false }),
|
|
237
|
-
async (req, res) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
);
|
|
290
|
+
async (req, res, next) => {
|
|
291
|
+
try {
|
|
292
|
+
const tokens = await authService.generateTokens(req.user);
|
|
293
|
+
await auditService.logActivity({
|
|
294
|
+
userId: req.user._id,
|
|
295
|
+
action: 'social_login',
|
|
296
|
+
status: 'success',
|
|
297
|
+
details: { provider: 'google' }
|
|
298
|
+
});
|
|
299
|
+
res.json({ success: true, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, user: req.user });
|
|
300
|
+
} catch (err) {
|
|
301
|
+
next(err);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
254
304
|
}
|
|
255
305
|
|
|
256
|
-
//
|
|
306
|
+
// centralized error handler (friendly for clients)
|
|
257
307
|
app.use((err, req, res, next) => {
|
|
258
|
-
console.error(err.stack);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
308
|
+
console.error(err && err.stack ? err.stack : err);
|
|
309
|
+
const status = err.status || (err.name === 'ValidationError' ? 400 : 500);
|
|
310
|
+
|
|
311
|
+
// If client error (4xx) show helpful message, else generic in production
|
|
312
|
+
let message;
|
|
313
|
+
if (status >= 400 && status < 500) {
|
|
314
|
+
message = err.message || 'Bad request';
|
|
315
|
+
} else {
|
|
316
|
+
message = process.env.NODE_ENV === 'development' ? (err.message || 'Internal Server Error') : 'Internal Server Error';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
res.status(status).json({ success: false, error: message });
|
|
263
320
|
});
|
|
264
321
|
};
|
|
265
322
|
}
|
|
266
323
|
|
|
267
|
-
module.exports = { auth };
|
|
324
|
+
module.exports = { auth, ApiError };
|
|
@@ -1,51 +1,111 @@
|
|
|
1
|
+
// packages/auth/src/middlewares/auth.middleware.js
|
|
1
2
|
const rateLimit = require('express-rate-limit');
|
|
2
3
|
const jwt = require('jsonwebtoken');
|
|
3
4
|
|
|
4
5
|
const createAuthMiddleware = (config) => {
|
|
5
|
-
|
|
6
|
+
// Default generic limiter (used for non-sensitive endpoints)
|
|
7
|
+
const genericLimiter = rateLimit({
|
|
6
8
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
7
|
-
max: 100
|
|
9
|
+
max: 100,
|
|
10
|
+
standardHeaders: true,
|
|
11
|
+
legacyHeaders: false
|
|
8
12
|
});
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
// Sensitive endpoint limiters - tuned for production
|
|
15
|
+
const loginLimiter = rateLimit({
|
|
16
|
+
windowMs: 15 * 60 * 1000,
|
|
17
|
+
max: config?.limits?.loginMax || 10, // default 10 attempts per 15m
|
|
18
|
+
message: { success: false, error: 'Too many login attempts, please try later' },
|
|
19
|
+
standardHeaders: true,
|
|
20
|
+
legacyHeaders: false
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const twoFALimiter = rateLimit({
|
|
24
|
+
windowMs: 15 * 60 * 1000,
|
|
25
|
+
max: config?.limits?.twoFAMax || 20, // 20 per 15m
|
|
26
|
+
message: { success: false, error: 'Too many 2FA attempts, please try later' },
|
|
27
|
+
standardHeaders: true,
|
|
28
|
+
legacyHeaders: false
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const forgotPasswordLimiter = rateLimit({
|
|
32
|
+
windowMs: 60 * 60 * 1000,
|
|
33
|
+
max: config?.limits?.forgotPasswordMax || 5, // 5 per hour
|
|
34
|
+
message: { success: false, error: 'Too many password reset requests, please try later' },
|
|
35
|
+
standardHeaders: true,
|
|
36
|
+
legacyHeaders: false
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Expose function to choose limiter per-route
|
|
40
|
+
const limiterFor = (routeName) => {
|
|
41
|
+
switch (routeName) {
|
|
42
|
+
case 'login': return loginLimiter;
|
|
43
|
+
case '2fa': return twoFALimiter;
|
|
44
|
+
case 'forgotPassword': return forgotPasswordLimiter;
|
|
45
|
+
case 'refresh': return refreshLimiter;
|
|
46
|
+
default: return genericLimiter;
|
|
18
47
|
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Refresh token limiter
|
|
51
|
+
const refreshLimiter = rateLimit({
|
|
52
|
+
windowMs: 15 * 60 * 1000,
|
|
53
|
+
max: config?.limits?.refreshMax || 30,
|
|
54
|
+
message: { success: false, error: 'Too many refresh attempts' }
|
|
55
|
+
});
|
|
56
|
+
|
|
19
57
|
|
|
58
|
+
const verifyToken = (req, res, next) => {
|
|
59
|
+
const header = req.headers.authorization;
|
|
60
|
+
if (!header || !header.startsWith('Bearer ')) {
|
|
61
|
+
return res.status(401).json({ success: false, error: 'No token provided' });
|
|
62
|
+
}
|
|
63
|
+
const token = header.split(' ')[1];
|
|
20
64
|
try {
|
|
21
65
|
const decoded = jwt.verify(token, config.env.JWT_SECRET);
|
|
22
|
-
req.user
|
|
66
|
+
// keep minimal user data on req.user
|
|
67
|
+
req.user = { id: decoded.id, role: decoded.role, email: decoded.email };
|
|
23
68
|
next();
|
|
24
|
-
} catch (
|
|
25
|
-
res.status(401).json({
|
|
26
|
-
success: false,
|
|
27
|
-
error: 'Invalid token'
|
|
28
|
-
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return res.status(401).json({ success: false, error: 'Invalid or expired token' });
|
|
29
71
|
}
|
|
30
72
|
};
|
|
31
73
|
|
|
32
|
-
const checkRole = (roles) => {
|
|
74
|
+
const checkRole = (roles = []) => {
|
|
33
75
|
return (req, res, next) => {
|
|
76
|
+
if (!req.user) {
|
|
77
|
+
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
78
|
+
}
|
|
34
79
|
if (!roles.includes(req.user.role)) {
|
|
35
|
-
return res.status(403).json({
|
|
36
|
-
success: false,
|
|
37
|
-
error: 'Insufficient permissions'
|
|
38
|
-
});
|
|
80
|
+
return res.status(403).json({ success: false, error: 'Insufficient permissions' });
|
|
39
81
|
}
|
|
40
82
|
next();
|
|
41
83
|
};
|
|
42
84
|
};
|
|
43
85
|
|
|
86
|
+
// Ownership check middleware for IDOR protection: allow if owner or admin
|
|
87
|
+
const checkOwnershipOrAdmin = (getTargetUserId) => {
|
|
88
|
+
return async (req, res, next) => {
|
|
89
|
+
try {
|
|
90
|
+
const targetId = typeof getTargetUserId === 'function' ? getTargetUserId(req) : req.params.id;
|
|
91
|
+
if (!req.user) return res.status(401).json({ success: false, error: 'Unauthorized' });
|
|
92
|
+
if (req.user.role === 'admin' || String(req.user.id) === String(targetId)) {
|
|
93
|
+
return next();
|
|
94
|
+
}
|
|
95
|
+
return res.status(403).json({ success: false, error: 'Insufficient permissions to modify this resource' });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return res.status(500).json({ success: false, error: 'Server error' });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
44
102
|
return {
|
|
45
|
-
rateLimiter,
|
|
103
|
+
rateLimiter: genericLimiter,
|
|
104
|
+
limiterFor,
|
|
46
105
|
verifyToken,
|
|
47
|
-
checkRole
|
|
106
|
+
checkRole,
|
|
107
|
+
checkOwnershipOrAdmin
|
|
48
108
|
};
|
|
49
109
|
};
|
|
50
110
|
|
|
51
|
-
module.exports = createAuthMiddleware;
|
|
111
|
+
module.exports = createAuthMiddleware;
|