mbkauthe 1.3.5 → 1.4.0
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 +249 -187
- package/docs/api.md +841 -0
- package/index.js +13 -2
- package/lib/main.js +104 -31
- package/lib/pool.js +8 -8
- package/lib/validateSessionAndRole.js +55 -29
- package/package.json +5 -5
- package/public/main.js +134 -0
- package/views/2fa.handlebars +35 -445
- package/views/Error/dError.handlebars +71 -551
- package/views/info.handlebars +180 -160
- package/views/loginmbkauthe.handlebars +37 -557
- package/views/sharedStyles.handlebars +498 -0
- package/views/showmessage.handlebars +97 -62
package/index.js
CHANGED
|
@@ -34,10 +34,21 @@ if (mbkautheVar.COOKIE_EXPIRE_TIME !== undefined) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const app = express();
|
|
37
|
-
if (process.env.test === "
|
|
38
|
-
console.log("[mbkauthe]
|
|
37
|
+
if (process.env.test === "dev") {
|
|
38
|
+
console.log("[mbkauthe] Dev mode is enabled. Starting server in dev mode.");
|
|
39
39
|
const port = 5555;
|
|
40
40
|
app.use(router);
|
|
41
|
+
app.use((req, res) => {
|
|
42
|
+
console.log(`Path not found: ${req.method} ${req.url}`);
|
|
43
|
+
return res.status(404).render("Error/dError.handlebars", {
|
|
44
|
+
layout: false,
|
|
45
|
+
code: 404,
|
|
46
|
+
error: "Not Found",
|
|
47
|
+
message: "The requested page was not found.",
|
|
48
|
+
pagename: "Home",
|
|
49
|
+
page: "/mbkauthe/login",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
41
52
|
app.listen(port, () => {
|
|
42
53
|
console.log(`[mbkauthe] Server running on http://localhost:${port}`);
|
|
43
54
|
});
|
package/lib/main.js
CHANGED
|
@@ -31,6 +31,10 @@ router.use(express.json());
|
|
|
31
31
|
router.use(express.urlencoded({ extended: true }));
|
|
32
32
|
router.use(cookieParser());
|
|
33
33
|
|
|
34
|
+
router.get('/mbkauthe/main.js', (req, res) => {
|
|
35
|
+
res.sendFile(path.join(process.cwd(), 'public', 'main.js'));
|
|
36
|
+
});
|
|
37
|
+
|
|
34
38
|
// CSRF protection middleware
|
|
35
39
|
const csrfProtection = csurf({ cookie: false });
|
|
36
40
|
|
|
@@ -55,6 +59,18 @@ const LoginLimit = rateLimit({
|
|
|
55
59
|
}
|
|
56
60
|
});
|
|
57
61
|
|
|
62
|
+
const LogoutLimit = rateLimit({
|
|
63
|
+
windowMs: 1 * 60 * 1000,
|
|
64
|
+
max: 10,
|
|
65
|
+
message: { success: false, message: "Too many logout attempts, please try again later" }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const TwoFALimit = rateLimit({
|
|
69
|
+
windowMs: 1 * 60 * 1000,
|
|
70
|
+
max: 5,
|
|
71
|
+
message: { success: false, message: "Too many 2FA attempts, please try again later" }
|
|
72
|
+
});
|
|
73
|
+
|
|
58
74
|
const sessionConfig = {
|
|
59
75
|
store: new PgSession({
|
|
60
76
|
pool: dblogin,
|
|
@@ -79,11 +95,19 @@ const sessionConfig = {
|
|
|
79
95
|
router.use(session(sessionConfig));
|
|
80
96
|
|
|
81
97
|
router.use(async (req, res, next) => {
|
|
98
|
+
// Only restore session if not already present and sessionId cookie exists
|
|
82
99
|
if (!req.session.user && req.cookies.sessionId) {
|
|
83
100
|
try {
|
|
84
101
|
const sessionId = req.cookies.sessionId;
|
|
85
|
-
|
|
86
|
-
|
|
102
|
+
|
|
103
|
+
// Validate sessionId format (should be 64 hex characters)
|
|
104
|
+
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
105
|
+
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE "SessionId" = $1 AND "Active" = true`;
|
|
110
|
+
const result = await dblogin.query({ name: 'get-user-by-sessionid', text: query, values: [sessionId] });
|
|
87
111
|
|
|
88
112
|
if (result.rows.length > 0) {
|
|
89
113
|
const user = result.rows[0];
|
|
@@ -91,7 +115,10 @@ router.use(async (req, res, next) => {
|
|
|
91
115
|
id: user.id,
|
|
92
116
|
username: user.UserName,
|
|
93
117
|
UserName: user.UserName,
|
|
118
|
+
role: user.Role,
|
|
119
|
+
Role: user.Role,
|
|
94
120
|
sessionId,
|
|
121
|
+
allowedApps: user.AllowedApps,
|
|
95
122
|
};
|
|
96
123
|
}
|
|
97
124
|
} catch (err) {
|
|
@@ -110,12 +137,20 @@ const getCookieOptions = () => ({
|
|
|
110
137
|
httpOnly: true
|
|
111
138
|
});
|
|
112
139
|
|
|
140
|
+
const getClearCookieOptions = () => ({
|
|
141
|
+
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
142
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false,
|
|
143
|
+
sameSite: 'lax',
|
|
144
|
+
path: '/',
|
|
145
|
+
httpOnly: true
|
|
146
|
+
});
|
|
147
|
+
|
|
113
148
|
async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
114
149
|
try {
|
|
115
150
|
// smaller session id is sufficient and faster to generate/serialize
|
|
116
151
|
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
117
152
|
console.log(`[mbkauthe] Generated session ID for username: ${user.username}`);
|
|
118
|
-
|
|
153
|
+
|
|
119
154
|
// Delete old session record for this user
|
|
120
155
|
await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.username]);
|
|
121
156
|
|
|
@@ -139,7 +174,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
139
174
|
|
|
140
175
|
req.session.save(async (err) => {
|
|
141
176
|
if (err) {
|
|
142
|
-
console.
|
|
177
|
+
console.error("[mbkauthe] Session save error:", err);
|
|
143
178
|
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
144
179
|
}
|
|
145
180
|
// avoid writing back into the session table here to reduce DB writes;
|
|
@@ -162,7 +197,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
162
197
|
res.status(200).json(responsePayload);
|
|
163
198
|
});
|
|
164
199
|
} catch (err) {
|
|
165
|
-
console.
|
|
200
|
+
console.error("[mbkauthe] Error during login completion:", err);
|
|
166
201
|
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
167
202
|
}
|
|
168
203
|
}
|
|
@@ -170,8 +205,11 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
170
205
|
router.use(async (req, res, next) => {
|
|
171
206
|
if (req.session && req.session.user) {
|
|
172
207
|
const cookieOptions = getCookieOptions();
|
|
173
|
-
|
|
174
|
-
|
|
208
|
+
// Only set cookies if they're missing or different
|
|
209
|
+
if (req.cookies.sessionId !== req.session.user.sessionId) {
|
|
210
|
+
res.cookie("username", req.session.user.username, { ...cookieOptions, httpOnly: false });
|
|
211
|
+
res.cookie("sessionId", req.session.user.sessionId, cookieOptions);
|
|
212
|
+
}
|
|
175
213
|
}
|
|
176
214
|
next();
|
|
177
215
|
});
|
|
@@ -187,7 +225,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
|
|
|
187
225
|
return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
|
|
188
226
|
}
|
|
189
227
|
|
|
190
|
-
const cookieOptions =
|
|
228
|
+
const cookieOptions = getClearCookieOptions();
|
|
191
229
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
192
230
|
res.clearCookie("sessionId", cookieOptions);
|
|
193
231
|
res.clearCookie("username", cookieOptions);
|
|
@@ -199,7 +237,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
|
|
|
199
237
|
});
|
|
200
238
|
});
|
|
201
239
|
} catch (err) {
|
|
202
|
-
console.
|
|
240
|
+
console.error("[mbkauthe] Database query error during session termination:", err);
|
|
203
241
|
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
204
242
|
}
|
|
205
243
|
});
|
|
@@ -208,8 +246,8 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
208
246
|
console.log("[mbkauthe] Login request received");
|
|
209
247
|
|
|
210
248
|
const { username, password } = req.body;
|
|
211
|
-
console.log(`[mbkauthe] Login attempt for username: ${username}`);
|
|
212
249
|
|
|
250
|
+
// Input validation
|
|
213
251
|
if (!username || !password) {
|
|
214
252
|
console.log("[mbkauthe] Missing username or password");
|
|
215
253
|
return res.status(400).json({
|
|
@@ -218,17 +256,45 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
218
256
|
});
|
|
219
257
|
}
|
|
220
258
|
|
|
259
|
+
// Validate username format and length
|
|
260
|
+
if (typeof username !== 'string' || username.trim().length === 0 || username.length > 255) {
|
|
261
|
+
console.warn("[mbkauthe] Invalid username format");
|
|
262
|
+
return res.status(400).json({
|
|
263
|
+
success: false,
|
|
264
|
+
message: "Invalid username format",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Validate password length
|
|
269
|
+
if (typeof password !== 'string' || password.length < 8 || password.length > 255) {
|
|
270
|
+
console.warn("[mbkauthe] Invalid password length");
|
|
271
|
+
return res.status(400).json({
|
|
272
|
+
success: false,
|
|
273
|
+
message: "Password must be at least 8 characters long",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log(`[mbkauthe] Login attempt for username: ${username.trim()}`);
|
|
278
|
+
|
|
279
|
+
const trimmedUsername = username.trim();
|
|
280
|
+
|
|
221
281
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
282
|
+
const userQuery = `SELECT id, "UserName", "Password", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
|
|
283
|
+
const userResult = await dblogin.query({ name: 'get-user-by-username', text: userQuery, values: [trimmedUsername] });
|
|
224
284
|
|
|
225
285
|
if (userResult.rows.length === 0) {
|
|
226
|
-
console.log(`[mbkauthe] Username does not exist: ${
|
|
286
|
+
console.log(`[mbkauthe] Username does not exist: ${trimmedUsername}`);
|
|
227
287
|
return res.status(404).json({ success: false, message: "Incorrect Username Or Password" });
|
|
228
288
|
}
|
|
229
289
|
|
|
230
290
|
const user = userResult.rows[0];
|
|
231
291
|
|
|
292
|
+
// Validate user has password field
|
|
293
|
+
if (!user.Password) {
|
|
294
|
+
console.error("[mbkauthe] User account has no password set");
|
|
295
|
+
return res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
296
|
+
}
|
|
297
|
+
|
|
232
298
|
if (mbkautheVar.EncryptedPassword === "true") {
|
|
233
299
|
try {
|
|
234
300
|
const result = await bcrypt.compare(password, user.Password);
|
|
@@ -243,13 +309,13 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
243
309
|
}
|
|
244
310
|
} else {
|
|
245
311
|
if (user.Password !== password) {
|
|
246
|
-
console.log(`[mbkauthe] Incorrect password for username: ${
|
|
312
|
+
console.log(`[mbkauthe] Incorrect password for username: ${trimmedUsername}`);
|
|
247
313
|
return res.status(401).json({ success: false, errorCode: 603, message: "Incorrect Username Or Password" });
|
|
248
314
|
}
|
|
249
315
|
}
|
|
250
316
|
|
|
251
317
|
if (!user.Active) {
|
|
252
|
-
console.log(`[mbkauthe] Inactive account for username: ${
|
|
318
|
+
console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
|
|
253
319
|
return res.status(403).json({ success: false, message: "Account is inactive" });
|
|
254
320
|
}
|
|
255
321
|
|
|
@@ -262,8 +328,8 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
262
328
|
}
|
|
263
329
|
|
|
264
330
|
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
|
|
265
|
-
|
|
266
|
-
|
|
331
|
+
const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
332
|
+
const twoFAResult = await dblogin.query({ name: 'get-2fa-status', text: query, values: [trimmedUsername] });
|
|
267
333
|
|
|
268
334
|
if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
|
|
269
335
|
// 2FA is enabled, prompt for token on a separate page
|
|
@@ -271,9 +337,9 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
271
337
|
id: user.id,
|
|
272
338
|
username: user.UserName,
|
|
273
339
|
role: user.Role,
|
|
274
|
-
Role: user.
|
|
340
|
+
Role: user.Role,
|
|
275
341
|
};
|
|
276
|
-
console.log(`[mbkauthe] 2FA required for user: ${
|
|
342
|
+
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
277
343
|
return res.json({ success: true, twoFactorRequired: true });
|
|
278
344
|
}
|
|
279
345
|
}
|
|
@@ -283,12 +349,12 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
283
349
|
id: user.id,
|
|
284
350
|
username: user.UserName,
|
|
285
351
|
role: user.Role,
|
|
286
|
-
Role: user.
|
|
352
|
+
Role: user.Role,
|
|
287
353
|
};
|
|
288
354
|
await completeLoginProcess(req, res, userForSession);
|
|
289
355
|
|
|
290
356
|
} catch (err) {
|
|
291
|
-
console.
|
|
357
|
+
console.error("[mbkauthe] Error during login process:", err);
|
|
292
358
|
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
293
359
|
}
|
|
294
360
|
});
|
|
@@ -304,7 +370,7 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
304
370
|
});
|
|
305
371
|
});
|
|
306
372
|
|
|
307
|
-
router.post("/mbkauthe/api/verify-2fa", async (req, res) => {
|
|
373
|
+
router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
|
|
308
374
|
if (!req.session.preAuthUser) {
|
|
309
375
|
return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
|
|
310
376
|
}
|
|
@@ -312,10 +378,17 @@ router.post("/mbkauthe/api/verify-2fa", async (req, res) => {
|
|
|
312
378
|
const { token } = req.body;
|
|
313
379
|
const { username, id, role } = req.session.preAuthUser;
|
|
314
380
|
|
|
315
|
-
|
|
381
|
+
// Validate 2FA token
|
|
382
|
+
if (!token || typeof token !== 'string') {
|
|
316
383
|
return res.status(400).json({ success: false, message: "2FA token is required" });
|
|
317
384
|
}
|
|
318
385
|
|
|
386
|
+
// Validate token format (should be 6 digits)
|
|
387
|
+
const sanitizedToken = token.trim();
|
|
388
|
+
if (!/^\d{6}$/.test(sanitizedToken)) {
|
|
389
|
+
return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
|
|
390
|
+
}
|
|
391
|
+
|
|
319
392
|
try {
|
|
320
393
|
const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
321
394
|
const twoFAResult = await dblogin.query(query, [username]);
|
|
@@ -328,7 +401,7 @@ router.post("/mbkauthe/api/verify-2fa", async (req, res) => {
|
|
|
328
401
|
const tokenValidates = speakeasy.totp.verify({
|
|
329
402
|
secret: sharedSecret,
|
|
330
403
|
encoding: "base32",
|
|
331
|
-
token:
|
|
404
|
+
token: sanitizedToken,
|
|
332
405
|
window: 1,
|
|
333
406
|
});
|
|
334
407
|
|
|
@@ -343,12 +416,12 @@ router.post("/mbkauthe/api/verify-2fa", async (req, res) => {
|
|
|
343
416
|
await completeLoginProcess(req, res, userForSession, redirectUrl);
|
|
344
417
|
|
|
345
418
|
} catch (err) {
|
|
346
|
-
console.
|
|
419
|
+
console.error("[mbkauthe] Error during 2FA verification:", err);
|
|
347
420
|
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
348
421
|
}
|
|
349
422
|
});
|
|
350
423
|
|
|
351
|
-
router.post("/mbkauthe/api/logout", async (req, res) => {
|
|
424
|
+
router.post("/mbkauthe/api/logout", LogoutLimit, csrfProtection, async (req, res) => {
|
|
352
425
|
if (req.session.user) {
|
|
353
426
|
try {
|
|
354
427
|
const { id, username } = req.session.user;
|
|
@@ -361,11 +434,11 @@ router.post("/mbkauthe/api/logout", async (req, res) => {
|
|
|
361
434
|
|
|
362
435
|
req.session.destroy((err) => {
|
|
363
436
|
if (err) {
|
|
364
|
-
console.
|
|
437
|
+
console.error("[mbkauthe] Error destroying session:", err);
|
|
365
438
|
return res.status(500).json({ success: false, message: "Logout failed" });
|
|
366
439
|
}
|
|
367
440
|
|
|
368
|
-
const cookieOptions =
|
|
441
|
+
const cookieOptions = getClearCookieOptions();
|
|
369
442
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
370
443
|
res.clearCookie("sessionId", cookieOptions);
|
|
371
444
|
res.clearCookie("username", cookieOptions);
|
|
@@ -374,7 +447,7 @@ router.post("/mbkauthe/api/logout", async (req, res) => {
|
|
|
374
447
|
res.status(200).json({ success: true, message: "Logout successful" });
|
|
375
448
|
});
|
|
376
449
|
} catch (err) {
|
|
377
|
-
console.
|
|
450
|
+
console.error("[mbkauthe] Database query error during logout:", err);
|
|
378
451
|
res.status(500).json({ success: false, message: "Internal Server Error" });
|
|
379
452
|
}
|
|
380
453
|
} else {
|
|
@@ -399,7 +472,7 @@ async function getLatestVersion() {
|
|
|
399
472
|
try {
|
|
400
473
|
const response = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkauthe/main/package.json');
|
|
401
474
|
if (!response.ok) {
|
|
402
|
-
console.
|
|
475
|
+
console.error(`GitHub API responded with status ${response.status}`);
|
|
403
476
|
return "0.0.0";
|
|
404
477
|
}
|
|
405
478
|
const latestPackageJson = await response.json();
|
|
@@ -556,7 +629,7 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
556
629
|
};
|
|
557
630
|
|
|
558
631
|
// Generate session and complete login
|
|
559
|
-
const sessionId = crypto.randomBytes(
|
|
632
|
+
const sessionId = crypto.randomBytes(32).toString("hex");
|
|
560
633
|
console.log(`[mbkauthe] GitHub login: Generated session ID for username: ${user.UserName}`);
|
|
561
634
|
|
|
562
635
|
// Delete old session record for this user
|
package/lib/pool.js
CHANGED
|
@@ -15,15 +15,15 @@ if (!mbkautheVar) {
|
|
|
15
15
|
}
|
|
16
16
|
const requiredKeys = ["APP_NAME", "SESSION_SECRET_KEY", "IS_DEPLOYED", "LOGIN_DB", "MBKAUTH_TWO_FA_ENABLE", "DOMAIN"];
|
|
17
17
|
requiredKeys.forEach(key => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (!mbkautheVar[key]) {
|
|
19
|
+
throw new Error(`mbkautheVar.${key} is required`);
|
|
20
|
+
}
|
|
21
21
|
});
|
|
22
22
|
if (mbkautheVar.COOKIE_EXPIRE_TIME !== undefined) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const expireTime = parseFloat(mbkautheVar.COOKIE_EXPIRE_TIME);
|
|
24
|
+
if (isNaN(expireTime) || expireTime <= 0) {
|
|
25
|
+
throw new Error("mbkautheVar.COOKIE_EXPIRE_TIME must be a valid positive number");
|
|
26
|
+
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const poolConfig = {
|
|
@@ -36,7 +36,7 @@ const poolConfig = {
|
|
|
36
36
|
// - keep max small to avoid exhausting DB connections
|
|
37
37
|
// - reduce idle time so connections are returned sooner
|
|
38
38
|
// - set a short connection timeout to fail fast
|
|
39
|
-
max:
|
|
39
|
+
max: 10,
|
|
40
40
|
idleTimeoutMillis: 50000,
|
|
41
41
|
connectionTimeoutMillis: 5000,
|
|
42
42
|
};
|
|
@@ -11,13 +11,27 @@ const getCookieOptions = () => ({
|
|
|
11
11
|
httpOnly: true
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
const getClearCookieOptions = () => ({
|
|
15
|
+
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
16
|
+
secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false,
|
|
17
|
+
sameSite: 'lax',
|
|
18
|
+
path: '/',
|
|
19
|
+
httpOnly: true
|
|
20
|
+
});
|
|
21
|
+
|
|
14
22
|
async function validateSession(req, res, next) {
|
|
15
23
|
if (!req.session.user && req.cookies.sessionId) {
|
|
16
24
|
try {
|
|
17
25
|
const sessionId = req.cookies.sessionId;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
// Validate sessionId format (should be 64 hex characters)
|
|
28
|
+
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
29
|
+
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
30
|
+
return next();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE "SessionId" = $1 AND "Active" = true`;
|
|
34
|
+
const result = await dblogin.query({ name: 'get-user-by-sessionid', text: query, values: [sessionId] });
|
|
21
35
|
|
|
22
36
|
if (result.rows.length > 0) {
|
|
23
37
|
const user = result.rows[0];
|
|
@@ -25,7 +39,10 @@ async function validateSession(req, res, next) {
|
|
|
25
39
|
id: user.id,
|
|
26
40
|
username: user.UserName,
|
|
27
41
|
UserName: user.UserName,
|
|
42
|
+
role: user.Role,
|
|
43
|
+
Role: user.Role,
|
|
28
44
|
sessionId,
|
|
45
|
+
allowedApps: user.AllowedApps,
|
|
29
46
|
};
|
|
30
47
|
}
|
|
31
48
|
} catch (err) {
|
|
@@ -48,15 +65,16 @@ async function validateSession(req, res, next) {
|
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
try {
|
|
51
|
-
const { id, sessionId } = req.session.user;
|
|
52
|
-
const query = `SELECT "SessionId", "Active", "Role", "AllowedApps" FROM "Users" WHERE "id" = $1`;
|
|
53
|
-
const result = await dblogin.query({ name: 'get-user-by-id', text: query, values: [id] });
|
|
54
|
-
const userResult = result.rows[0];
|
|
68
|
+
const { id, sessionId, role, allowedApps } = req.session.user;
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
// Fetch only SessionId and Active status for validation (reduced query)
|
|
71
|
+
const query = `SELECT "SessionId", "Active" FROM "Users" WHERE "id" = $1`;
|
|
72
|
+
const result = await dblogin.query({ name: 'get-user-by-id', text: query, values: [id] });
|
|
73
|
+
|
|
74
|
+
if (result.rows.length === 0 || result.rows[0].SessionId !== sessionId) {
|
|
57
75
|
console.log(`[mbkauthe] Session invalidated for user "${req.session.user.username}"`);
|
|
58
76
|
req.session.destroy();
|
|
59
|
-
const cookieOptions =
|
|
77
|
+
const cookieOptions = getClearCookieOptions();
|
|
60
78
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
61
79
|
res.clearCookie("sessionId", cookieOptions);
|
|
62
80
|
res.clearCookie("username", cookieOptions);
|
|
@@ -70,10 +88,10 @@ async function validateSession(req, res, next) {
|
|
|
70
88
|
});
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
if (!
|
|
91
|
+
if (!result.rows[0].Active) {
|
|
74
92
|
console.log(`[mbkauthe] Account is inactive for user "${req.session.user.username}"`);
|
|
75
93
|
req.session.destroy();
|
|
76
|
-
const cookieOptions =
|
|
94
|
+
const cookieOptions = getClearCookieOptions();
|
|
77
95
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
78
96
|
res.clearCookie("sessionId", cookieOptions);
|
|
79
97
|
res.clearCookie("username", cookieOptions);
|
|
@@ -87,12 +105,11 @@ async function validateSession(req, res, next) {
|
|
|
87
105
|
});
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
if (
|
|
91
|
-
const allowedApps = userResult.AllowedApps;
|
|
108
|
+
if (role !== "SuperAdmin") {
|
|
92
109
|
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
93
110
|
console.warn(`[mbkauthe] User \"${req.session.user.username}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
94
111
|
req.session.destroy();
|
|
95
|
-
const cookieOptions =
|
|
112
|
+
const cookieOptions = getClearCookieOptions();
|
|
96
113
|
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
97
114
|
res.clearCookie("sessionId", cookieOptions);
|
|
98
115
|
res.clearCookie("username", cookieOptions);
|
|
@@ -132,8 +149,8 @@ const checkRolePermission = (requiredRole, notAllowed) => {
|
|
|
132
149
|
|
|
133
150
|
const userId = req.session.user.id;
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
const query = `SELECT "Role" FROM "Users" WHERE "id" = $1`;
|
|
153
|
+
const result = await dblogin.query({ name: 'get-role-by-id', text: query, values: [userId] });
|
|
137
154
|
|
|
138
155
|
if (result.rows.length === 0) {
|
|
139
156
|
return res.status(401).json({ success: false, message: "User not found" });
|
|
@@ -202,20 +219,29 @@ const authapi = (requiredRole = []) => {
|
|
|
202
219
|
return (req, res, next) => {
|
|
203
220
|
const token = req.headers["authorization"];
|
|
204
221
|
|
|
205
|
-
|
|
206
|
-
console.log("[mbkauthe] [authapi] Received request with token:", token[0] + token[1] + token[2], ".....", token[63]);
|
|
207
|
-
} else {
|
|
208
|
-
console.log("[mbkauthe] [authapi] Token is not a valid string:", token);
|
|
209
|
-
}
|
|
210
|
-
|
|
222
|
+
// Validate token
|
|
211
223
|
if (!token) {
|
|
212
|
-
console.
|
|
224
|
+
console.warn("[mbkauthe] [authapi] No token provided in the request headers");
|
|
213
225
|
return res.status(401).json({
|
|
214
226
|
success: false,
|
|
215
227
|
message: "Authorization token is required"
|
|
216
228
|
});
|
|
217
229
|
}
|
|
218
230
|
|
|
231
|
+
if (typeof token !== 'string' || token.length === 0 || token.length > 512) {
|
|
232
|
+
console.warn("[mbkauthe] [authapi] Invalid token format");
|
|
233
|
+
return res.status(401).json({
|
|
234
|
+
success: false,
|
|
235
|
+
message: "Invalid authorization token format"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof token === 'string' && token.length >= 64) {
|
|
240
|
+
console.log("[mbkauthe] [authapi] Received request with token:", token.substring(0, 3) + ".....", token.charAt(63));
|
|
241
|
+
} else {
|
|
242
|
+
console.log("[mbkauthe] [authapi] Received request with short token");
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
// Single query to validate API key and fetch user in one DB round trip.
|
|
220
246
|
(async () => {
|
|
221
247
|
try {
|
|
@@ -223,27 +249,27 @@ const authapi = (requiredRole = []) => {
|
|
|
223
249
|
SELECT u.id, u."UserName", u."Active", u."Role", k."key" as apikey
|
|
224
250
|
FROM "UserAuthApiKey" k
|
|
225
251
|
JOIN "Users" u ON u."UserName" = k.username
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
252
|
+
WHERE k."key" = $1 AND u."Active" = true
|
|
253
|
+
LIMIT 1
|
|
254
|
+
`;
|
|
229
255
|
|
|
230
256
|
const result = await pool.query(jointQuery, [token]);
|
|
231
257
|
|
|
232
258
|
if (result.rows.length === 0) {
|
|
233
|
-
console.
|
|
259
|
+
console.warn("[mbkauthe] [authapi] Invalid token or associated user inactive");
|
|
234
260
|
return res.status(401).json({ success: false, message: "The AuthApiToken Is Invalid or user inactive" });
|
|
235
261
|
}
|
|
236
262
|
|
|
237
263
|
const user = result.rows[0];
|
|
238
264
|
|
|
239
265
|
if (user.UserName === "demo") {
|
|
240
|
-
console.
|
|
266
|
+
console.warn("[mbkauthe] [authapi] Demo user attempted to access an endpoint. Access denied.");
|
|
241
267
|
return res.status(401).json({ success: false, message: "Demo user is not allowed to access endpoints" });
|
|
242
268
|
}
|
|
243
269
|
|
|
244
270
|
// role check
|
|
245
271
|
if ((requiredRole && user.Role !== requiredRole) && user.Role !== "SuperAdmin") {
|
|
246
|
-
console.
|
|
272
|
+
console.warn(`[mbkauthe] [authapi] User does not have the required role. Required: ${requiredRole}, User's role: ${user.Role}`);
|
|
247
273
|
return res.status(403).json({ success: false, message: `Access denied. Required role: ${requiredRole}` });
|
|
248
274
|
}
|
|
249
275
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mbkauthe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MBKTechStudio's reusable authentication system for Node.js applications.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
8
|
+
"dev": "set test=dev&& nodemon index.js"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/MIbnEKhalid/mbkauthe#readme",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"bcrypt": "^
|
|
29
|
+
"bcrypt": "^6.0.0",
|
|
30
30
|
"cheerio": "^1.0.0",
|
|
31
31
|
"connect-pg-simple": "^10.0.0",
|
|
32
32
|
"cookie-parser": "^1.4.7",
|
|
33
|
-
"csurf": "^1.
|
|
33
|
+
"csurf": "^1.2.2",
|
|
34
34
|
"dotenv": "^16.4.7",
|
|
35
35
|
"express": "^5.1.0",
|
|
36
36
|
"express-handlebars": "^8.0.1",
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"url": "^0.11.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"nodemon": "^
|
|
47
|
+
"nodemon": "^3.1.11"
|
|
48
48
|
}
|
|
49
49
|
}
|