mbkauthe 2.1.0 → 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 +234 -161
- package/lib/pool.js +1 -25
- 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,20 +14,13 @@ import speakeasy from "speakeasy";
|
|
|
14
14
|
import passport from 'passport';
|
|
15
15
|
import GitHubStrategy from 'passport-github2';
|
|
16
16
|
|
|
17
|
-
import { createRequire } from "module";
|
|
18
17
|
import { fileURLToPath } from "url";
|
|
19
18
|
import fs from "fs";
|
|
20
19
|
import path from "path";
|
|
21
|
-
|
|
22
|
-
import dotenv from "dotenv";
|
|
23
|
-
dotenv.config();
|
|
24
|
-
const mbkautheVar = JSON.parse(process.env.mbkautheVar);
|
|
20
|
+
import { mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS } from "./config.js";
|
|
25
21
|
|
|
26
22
|
const router = express.Router();
|
|
27
23
|
|
|
28
|
-
const require = createRequire(import.meta.url);
|
|
29
|
-
const packageJson = require("../package.json");
|
|
30
|
-
|
|
31
24
|
router.use(express.json());
|
|
32
25
|
router.use(express.urlencoded({ extended: true }));
|
|
33
26
|
router.use(cookieParser());
|
|
@@ -35,16 +28,27 @@ router.use(cookieParser());
|
|
|
35
28
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
29
|
|
|
37
30
|
router.get('/mbkauthe/main.js', (req, res) => {
|
|
31
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
38
32
|
res.sendFile(path.join(__dirname, '..', 'public', 'main.js'));
|
|
39
33
|
});
|
|
40
34
|
|
|
41
|
-
router.get(
|
|
42
|
-
|
|
43
|
-
res.
|
|
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');
|
|
44
48
|
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
45
49
|
const stream = fs.createReadStream(imgPath);
|
|
46
50
|
stream.on('error', (err) => {
|
|
47
|
-
console.error('[mbkauthe] Error streaming bg.
|
|
51
|
+
console.error('[mbkauthe] Error streaming bg.webp:', err);
|
|
48
52
|
res.status(404).send('Image not found');
|
|
49
53
|
});
|
|
50
54
|
stream.pipe(res);
|
|
@@ -128,14 +132,16 @@ router.use(session(sessionConfig));
|
|
|
128
132
|
router.use(async (req, res, next) => {
|
|
129
133
|
// Only restore session if not already present and sessionId cookie exists
|
|
130
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
|
+
|
|
131
144
|
try {
|
|
132
|
-
const sessionId = req.cookies.sessionId;
|
|
133
|
-
|
|
134
|
-
// Validate sessionId format (should be 64 hex characters) and normalize to lowercase
|
|
135
|
-
if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
|
|
136
|
-
console.warn("[mbkauthe] Invalid sessionId format detected");
|
|
137
|
-
return next();
|
|
138
|
-
}
|
|
139
145
|
|
|
140
146
|
const normalizedSessionId = sessionId.toLowerCase();
|
|
141
147
|
|
|
@@ -151,6 +157,7 @@ router.use(async (req, res, next) => {
|
|
|
151
157
|
role: user.Role,
|
|
152
158
|
Role: user.Role,
|
|
153
159
|
sessionId: normalizedSessionId,
|
|
160
|
+
allowedApps: user.AllowedApps,
|
|
154
161
|
};
|
|
155
162
|
}
|
|
156
163
|
} catch (err) {
|
|
@@ -160,23 +167,6 @@ router.use(async (req, res, next) => {
|
|
|
160
167
|
next();
|
|
161
168
|
});
|
|
162
169
|
|
|
163
|
-
const getCookieOptions = () => ({
|
|
164
|
-
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
|
|
165
|
-
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
166
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
167
|
-
sameSite: 'lax',
|
|
168
|
-
path: '/',
|
|
169
|
-
httpOnly: true
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const getClearCookieOptions = () => ({
|
|
173
|
-
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
|
|
174
|
-
secure: mbkautheVar.IS_DEPLOYED === 'true',
|
|
175
|
-
sameSite: 'lax',
|
|
176
|
-
path: '/',
|
|
177
|
-
httpOnly: true
|
|
178
|
-
});
|
|
179
|
-
|
|
180
170
|
router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
|
|
181
171
|
if (req.session?.user) {
|
|
182
172
|
return res.send(`
|
|
@@ -190,7 +180,7 @@ router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
|
|
|
190
180
|
}
|
|
191
181
|
});
|
|
192
182
|
|
|
193
|
-
async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
183
|
+
async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
|
|
194
184
|
try {
|
|
195
185
|
// Ensure both username formats are available for compatibility
|
|
196
186
|
const username = user.username || user.UserName;
|
|
@@ -211,19 +201,21 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
211
201
|
});
|
|
212
202
|
});
|
|
213
203
|
|
|
214
|
-
//
|
|
215
|
-
await
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
]);
|
|
227
219
|
|
|
228
220
|
req.session.user = {
|
|
229
221
|
id: user.id,
|
|
@@ -232,6 +224,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
232
224
|
role: user.role || user.Role,
|
|
233
225
|
Role: user.role || user.Role,
|
|
234
226
|
sessionId,
|
|
227
|
+
allowedApps: user.allowedApps || user.AllowedApps,
|
|
235
228
|
};
|
|
236
229
|
|
|
237
230
|
if (req.session.preAuthUser) {
|
|
@@ -246,8 +239,33 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
246
239
|
// avoid writing back into the session table here to reduce DB writes;
|
|
247
240
|
// the pg session store will already persist the session data.
|
|
248
241
|
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
|
|
251
269
|
console.log(`[mbkauthe] User "${username}" logged in successfully`);
|
|
252
270
|
|
|
253
271
|
const responsePayload = {
|
|
@@ -270,11 +288,10 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
|
|
|
270
288
|
|
|
271
289
|
router.use(async (req, res, next) => {
|
|
272
290
|
if (req.session && req.session.user) {
|
|
273
|
-
const cookieOptions = getCookieOptions();
|
|
274
291
|
// Only set cookies if they're missing or different
|
|
275
292
|
if (req.cookies.sessionId !== req.session.user.sessionId) {
|
|
276
|
-
res.cookie("username", req.session.user.username, { ...
|
|
277
|
-
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);
|
|
278
295
|
}
|
|
279
296
|
}
|
|
280
297
|
next();
|
|
@@ -282,8 +299,11 @@ router.use(async (req, res, next) => {
|
|
|
282
299
|
|
|
283
300
|
router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
|
|
284
301
|
try {
|
|
285
|
-
|
|
286
|
-
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
|
+
]);
|
|
287
307
|
|
|
288
308
|
req.session.destroy((err) => {
|
|
289
309
|
if (err) {
|
|
@@ -291,10 +311,7 @@ router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_
|
|
|
291
311
|
return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
|
|
292
312
|
}
|
|
293
313
|
|
|
294
|
-
|
|
295
|
-
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
296
|
-
res.clearCookie("sessionId", cookieOptions);
|
|
297
|
-
res.clearCookie("username", cookieOptions);
|
|
314
|
+
clearSessionCookies(res);
|
|
298
315
|
|
|
299
316
|
console.log("[mbkauthe] All sessions terminated successfully");
|
|
300
317
|
res.status(200).json({
|
|
@@ -345,7 +362,84 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
345
362
|
const trimmedUsername = username.trim();
|
|
346
363
|
|
|
347
364
|
try {
|
|
348
|
-
|
|
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
|
+
`;
|
|
349
443
|
const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
|
|
350
444
|
|
|
351
445
|
if (userResult.rows.length === 0) {
|
|
@@ -393,21 +487,16 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
393
487
|
}
|
|
394
488
|
}
|
|
395
489
|
|
|
396
|
-
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
Role: user.Role,
|
|
407
|
-
};
|
|
408
|
-
console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
|
|
409
|
-
return res.json({ success: true, twoFactorRequired: true });
|
|
410
|
-
}
|
|
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 });
|
|
411
500
|
}
|
|
412
501
|
|
|
413
502
|
// If 2FA is not enabled, proceed with login
|
|
@@ -416,6 +505,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
|
|
|
416
505
|
username: user.UserName,
|
|
417
506
|
role: user.Role,
|
|
418
507
|
Role: user.Role,
|
|
508
|
+
allowedApps: user.AllowedApps,
|
|
419
509
|
};
|
|
420
510
|
await completeLoginProcess(req, res, userForSession);
|
|
421
511
|
|
|
@@ -431,9 +521,10 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
|
|
|
431
521
|
}
|
|
432
522
|
res.render("2fa.handlebars", {
|
|
433
523
|
layout: false,
|
|
434
|
-
customURL: mbkautheVar.loginRedirectURL || '/
|
|
524
|
+
customURL: mbkautheVar.loginRedirectURL || '/dashboard',
|
|
435
525
|
csrfToken: req.csrfToken(),
|
|
436
526
|
appName: mbkautheVar.APP_NAME.toLowerCase(),
|
|
527
|
+
DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
|
|
437
528
|
});
|
|
438
529
|
});
|
|
439
530
|
|
|
@@ -442,7 +533,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
442
533
|
return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
|
|
443
534
|
}
|
|
444
535
|
|
|
445
|
-
const { token } = req.body;
|
|
536
|
+
const { token, trustDevice } = req.body;
|
|
446
537
|
const { username, id, role } = req.session.preAuthUser;
|
|
447
538
|
|
|
448
539
|
// Validate 2FA token
|
|
@@ -456,8 +547,11 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
456
547
|
return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
|
|
457
548
|
}
|
|
458
549
|
|
|
550
|
+
// Validate trustDevice parameter if provided
|
|
551
|
+
const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
|
|
552
|
+
|
|
459
553
|
try {
|
|
460
|
-
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`;
|
|
461
555
|
const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
|
|
462
556
|
|
|
463
557
|
if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
|
|
@@ -465,6 +559,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
465
559
|
}
|
|
466
560
|
|
|
467
561
|
const sharedSecret = twoFAResult.rows[0].TwoFASecret;
|
|
562
|
+
const allowedApps = twoFAResult.rows[0].AllowedApps;
|
|
468
563
|
const tokenValidates = speakeasy.totp.verify({
|
|
469
564
|
secret: sharedSecret,
|
|
470
565
|
encoding: "base32",
|
|
@@ -477,10 +572,10 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
|
|
|
477
572
|
return res.status(401).json({ success: false, message: "Invalid 2FA code" });
|
|
478
573
|
}
|
|
479
574
|
|
|
480
|
-
// 2FA successful, complete login
|
|
481
|
-
const userForSession = { id, username, role };
|
|
482
|
-
const redirectUrl = mbkautheVar.loginRedirectURL || '/
|
|
483
|
-
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);
|
|
484
579
|
|
|
485
580
|
} catch (err) {
|
|
486
581
|
console.error("[mbkauthe] Error during 2FA verification:", err);
|
|
@@ -493,11 +588,18 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
|
493
588
|
try {
|
|
494
589
|
const { id, username } = req.session.user;
|
|
495
590
|
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
|
|
498
596
|
if (req.sessionID) {
|
|
499
|
-
|
|
597
|
+
operations.push(
|
|
598
|
+
dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
|
|
599
|
+
);
|
|
500
600
|
}
|
|
601
|
+
|
|
602
|
+
await Promise.all(operations);
|
|
501
603
|
|
|
502
604
|
req.session.destroy((err) => {
|
|
503
605
|
if (err) {
|
|
@@ -505,10 +607,7 @@ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
|
|
|
505
607
|
return res.status(500).json({ success: false, message: "Logout failed" });
|
|
506
608
|
}
|
|
507
609
|
|
|
508
|
-
|
|
509
|
-
res.clearCookie("mbkauthe.sid", cookieOptions);
|
|
510
|
-
res.clearCookie("sessionId", cookieOptions);
|
|
511
|
-
res.clearCookie("username", cookieOptions);
|
|
610
|
+
clearSessionCookies(res);
|
|
512
611
|
|
|
513
612
|
console.log(`[mbkauthe] User "${username}" logged out successfully`);
|
|
514
613
|
res.status(200).json({ success: true, message: "Logout successful" });
|
|
@@ -526,7 +625,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
|
|
|
526
625
|
return res.render("loginmbkauthe.handlebars", {
|
|
527
626
|
layout: false,
|
|
528
627
|
githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
|
|
529
|
-
customURL: mbkautheVar.loginRedirectURL || '/
|
|
628
|
+
customURL: mbkautheVar.loginRedirectURL || '/dashboard',
|
|
530
629
|
userLoggedIn: !!req.session?.user,
|
|
531
630
|
username: req.session?.user?.username || '',
|
|
532
631
|
version: packageJson.version,
|
|
@@ -677,16 +776,13 @@ router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
|
|
|
677
776
|
passport.authenticate('github-login')(req, res, next);
|
|
678
777
|
}
|
|
679
778
|
else {
|
|
680
|
-
res.status(403).
|
|
681
|
-
layout: false,
|
|
779
|
+
return res.status(403).send(renderError(res, {
|
|
682
780
|
code: '403',
|
|
683
781
|
error: 'GitHub Login Disabled',
|
|
684
782
|
message: 'GitHub login is currently disabled. Please use your username and password to log in.',
|
|
685
783
|
page: '/mbkauthe/login',
|
|
686
784
|
pagename: 'Login',
|
|
687
|
-
|
|
688
|
-
app: mbkautheVar.APP_NAME
|
|
689
|
-
});
|
|
785
|
+
}).render());
|
|
690
786
|
}
|
|
691
787
|
});
|
|
692
788
|
|
|
@@ -704,68 +800,53 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
704
800
|
// Map error codes to user-friendly messages
|
|
705
801
|
switch (err.code) {
|
|
706
802
|
case 'GITHUB_NOT_LINKED':
|
|
707
|
-
return res.status(403).
|
|
708
|
-
layout: false,
|
|
803
|
+
return res.status(403).send(renderError(res, {
|
|
709
804
|
code: '403',
|
|
710
805
|
error: 'GitHub Account Not Linked',
|
|
711
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.',
|
|
712
807
|
page: '/mbkauthe/login',
|
|
713
|
-
pagename: 'Login'
|
|
714
|
-
|
|
715
|
-
app: mbkautheVar.APP_NAME
|
|
716
|
-
});
|
|
808
|
+
pagename: 'Login'
|
|
809
|
+
}).render());
|
|
717
810
|
|
|
718
811
|
case 'ACCOUNT_INACTIVE':
|
|
719
|
-
return res.status(403).
|
|
720
|
-
layout: false,
|
|
812
|
+
return res.status(403).send(renderError(res, {
|
|
721
813
|
code: '403',
|
|
722
814
|
error: 'Account Inactive',
|
|
723
815
|
message: 'Your account has been deactivated. Please contact your administrator.',
|
|
724
816
|
page: '/mbkauthe/login',
|
|
725
|
-
pagename: 'Login'
|
|
726
|
-
|
|
727
|
-
app: mbkautheVar.APP_NAME
|
|
728
|
-
});
|
|
817
|
+
pagename: 'Login'
|
|
818
|
+
}).render());
|
|
729
819
|
|
|
730
820
|
case 'NOT_AUTHORIZED':
|
|
731
|
-
return res.status(403).
|
|
732
|
-
layout: false,
|
|
821
|
+
return res.status(403).send(renderError(res, {
|
|
733
822
|
code: '403',
|
|
734
823
|
error: 'Not Authorized',
|
|
735
824
|
message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
|
|
736
825
|
page: '/mbkauthe/login',
|
|
737
|
-
pagename: 'Login'
|
|
738
|
-
|
|
739
|
-
app: mbkautheVar.APP_NAME
|
|
740
|
-
});
|
|
826
|
+
pagename: 'Login'
|
|
827
|
+
}).render());
|
|
741
828
|
|
|
742
829
|
default:
|
|
743
|
-
return res.status(500).
|
|
744
|
-
layout: false,
|
|
830
|
+
return res.status(500).send(renderError(res, {
|
|
745
831
|
code: '500',
|
|
746
832
|
error: 'Authentication Error',
|
|
747
833
|
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
748
834
|
page: '/mbkauthe/login',
|
|
749
835
|
pagename: 'Login',
|
|
750
|
-
version: packageJson.version,
|
|
751
|
-
app: mbkautheVar.APP_NAME,
|
|
752
836
|
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
753
|
-
});
|
|
837
|
+
}).render());
|
|
754
838
|
}
|
|
755
839
|
}
|
|
756
840
|
|
|
757
841
|
if (!user) {
|
|
758
842
|
console.error('[mbkauthe] GitHub callback: No user data received');
|
|
759
|
-
return res.status(401).
|
|
760
|
-
layout: false,
|
|
843
|
+
return res.status(401).send(renderError(res, {
|
|
761
844
|
code: '401',
|
|
762
845
|
error: 'Authentication Failed',
|
|
763
846
|
message: 'GitHub authentication failed. Please try again.',
|
|
764
847
|
page: '/mbkauthe/login',
|
|
765
|
-
pagename: 'Login'
|
|
766
|
-
|
|
767
|
-
app: mbkautheVar.APP_NAME
|
|
768
|
-
});
|
|
848
|
+
pagename: 'Login'
|
|
849
|
+
}).render());
|
|
769
850
|
}
|
|
770
851
|
|
|
771
852
|
// Authentication successful, attach user to request
|
|
@@ -777,8 +858,14 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
777
858
|
try {
|
|
778
859
|
const githubUser = req.user;
|
|
779
860
|
|
|
780
|
-
//
|
|
781
|
-
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
|
+
`;
|
|
782
869
|
const userResult = await dblogin.query({
|
|
783
870
|
name: 'github-callback-get-user',
|
|
784
871
|
text: userQuery,
|
|
@@ -787,43 +874,31 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
787
874
|
|
|
788
875
|
if (userResult.rows.length === 0) {
|
|
789
876
|
console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
|
|
790
|
-
return res.status(404).
|
|
791
|
-
layout: false,
|
|
877
|
+
return res.status(404).send(renderError(res, {
|
|
792
878
|
code: '404',
|
|
793
879
|
error: 'User Not Found',
|
|
794
880
|
message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
|
|
795
881
|
page: '/mbkauthe/login',
|
|
796
882
|
pagename: 'Login',
|
|
797
|
-
version: packageJson.version,
|
|
798
|
-
app: mbkautheVar.APP_NAME,
|
|
799
883
|
details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
|
|
800
|
-
});
|
|
884
|
+
}).render());
|
|
801
885
|
}
|
|
802
886
|
|
|
803
887
|
const user = userResult.rows[0];
|
|
804
888
|
|
|
805
889
|
// Check 2FA if enabled
|
|
806
|
-
if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
username: user.UserName,
|
|
819
|
-
UserName: user.UserName,
|
|
820
|
-
role: user.Role,
|
|
821
|
-
Role: user.Role,
|
|
822
|
-
loginMethod: 'github'
|
|
823
|
-
};
|
|
824
|
-
console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
|
|
825
|
-
return res.redirect('/mbkauthe/2fa');
|
|
826
|
-
}
|
|
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');
|
|
827
902
|
}
|
|
828
903
|
|
|
829
904
|
// Complete login process using the shared function
|
|
@@ -832,7 +907,8 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
832
907
|
username: user.UserName,
|
|
833
908
|
UserName: user.UserName,
|
|
834
909
|
role: user.Role,
|
|
835
|
-
Role: user.Role
|
|
910
|
+
Role: user.Role,
|
|
911
|
+
allowedApps: user.AllowedApps,
|
|
836
912
|
};
|
|
837
913
|
|
|
838
914
|
// For OAuth redirect flow, we need to handle redirect differently
|
|
@@ -853,7 +929,7 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
853
929
|
res.json = function (data) {
|
|
854
930
|
if (data.success && statusCode === 200) {
|
|
855
931
|
// If login successful, redirect instead of sending JSON
|
|
856
|
-
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/
|
|
932
|
+
const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
|
|
857
933
|
console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
|
|
858
934
|
// Restore original methods before redirect
|
|
859
935
|
res.json = originalJson;
|
|
@@ -870,17 +946,14 @@ router.get('/mbkauthe/api/github/login/callback',
|
|
|
870
946
|
|
|
871
947
|
} catch (err) {
|
|
872
948
|
console.error('[mbkauthe] GitHub login callback error:', err);
|
|
873
|
-
return res.status(500).
|
|
874
|
-
layout: false,
|
|
949
|
+
return res.status(500).send(renderError(res, {
|
|
875
950
|
code: '500',
|
|
876
951
|
error: 'Internal Server Error',
|
|
877
952
|
message: 'An error occurred during GitHub authentication. Please try again.',
|
|
878
953
|
page: '/mbkauthe/login',
|
|
879
954
|
pagename: 'Login',
|
|
880
|
-
version: packageJson.version,
|
|
881
|
-
app: mbkautheVar.APP_NAME,
|
|
882
955
|
details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
|
|
883
|
-
});
|
|
956
|
+
}).render());
|
|
884
957
|
}
|
|
885
958
|
}
|
|
886
959
|
);
|