mbkauthe 2.0.1 → 2.2.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/.env.example +2 -2
- package/README.md +128 -7
- package/docs/api.md +48 -1
- package/docs/db.md +53 -15
- package/{env.md → docs/env.md} +76 -2
- package/index.js +9 -29
- package/lib/config.js +199 -0
- package/lib/main.js +246 -162
- package/lib/pool.js +3 -27
- package/lib/validateSessionAndRole.js +19 -131
- package/package.json +2 -2
- package/public/bg.webp +0 -0
- package/public/icon.ico +0 -0
- package/public/icon.svg +5 -0
- package/views/2fa.handlebars +21 -41
- package/views/Error/dError.handlebars +7 -35
- package/views/backgroundElements.handlebars +6 -0
- package/views/head.handlebars +14 -0
- package/views/header.handlebars +15 -0
- package/views/info.handlebars +8 -29
- package/views/loginmbkauthe.handlebars +5 -40
- package/views/sharedStyles.handlebars +112 -1
- package/views/versionInfo.handlebars +6 -0
- package/public/bg.avif +0 -0
package/lib/main.js
CHANGED
|
@@ -14,25 +14,44 @@ import speakeasy from "speakeasy";
|
|
|
14
14
|
import passport from 'passport';
|
|
15
15
|
import GitHubStrategy from 'passport-github2';
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
18
|
import fs from "fs";
|
|
19
19
|
import path from "path";
|
|
20
|
-
|
|
21
|
-
import dotenv from "dotenv";
|
|
22
|
-
dotenv.config();
|
|
23
|
-
const mbkautheVar = JSON.parse(process.env.mbkautheVar);
|
|
20
|
+
import { mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS } from "./config.js";
|
|
24
21
|
|
|
25
22
|
const router = express.Router();
|
|
26
23
|
|
|
27
|
-
const require = createRequire(import.meta.url);
|
|
28
|
-
const packageJson = require("../package.json");
|
|
29
|
-
|
|
30
24
|
router.use(express.json());
|
|
31
25
|
router.use(express.urlencoded({ extended: true }));
|
|
32
26
|
router.use(cookieParser());
|
|
33
27
|
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
34
30
|
router.get('/mbkauthe/main.js', (req, res) => {
|
|
35
|
-
res.
|
|
31
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
32
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'main.js'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
router.get('/icon.svg', (req, res) => {
|
|
36
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
37
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'icon.svg'));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
router.get(['/favicon.ico','/icon.ico'], (req, res) => {
|
|
41
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
42
|
+
res.sendFile(path.join(__dirname, '..', 'public', 'icon.ico'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.get("/mbkauthe/bg.webp", (req, res) => {
|
|
46
|
+
const imgPath = path.join(__dirname, "..", "public", "bg.webp");
|
|
47
|
+
res.setHeader('Content-Type', 'image/webp');
|
|
48
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
49
|
+
const stream = fs.createReadStream(imgPath);
|
|
50
|
+
stream.on('error', (err) => {
|
|
51
|
+
console.error('[mbkauthe] Error streaming bg.webp:', err);
|
|
52
|
+
res.status(404).send('Image not found');
|
|
53
|
+
});
|
|
54
|
+
stream.pipe(res);
|
|
36
55
|
});
|
|
37
56
|
|
|
38
57
|
// CSRF protection middleware
|
|
@@ -113,14 +132,16 @@ router.use(session(sessionConfig));
|
|
|
113
132
|
router.use(async (req, res, next) => {
|
|
114
133
|
// Only restore session if not already present and sessionId cookie exists
|
|
115
134
|
if (!req.session.user && req.cookies.sessionId) {
|
|
135
|
+
const sessionId = req.cookies.sessionId;
|
|
136
|
+
|
|
137
|
+
// Early validation to avoid unnecessary processing
|
|
138
|
+
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
139
|
+
// Clear invalid cookie to prevent repeated attempts
|
|
140
|
+
res.clearCookie('sessionId', cachedClearCookieOptions);
|
|
141
|
+
return next();
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
try {
|
|
117
|
-
const sessionId = req.cookies.sessionId;
|
|
118
|
-
|
|
119
|
-
// Validate sessionId format (should be 64 hex characters) and normalize to lowercase
|
|
120
|
-
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
121
|
-
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
122
|
-
return next();
|
|
123
|
-
}
|
|
124
145
|
|
|
125
146
|
const normalizedSessionId = sessionId.toLowerCase();
|
|
126
147
|
|
|
@@ -136,6 +157,7 @@ router.use(async (req, res, next) => {
|
|
|
136
157
|
role: user.Role,
|
|
137
158
|
Role: user.Role,
|
|
138
159
|
sessionId: normalizedSessionId,
|
|
160
|
+
allowedApps: user.AllowedApps,
|
|
139
161
|
};
|
|
140
162
|
}
|
|
141
163
|
} catch (err) {
|
|
@@ -145,23 +167,6 @@ router.use(async (req, res, next) => {
|
|
|
145
167
|
next();
|
|
146
168
|
});
|
|
147
169
|
|
|
148
|
-
const getCookieOptions = () => ({
|
|
149
|
-
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
150
|
-
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
151
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
152
|
-
sameSite: 'lax',
|
|
153
|
-
path: '/',
|
|
154
|
-
httpOnly: true
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const getClearCookieOptions = () => ({
|
|
158
|
-
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
159
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
160
|
-
sameSite: 'lax',
|
|
161
|
-
path: '/',
|
|
162
|
-
httpOnly: true
|
|
163
|
-
});
|
|
164
|
-
|
|
165
170
|
router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
|
|
166
171
|
if (req.session?.user) {
|
|
167
172
|
return res.send(`
|
|
@@ -175,7 +180,7 @@ router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
|
|
|
175
180
|
}
|
|
176
181
|
});
|
|
177
182
|
|
|
178
|
-
async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
183
|
+
async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
|
|
179
184
|
try {
|
|
180
185
|
// Ensure both username formats are available for compatibility
|
|
181
186
|
const username = user.username || user.UserName;
|
|
@@ -196,19 +201,21 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
196
201
|
});
|
|
197
202
|
});
|
|
198
203
|
|
|
199
|
-
//
|
|
200
|
-
await
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
204
|
+
// Run both queries in parallel for better performance
|
|
205
|
+
await Promise.all([
|
|
206
|
+
// Delete old sessions using indexed lookup on sess->'user'->>'id'
|
|
207
|
+
dblogin.query({
|
|
208
|
+
name: 'login-delete-old-user-sessions',
|
|
209
|
+
text: 'DELETE FROM "session" WHERE (sess->\'user\'->>\'id\')::int = $1',
|
|
210
|
+
values: [user.id]
|
|
211
|
+
}),
|
|
212
|
+
// Update session ID in Users table
|
|
213
|
+
dblogin.query({
|
|
214
|
+
name: 'login-update-session-id',
|
|
215
|
+
text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`,
|
|
216
|
+
values: [sessionId, user.id]
|
|
217
|
+
})
|
|
218
|
+
]);
|
|
212
219
|
|
|
213
220
|
req.session.user = {
|
|
214
221
|
id: user.id,
|
|
@@ -217,6 +224,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
217
224
|
role: user.role || user.Role,
|
|
218
225
|
Role: user.role || user.Role,
|
|
219
226
|
sessionId,
|
|
227
|
+
allowedApps: user.allowedApps || user.AllowedApps,
|
|
220
228
|
};
|
|
221
229
|
|
|
222
230
|
if (req.session.preAuthUser) {
|
|
@@ -231,8 +239,33 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
231
239
|
// avoid writing back into the session table here to reduce DB writes;
|
|
232
240
|
// the pg session store will already persist the session data.
|
|
233
241
|
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
res.cookie("sessionId", sessionId, cachedCookieOptions);
|
|
243
|
+
|
|
244
|
+
// Handle trusted device if requested
|
|
245
|
+
if (trustDevice) {
|
|
246
|
+
try {
|
|
247
|
+
const deviceToken = generateDeviceToken();
|
|
248
|
+
const deviceName = req.headers['user-agent'] ?
|
|
249
|
+
req.headers['user-agent'].substring(0, 255) : 'Unknown Device';
|
|
250
|
+
const userAgent = req.headers['user-agent'] || 'Unknown';
|
|
251
|
+
const ipAddress = req.ip || req.connection.remoteAddress || 'Unknown';
|
|
252
|
+
const expiresAt = new Date(Date.now() + DEVICE_TRUST_DURATION_MS);
|
|
253
|
+
|
|
254
|
+
await dblogin.query({
|
|
255
|
+
name: 'insert-trusted-device',
|
|
256
|
+
text: `INSERT INTO "TrustedDevices" ("UserName", "DeviceToken", "DeviceName", "UserAgent", "IpAddress", "ExpiresAt")
|
|
257
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
258
|
+
values: [username, deviceToken, deviceName, userAgent, ipAddress, expiresAt]
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
res.cookie("device_token", deviceToken, getDeviceTokenCookieOptions());
|
|
262
|
+
console.log(`[mbkauthe] Trusted device token created for user: ${username}`);
|
|
263
|
+
} catch (deviceErr) {
|
|
264
|
+
console.error("[mbkauthe] Error creating trusted device:", deviceErr);
|
|
265
|
+
// Continue with login even if device trust fails
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
236
269
|
console.log(`[mbkauthe] User "${username}" logged in successfully`);
|
|
237
270
|
|
|
238
271
|
const responsePayload = {
|
|
@@ -255,11 +288,10 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
255
288
|
|
|
256
289
|
router.use(async (req, res, next) => {
|
|
257
290
|
if (req.session && req.session.user) {
|
|
258
|
-
const cookieOptions = getCookieOptions();
|
|
259
291
|
// Only set cookies if they're missing or different
|
|
260
292
|
if (req.cookies.sessionId !== req.session.user.sessionId) {
|
|
261
|
-
res.cookie("username", req.session.user.username, { ...
|
|
262
|
-
res.cookie("sessionId", req.session.user.sessionId,
|
|
293
|
+
res.cookie("username", req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
|
|
294
|
+
res.cookie("sessionId", req.session.user.sessionId, cachedCookieOptions);
|
|
263
295
|
}
|
|
264
296
|
}
|
|
265
297
|
next();
|
|
@@ -267,8 +299,11 @@ router.use(async (req, res, next) => {
|
|
|
267
299
|
|
|
268
300
|
router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
269
301
|
try {
|
|
270
|
-
|
|
271
|
-
await
|
|
302
|
+
// Run both operations in parallel for better performance
|
|
303
|
+
await Promise.all([
|
|
304
|
+
dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` }),
|
|
305
|
+
dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' })
|
|
306
|
+
]);
|
|
272
307
|
|
|
273
308
|
req.session.destroy((err) => {
|
|
274
309
|
if (err) {
|
|
@@ -276,10 +311,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
|
|
|
276
311
|
return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
|
|
277
312
|
}
|
|
278
313
|
|
|
279
|
-
|
|
280
|
-
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
281
|
-
res.clearCookie("sessionId", cookieOptions);
|
|
282
|
-
res.clearCookie("username", cookieOptions);
|
|
314
|
+
clearSessionCookies(res);
|
|
283
315
|
|
|
284
316
|
console.log("[mbkauthe] All sessions terminated successfully");
|
|
285
317
|
res.status(200).json({
|
|
@@ -330,7 +362,84 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
330
362
|
const trimmedUsername = username.trim();
|
|
331
363
|
|
|
332
364
|
try {
|
|
333
|
-
|
|
365
|
+
// Check for trusted device token first
|
|
366
|
+
const deviceToken = req.cookies.device_token;
|
|
367
|
+
if (deviceToken && typeof deviceToken === 'string') {
|
|
368
|
+
try {
|
|
369
|
+
const deviceQuery = `
|
|
370
|
+
SELECT td."UserName", td."LastUsed", td."ExpiresAt", u."id", u."Password", u."Active", u."Role", u."AllowedApps"
|
|
371
|
+
FROM "TrustedDevices" td
|
|
372
|
+
JOIN "Users" u ON td."UserName" = u."UserName"
|
|
373
|
+
WHERE td."DeviceToken" = $1 AND td."UserName" = $2 AND td."ExpiresAt" > NOW()
|
|
374
|
+
`;
|
|
375
|
+
const deviceResult = await dblogin.query({
|
|
376
|
+
name: 'login-check-trusted-device',
|
|
377
|
+
text: deviceQuery,
|
|
378
|
+
values: [deviceToken, trimmedUsername]
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (deviceResult.rows.length > 0) {
|
|
382
|
+
const deviceUser = deviceResult.rows[0];
|
|
383
|
+
|
|
384
|
+
// Validate password even with trusted device
|
|
385
|
+
let passwordValid = false;
|
|
386
|
+
if (mbkautheVar.EncryptedPassword === "true") {
|
|
387
|
+
passwordValid = await bcrypt.compare(password, deviceUser.Password);
|
|
388
|
+
} else {
|
|
389
|
+
passwordValid = deviceUser.Password === password;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!passwordValid) {
|
|
393
|
+
console.log("[mbkauthe] Login failed: invalid credentials (trusted device)");
|
|
394
|
+
return res.status(401).json({ success: false, message: "Invalid credentials" });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!deviceUser.Active) {
|
|
398
|
+
console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
|
|
399
|
+
return res.status(403).json({ success: false, message: "Account is inactive" });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (deviceUser.Role !== "SuperAdmin") {
|
|
403
|
+
const allowedApps = deviceUser.AllowedApps;
|
|
404
|
+
if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
|
|
405
|
+
console.warn(`[mbkauthe] User \"${trimmedUsername}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
|
|
406
|
+
return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Update last used timestamp
|
|
411
|
+
await dblogin.query({
|
|
412
|
+
name: 'login-update-device-last-used',
|
|
413
|
+
text: 'UPDATE "TrustedDevices" SET "LastUsed" = NOW() WHERE "DeviceToken" = $1',
|
|
414
|
+
values: [deviceToken]
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
console.log(`[mbkauthe] Trusted device login for user: ${trimmedUsername}, skipping 2FA`);
|
|
418
|
+
|
|
419
|
+
// Skip 2FA and complete login
|
|
420
|
+
const userForSession = {
|
|
421
|
+
id: deviceUser.id,
|
|
422
|
+
username: trimmedUsername,
|
|
423
|
+
role: deviceUser.Role,
|
|
424
|
+
Role: deviceUser.Role,
|
|
425
|
+
allowedApps: deviceUser.AllowedApps,
|
|
426
|
+
};
|
|
427
|
+
return await completeLoginProcess(req, res, userForSession);
|
|
428
|
+
}
|
|
429
|
+
} catch (deviceErr) {
|
|
430
|
+
console.error("[mbkauthe] Error checking trusted device:", deviceErr);
|
|
431
|
+
// Continue with normal login flow if device check fails
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Combined query: fetch user data and 2FA status in one query
|
|
436
|
+
const userQuery = `
|
|
437
|
+
SELECT u.id, u."UserName", u."Password", u."Active", u."Role", u."AllowedApps",
|
|
438
|
+
tfa."TwoFAStatus"
|
|
439
|
+
FROM "Users" u
|
|
440
|
+
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
441
|
+
WHERE u."UserName" = $1
|
|
442
|
+
`;
|
|
334
443
|
const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
|
|
335
444
|
|
|
336
445
|
if (userResult.rows.length === 0) {
|
|
@@ -378,21 +487,16 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
378
487
|
}
|
|
379
488
|
}
|
|
380
489
|
|
|
381
|
-
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
Role: user.Role,
|
|
392
|
-
};
|
|
393
|
-
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
394
|
-
return res.json({ success: true, twoFactorRequired: true });
|
|
395
|
-
}
|
|
490
|
+
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
|
|
491
|
+
// 2FA is enabled, prompt for token on a separate page
|
|
492
|
+
req.session.preAuthUser = {
|
|
493
|
+
id: user.id,
|
|
494
|
+
username: user.UserName,
|
|
495
|
+
role: user.Role,
|
|
496
|
+
Role: user.Role,
|
|
497
|
+
};
|
|
498
|
+
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
499
|
+
return res.json({ success: true, twoFactorRequired: true });
|
|
396
500
|
}
|
|
397
501
|
|
|
398
502
|
// If 2FA is not enabled, proceed with login
|
|
@@ -401,6 +505,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
401
505
|
username: user.UserName,
|
|
402
506
|
role: user.Role,
|
|
403
507
|
Role: user.Role,
|
|
508
|
+
allowedApps: user.AllowedApps,
|
|
404
509
|
};
|
|
405
510
|
await completeLoginProcess(req, res, userForSession);
|
|
406
511
|
|
|
@@ -416,9 +521,10 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
416
521
|
}
|
|
417
522
|
res.render("2fa.handlebars", {
|
|
418
523
|
layout: false,
|
|
419
|
-
customURL: mbkautheVar.loginRedirectURL || '/
|
|
524
|
+
customURL: mbkautheVar.loginRedirectURL || '/dashboard',
|
|
420
525
|
csrfToken: req.csrfToken(),
|
|
421
526
|
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
527
|
+
DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
|
|
422
528
|
});
|
|
423
529
|
});
|
|
424
530
|
|
|
@@ -427,7 +533,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
427
533
|
return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
|
|
428
534
|
}
|
|
429
535
|
|
|
430
|
-
const { token } = req.body;
|
|
536
|
+
const { token, trustDevice } = req.body;
|
|
431
537
|
const { username, id, role } = req.session.preAuthUser;
|
|
432
538
|
|
|
433
539
|
// Validate 2FA token
|
|
@@ -441,8 +547,11 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
441
547
|
return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
|
|
442
548
|
}
|
|
443
549
|
|
|
550
|
+
// Validate trustDevice parameter if provided
|
|
551
|
+
const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
|
|
552
|
+
|
|
444
553
|
try {
|
|
445
|
-
const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
|
|
554
|
+
const query = `SELECT tfa."TwoFASecret", u."AllowedApps" FROM "TwoFA" tfa JOIN "Users" u ON tfa."UserName" = u."UserName" WHERE tfa."UserName" = $1`;
|
|
446
555
|
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
447
556
|
|
|
448
557
|
if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
|
|
@@ -450,6 +559,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
450
559
|
}
|
|
451
560
|
|
|
452
561
|
const sharedSecret = twoFAResult.rows[0].TwoFASecret;
|
|
562
|
+
const allowedApps = twoFAResult.rows[0].AllowedApps;
|
|
453
563
|
const tokenValidates = speakeasy.totp.verify({
|
|
454
564
|
secret: sharedSecret,
|
|
455
565
|
encoding: "base32",
|
|
@@ -462,10 +572,10 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
462
572
|
return res.status(401).json({ success: false, message: "Invalid 2FA code" });
|
|
463
573
|
}
|
|
464
574
|
|
|
465
|
-
// 2FA successful, complete login
|
|
466
|
-
const userForSession = { id, username, role };
|
|
467
|
-
const redirectUrl = mbkautheVar.loginRedirectURL || '/
|
|
468
|
-
await completeLoginProcess(req, res, userForSession, redirectUrl);
|
|
575
|
+
// 2FA successful, complete login with optional device trust
|
|
576
|
+
const userForSession = { id, username, role, allowedApps };
|
|
577
|
+
const redirectUrl = mbkautheVar.loginRedirectURL || '/dashboard';
|
|
578
|
+
await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
|
|
469
579
|
|
|
470
580
|
} catch (err) {
|
|
471
581
|
console.error("[mbkauthe] Error during 2FA verification:", err);
|
|
@@ -478,11 +588,18 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
|
478
588
|
try {
|
|
479
589
|
const { id, username } = req.session.user;
|
|
480
590
|
|
|
481
|
-
|
|
482
|
-
|
|
591
|
+
// Run both database operations in parallel
|
|
592
|
+
const operations = [
|
|
593
|
+
dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
|
|
594
|
+
];
|
|
595
|
+
|
|
483
596
|
if (req.sessionID) {
|
|
484
|
-
|
|
597
|
+
operations.push(
|
|
598
|
+
dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
|
|
599
|
+
);
|
|
485
600
|
}
|
|
601
|
+
|
|
602
|
+
await Promise.all(operations);
|
|
486
603
|
|
|
487
604
|
req.session.destroy((err) => {
|
|
488
605
|
if (err) {
|
|
@@ -490,10 +607,7 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
|
490
607
|
return res.status(500).json({ success: false, message: "Logout failed" });
|
|
491
608
|
}
|
|
492
609
|
|
|
493
|
-
|
|
494
|
-
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
495
|
-
res.clearCookie("sessionId", cookieOptions);
|
|
496
|
-
res.clearCookie("username", cookieOptions);
|
|
610
|
+
clearSessionCookies(res);
|
|
497
611
|
|
|
498
612
|
console.log(`[mbkauthe] User "${username}" logged out successfully`);
|
|
499
613
|
res.status(200).json({ success: true, message: "Logout successful" });
|
|
@@ -511,7 +625,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
|
|
|
511
625
|
return res.render("loginmbkauthe.handlebars", {
|
|
512
626
|
layout: false,
|
|
513
627
|
githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
|
|
514
|
-
customURL: mbkautheVar.loginRedirectURL || '/
|
|
628
|
+
customURL: mbkautheVar.loginRedirectURL || '/dashboard',
|
|
515
629
|
userLoggedIn: !!req.session?.user,
|
|
516
630
|
username: req.session?.user?.username || '',
|
|
517
631
|
version: packageJson.version,
|
|
@@ -535,10 +649,6 @@ async function getLatestVersion() {
|
|
|
535
649
|
}
|
|
536
650
|
}
|
|
537
651
|
|
|
538
|
-
router.get("/mbkauthe/bg.avif", (req, res) => {
|
|
539
|
-
res.sendFile("bg.avif", { root: path.join(process.cwd(), "public") });
|
|
540
|
-
});
|
|
541
|
-
|
|
542
652
|
router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
|
|
543
653
|
let latestVersion;
|
|
544
654
|
const parameters = req.query;
|
|
@@ -666,16 +776,13 @@ router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
|
|
|
666
776
|
passport.authenticate('github-login')(req, res, next);
|
|
667
777
|
}
|
|
668
778
|
else {
|
|
669
|
-
res.status(403).
|
|
670
|
-
layout: false,
|
|
779
|
+
return res.status(403).send(renderError(res, {
|
|
671
780
|
code: '403',
|
|
672
781
|
error: 'GitHub Login Disabled',
|
|
673
782
|
message: 'GitHub login is currently disabled. Please use your username and password to log in.',
|
|
674
783
|
page: '/mbkauthe/login',
|
|
675
784
|
pagename: 'Login',
|
|
676
|
-
|
|
677
|
-
app: mbkautheVar.APP_NAME
|
|
678
|
-
});
|
|
785
|
+
}).render());
|
|
679
786
|
}
|
|
680
787
|
});
|
|
681
788
|
|
|
@@ -693,68 +800,53 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
693
800
|
// Map error codes to user-friendly messages
|
|
694
801
|
switch (err.code) {
|
|
695
802
|
case 'GITHUB_NOT_LINKED':
|
|
696
|
-
return res.status(403).
|
|
697
|
-
layout: false,
|
|
803
|
+
return res.status(403).send(renderError(res, {
|
|
698
804
|
code: '403',
|
|
699
805
|
error: 'GitHub Account Not Linked',
|
|
700
806
|
message: 'Your GitHub account is not linked to any user in our system. To link your GitHub account, a User must connect their GitHub account to mbktech account through the user settings.',
|
|
701
807
|
page: '/mbkauthe/login',
|
|
702
|
-
pagename: 'Login'
|
|
703
|
-
|
|
704
|
-
app: mbkautheVar.APP_NAME
|
|
705
|
-
});
|
|
808
|
+
pagename: 'Login'
|
|
809
|
+
}).render());
|
|
706
810
|
|
|
707
811
|
case 'ACCOUNT_INACTIVE':
|
|
708
|
-
return res.status(403).
|
|
709
|
-
layout: false,
|
|
812
|
+
return res.status(403).send(renderError(res, {
|
|
710
813
|
code: '403',
|
|
711
814
|
error: 'Account Inactive',
|
|
712
815
|
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
713
816
|
page: '/mbkauthe/login',
|
|
714
|
-
pagename: 'Login'
|
|
715
|
-
|
|
716
|
-
app: mbkautheVar.APP_NAME
|
|
717
|
-
});
|
|
817
|
+
pagename: 'Login'
|
|
818
|
+
}).render());
|
|
718
819
|
|
|
719
820
|
case 'NOT_AUTHORIZED':
|
|
720
|
-
return res.status(403).
|
|
721
|
-
layout: false,
|
|
821
|
+
return res.status(403).send(renderError(res, {
|
|
722
822
|
code: '403',
|
|
723
823
|
error: 'Not Authorized',
|
|
724
824
|
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
725
825
|
page: '/mbkauthe/login',
|
|
726
|
-
pagename: 'Login'
|
|
727
|
-
|
|
728
|
-
app: mbkautheVar.APP_NAME
|
|
729
|
-
});
|
|
826
|
+
pagename: 'Login'
|
|
827
|
+
}).render());
|
|
730
828
|
|
|
731
829
|
default:
|
|
732
|
-
return res.status(500).
|
|
733
|
-
layout: false,
|
|
830
|
+
return res.status(500).send(renderError(res, {
|
|
734
831
|
code: '500',
|
|
735
832
|
error: 'Authentication Error',
|
|
736
833
|
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
737
834
|
page: '/mbkauthe/login',
|
|
738
835
|
pagename: 'Login',
|
|
739
|
-
version: packageJson.version,
|
|
740
|
-
app: mbkautheVar.APP_NAME,
|
|
741
836
|
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
742
|
-
});
|
|
837
|
+
}).render());
|
|
743
838
|
}
|
|
744
839
|
}
|
|
745
840
|
|
|
746
841
|
if (!user) {
|
|
747
842
|
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
748
|
-
return res.status(401).
|
|
749
|
-
layout: false,
|
|
843
|
+
return res.status(401).send(renderError(res, {
|
|
750
844
|
code: '401',
|
|
751
845
|
error: 'Authentication Failed',
|
|
752
846
|
message: 'GitHub authentication failed. Please try again.',
|
|
753
847
|
page: '/mbkauthe/login',
|
|
754
|
-
pagename: 'Login'
|
|
755
|
-
|
|
756
|
-
app: mbkautheVar.APP_NAME
|
|
757
|
-
});
|
|
848
|
+
pagename: 'Login'
|
|
849
|
+
}).render());
|
|
758
850
|
}
|
|
759
851
|
|
|
760
852
|
// Authentication successful, attach user to request
|
|
@@ -766,8 +858,14 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
766
858
|
try {
|
|
767
859
|
const githubUser = req.user;
|
|
768
860
|
|
|
769
|
-
//
|
|
770
|
-
const userQuery = `
|
|
861
|
+
// Combined query: fetch user data and 2FA status in one query
|
|
862
|
+
const userQuery = `
|
|
863
|
+
SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
|
|
864
|
+
tfa."TwoFAStatus"
|
|
865
|
+
FROM "Users" u
|
|
866
|
+
LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
|
|
867
|
+
WHERE u."UserName" = $1
|
|
868
|
+
`;
|
|
771
869
|
const userResult = await dblogin.query({
|
|
772
870
|
name: 'github-callback-get-user',
|
|
773
871
|
text: userQuery,
|
|
@@ -776,43 +874,31 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
776
874
|
|
|
777
875
|
if (userResult.rows.length === 0) {
|
|
778
876
|
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
779
|
-
return res.status(404).
|
|
780
|
-
layout: false,
|
|
877
|
+
return res.status(404).send(renderError(res, {
|
|
781
878
|
code: '404',
|
|
782
879
|
error: 'User Not Found',
|
|
783
880
|
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
784
881
|
page: '/mbkauthe/login',
|
|
785
882
|
pagename: 'Login',
|
|
786
|
-
version: packageJson.version,
|
|
787
|
-
app: mbkautheVar.APP_NAME,
|
|
788
883
|
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
789
|
-
});
|
|
884
|
+
}).render());
|
|
790
885
|
}
|
|
791
886
|
|
|
792
887
|
const user = userResult.rows[0];
|
|
793
888
|
|
|
794
889
|
// Check 2FA if enabled
|
|
795
|
-
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
username: user.UserName,
|
|
808
|
-
UserName: user.UserName,
|
|
809
|
-
role: user.Role,
|
|
810
|
-
Role: user.Role,
|
|
811
|
-
loginMethod: 'github'
|
|
812
|
-
};
|
|
813
|
-
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
814
|
-
return res.redirect('/mbkauthe/2fa');
|
|
815
|
-
}
|
|
890
|
+
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
|
|
891
|
+
// 2FA is enabled, store pre-auth user and redirect to 2FA
|
|
892
|
+
req.session.preAuthUser = {
|
|
893
|
+
id: user.id,
|
|
894
|
+
username: user.UserName,
|
|
895
|
+
UserName: user.UserName,
|
|
896
|
+
role: user.Role,
|
|
897
|
+
Role: user.Role,
|
|
898
|
+
loginMethod: 'github'
|
|
899
|
+
};
|
|
900
|
+
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
901
|
+
return res.redirect('/mbkauthe/2fa');
|
|
816
902
|
}
|
|
817
903
|
|
|
818
904
|
// Complete login process using the shared function
|
|
@@ -821,7 +907,8 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
821
907
|
username: user.UserName,
|
|
822
908
|
UserName: user.UserName,
|
|
823
909
|
role: user.Role,
|
|
824
|
-
Role: user.Role
|
|
910
|
+
Role: user.Role,
|
|
911
|
+
allowedApps: user.AllowedApps,
|
|
825
912
|
};
|
|
826
913
|
|
|
827
914
|
// For OAuth redirect flow, we need to handle redirect differently
|
|
@@ -842,7 +929,7 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
842
929
|
res.json = function (data) {
|
|
843
930
|
if (data.success && statusCode === 200) {
|
|
844
931
|
// If login successful, redirect instead of sending JSON
|
|
845
|
-
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/
|
|
932
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
846
933
|
console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
|
|
847
934
|
// Restore original methods before redirect
|
|
848
935
|
res.json = originalJson;
|
|
@@ -859,17 +946,14 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
859
946
|
|
|
860
947
|
} catch (err) {
|
|
861
948
|
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
862
|
-
return res.status(500).
|
|
863
|
-
layout: false,
|
|
949
|
+
return res.status(500).send(renderError(res, {
|
|
864
950
|
code: '500',
|
|
865
951
|
error: 'Internal Server Error',
|
|
866
952
|
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
867
953
|
page: '/mbkauthe/login',
|
|
868
954
|
pagename: 'Login',
|
|
869
|
-
version: packageJson.version,
|
|
870
|
-
app: mbkautheVar.APP_NAME,
|
|
871
955
|
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
872
|
-
});
|
|
956
|
+
}).render());
|
|
873
957
|
}
|
|
874
958
|
}
|
|
875
959
|
);
|