mbkauthe 2.4.0 → 3.0.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/lib/main.js CHANGED
@@ -1,988 +1,52 @@
1
1
  import express from "express";
2
- import csurf from "csurf";
3
- import crypto from "crypto";
4
2
  import session from "express-session";
5
- import pgSession from "connect-pg-simple";
6
- const PgSession = pgSession(session);
7
- import { dblogin } from "./pool.js";
8
- import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
9
- import fetch from 'node-fetch';
10
3
  import cookieParser from "cookie-parser";
11
- import bcrypt from 'bcrypt';
12
- import rateLimit from 'express-rate-limit';
13
- import speakeasy from "speakeasy";
14
4
  import passport from 'passport';
15
- import GitHubStrategy from 'passport-github2';
16
-
17
- import { fileURLToPath } from "url";
18
- import fs from "fs";
19
- import path from "path";
20
- import { mbkautheVar, cachedCookieOptions, cachedClearCookieOptions, clearSessionCookies, renderError, packageJson, generateDeviceToken, getDeviceTokenCookieOptions, DEVICE_TRUST_DURATION_MS } from "./config.js";
5
+ import { packageJson } from "./config/index.js";
6
+ import {
7
+ sessionConfig,
8
+ corsMiddleware,
9
+ sessionRestorationMiddleware,
10
+ sessionCookieSyncMiddleware
11
+ } from "./middleware/index.js";
12
+ import authRoutes from "./routes/auth.js";
13
+ import oauthRoutes from "./routes/oauth.js";
14
+ import miscRoutes from "./routes/misc.js";
21
15
 
22
16
  const router = express.Router();
23
17
 
18
+ // Basic middleware
24
19
  router.use(express.json());
25
20
  router.use(express.urlencoded({ extended: true }));
26
21
  router.use(cookieParser());
27
22
 
28
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
-
30
- router.get('/mbkauthe/main.js', (req, 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);
55
- });
56
-
57
- // CSRF protection middleware
58
- const csrfProtection = csurf({ cookie: true });
59
-
60
23
  // CORS and security headers
61
- router.use((req, res, next) => {
62
- const origin = req.headers.origin;
63
- if (origin) {
64
- try {
65
- const originUrl = new URL(origin);
66
- const allowedDomain = `.${mbkautheVar.DOMAIN}`;
67
- // Exact match or subdomain match (must end with .domain.com, not just domain.com)
68
- if (originUrl.hostname === mbkautheVar.DOMAIN ||
69
- (originUrl.hostname.endsWith(allowedDomain) && originUrl.hostname.charAt(originUrl.hostname.length - allowedDomain.length - 1) !== '.')) {
70
- res.header('Access-Control-Allow-Origin', origin);
71
- res.header('Access-Control-Allow-Credentials', 'true');
72
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
73
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
74
- }
75
- } catch (err) {
76
- // Invalid origin URL, skip CORS headers
77
- }
78
- }
79
- next();
80
- });
81
-
82
- const LoginLimit = rateLimit({
83
- windowMs: 1 * 60 * 1000,
84
- max: 8,
85
- message: { success: false, message: "Too many attempts, please try again later" },
86
- skip: (req) => {
87
- return !!req.session.user;
88
- }
89
- });
90
-
91
- const LogoutLimit = rateLimit({
92
- windowMs: 1 * 60 * 1000,
93
- max: 10,
94
- message: { success: false, message: "Too many logout attempts, please try again later" }
95
- });
96
-
97
- const TwoFALimit = rateLimit({
98
- windowMs: 1 * 60 * 1000,
99
- max: 5,
100
- message: { success: false, message: "Too many 2FA attempts, please try again later" }
101
- });
102
-
103
- const GitHubOAuthLimit = rateLimit({
104
- windowMs: 5 * 60 * 1000,
105
- max: 10,
106
- message: "Too many GitHub login attempts, please try again later"
107
- });
108
-
109
- const sessionConfig = {
110
- store: new PgSession({
111
- pool: dblogin,
112
- tableName: "session",
113
- createTableIfMissing: true
114
- }),
115
- secret: mbkautheVar.SESSION_SECRET_KEY,
116
- resave: false,
117
- saveUninitialized: false,
118
- proxy: true,
119
- cookie: {
120
- maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
121
- domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
122
- httpOnly: true,
123
- secure: mbkautheVar.IS_DEPLOYED === 'true',
124
- sameSite: 'lax',
125
- path: '/'
126
- },
127
- name: 'mbkauthe.sid'
128
- };
24
+ router.use(corsMiddleware);
129
25
 
26
+ // Session configuration
130
27
  router.use(session(sessionConfig));
131
28
 
132
- router.use(async (req, res, next) => {
133
- // Only restore session if not already present and sessionId cookie exists
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
-
144
- try {
145
-
146
- const normalizedSessionId = sessionId.toLowerCase();
147
-
148
- const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE LOWER("SessionId") = $1 AND "Active" = true`;
149
- const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [normalizedSessionId] });
150
-
151
- if (result.rows.length > 0) {
152
- const user = result.rows[0];
153
- req.session.user = {
154
- id: user.id,
155
- username: user.UserName,
156
- UserName: user.UserName,
157
- role: user.Role,
158
- Role: user.Role,
159
- sessionId: normalizedSessionId,
160
- allowedApps: user.AllowedApps,
161
- };
162
- }
163
- } catch (err) {
164
- console.error("[mbkauthe] Session restoration error:", err);
165
- }
166
- }
167
- next();
168
- });
169
-
170
- router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
171
- if (req.session?.user) {
172
- return res.send(`
173
- <head> <script src="/mbkauthe/main.js"></script> </head>
174
- <p>if you are seeing this page than User is logged in.</p>
175
- <p>id: '${req.session.user.id}', UserName: '${req.session.user.username}', Role: '${req.session.user.role}', SessionId: '${req.session.user.sessionId}'</p>
176
- <button onclick="logout()">Logout</button><br>
177
- <a href="/mbkauthe/info">Info Page</a><br>
178
- <a href="/mbkauthe/login">Login Page</a><br>
179
- `);
180
- }
181
- });
182
-
183
- async function completeLoginProcess(req, res, user, redirectUrl = null, trustDevice = false) {
184
- try {
185
- // Ensure both username formats are available for compatibility
186
- const username = user.username || user.UserName;
187
- if (!username) {
188
- throw new Error('Username is required in user object');
189
- }
190
-
191
- // smaller session id is sufficient and faster to generate/serialize
192
- const sessionId = crypto.randomBytes(32).toString("hex");
193
- console.log(`[mbkauthe] Generated session ID for username: ${username}`);
194
-
195
- // Regenerate session to prevent session fixation attacks
196
- const oldSessionId = req.sessionID;
197
- await new Promise((resolve, reject) => {
198
- req.session.regenerate((err) => {
199
- if (err) reject(err);
200
- else resolve();
201
- });
202
- });
203
-
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
- ]);
219
-
220
- req.session.user = {
221
- id: user.id,
222
- username: username,
223
- UserName: username,
224
- role: user.role || user.Role,
225
- Role: user.role || user.Role,
226
- sessionId,
227
- allowedApps: user.allowedApps || user.AllowedApps,
228
- };
229
-
230
- if (req.session.preAuthUser) {
231
- delete req.session.preAuthUser;
232
- }
233
-
234
- req.session.save(async (err) => {
235
- if (err) {
236
- console.error("[mbkauthe] Session save error:", err);
237
- return res.status(500).json({ success: false, message: "Internal Server Error" });
238
- }
239
- // avoid writing back into the session table here to reduce DB writes;
240
- // the pg session store will already persist the session data.
241
-
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
-
269
- console.log(`[mbkauthe] User "${username}" logged in successfully`);
270
-
271
- const responsePayload = {
272
- success: true,
273
- message: "Login successful",
274
- sessionId,
275
- };
276
-
277
- if (redirectUrl) {
278
- responsePayload.redirectUrl = redirectUrl;
279
- }
280
-
281
- res.status(200).json(responsePayload);
282
- });
283
- } catch (err) {
284
- console.error("[mbkauthe] Error during login completion:", err);
285
- res.status(500).json({ success: false, message: "Internal Server Error" });
286
- }
287
- }
288
-
289
- router.use(async (req, res, next) => {
290
- if (req.session && req.session.user) {
291
- // Only set cookies if they're missing or different
292
- if (req.cookies.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);
295
- }
296
- }
297
- next();
298
- });
299
-
300
- router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
301
- try {
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
- ]);
307
-
308
- req.session.destroy((err) => {
309
- if (err) {
310
- console.log("[mbkauthe] Error destroying session:", err);
311
- return res.status(500).json({ success: false, message: "Failed to terminate sessions" });
312
- }
313
-
314
- clearSessionCookies(res);
315
-
316
- console.log("[mbkauthe] All sessions terminated successfully");
317
- res.status(200).json({
318
- success: true,
319
- message: "All sessions terminated successfully",
320
- });
321
- });
322
- } catch (err) {
323
- console.error("[mbkauthe] Database query error during session termination:", err);
324
- res.status(500).json({ success: false, message: "Internal Server Error" });
325
- }
326
- });
327
-
328
- router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
329
- console.log("[mbkauthe] Login request received");
330
-
331
- const { username, password, redirect } = req.body;
332
-
333
- // Input validation
334
- if (!username || !password) {
335
- console.log("[mbkauthe] Missing username or password");
336
- return res.status(400).json({
337
- success: false,
338
- message: "Username and password are required",
339
- });
340
- }
341
-
342
- // Validate username format and length
343
- if (typeof username !== 'string' || username.trim().length === 0 || username.length > 255) {
344
- console.warn("[mbkauthe] Invalid username format");
345
- return res.status(400).json({
346
- success: false,
347
- message: "Invalid username format",
348
- });
349
- }
350
-
351
- // Validate password length
352
- if (typeof password !== 'string' || password.length < 8 || password.length > 255) {
353
- console.warn("[mbkauthe] Invalid password length");
354
- return res.status(400).json({
355
- success: false,
356
- message: "Password must be at least 8 characters long",
357
- });
358
- }
359
-
360
- console.log(`[mbkauthe] Login attempt for username: ${username.trim()}`);
361
-
362
- const trimmedUsername = username.trim();
363
-
364
- try {
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 && 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
- // If a redirect is provided, validate and pass it through
428
- const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
429
- return await completeLoginProcess(req, res, userForSession, requestedRedirect);
430
- }
431
- } catch (deviceErr) {
432
- console.error("[mbkauthe] Error checking trusted device:", deviceErr);
433
- // Continue with normal login flow if device check fails
434
- }
435
- }
436
-
437
- // Combined query: fetch user data and 2FA status in one query
438
- const userQuery = `
439
- SELECT u.id, u."UserName", u."Password", u."Active", u."Role", u."AllowedApps",
440
- tfa."TwoFAStatus"
441
- FROM "Users" u
442
- LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
443
- WHERE u."UserName" = $1
444
- `;
445
- const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
446
-
447
- if (userResult.rows.length === 0) {
448
- console.log(`[mbkauthe] Login failed: invalid credentials`);
449
- return res.status(401).json({ success: false, message: "Invalid credentials" });
450
- }
451
-
452
- const user = userResult.rows[0];
453
-
454
- // Validate user has password field
455
- if (!user.Password) {
456
- console.error("[mbkauthe] User account has no password set");
457
- return res.status(500).json({ success: false, message: "Internal Server Error" });
458
- }
459
-
460
- if (mbkautheVar.EncryptedPassword === "true") {
461
- try {
462
- const result = await bcrypt.compare(password, user.Password);
463
- if (!result) {
464
- console.log("[mbkauthe] Login failed: invalid credentials");
465
- return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
466
- }
467
- console.log("[mbkauthe] Password validated successfully");
468
- } catch (err) {
469
- console.error("[mbkauthe] Error comparing password:", err);
470
- return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
471
- }
472
- } else {
473
- if (user.Password !== password) {
474
- console.log(`[mbkauthe] Login failed: invalid credentials`);
475
- return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
476
- }
477
- }
478
-
479
- if (!user.Active) {
480
- console.log(`[mbkauthe] Inactive account for username: ${trimmedUsername}`);
481
- return res.status(403).json({ success: false, message: "Account is inactive" });
482
- }
483
-
484
- if (user.Role !== "SuperAdmin") {
485
- const allowedApps = user.AllowedApps;
486
- if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
487
- console.warn(`[mbkauthe] User \"${user.UserName}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`);
488
- return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` });
489
- }
490
- }
491
-
492
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true" && user.TwoFAStatus) {
493
- // 2FA is enabled, prompt for token on a separate page
494
- const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
495
- req.session.preAuthUser = {
496
- id: user.id,
497
- username: user.UserName,
498
- role: user.Role,
499
- Role: user.Role,
500
- redirectUrl: requestedRedirect
501
- };
502
- console.log(`[mbkauthe] 2FA required for user: ${trimmedUsername}`);
503
- return res.json({ success: true, twoFactorRequired: true, redirectUrl: requestedRedirect });
504
- }
505
-
506
- // If 2FA is not enabled, proceed with login
507
- const userForSession = {
508
- id: user.id,
509
- username: user.UserName,
510
- role: user.Role,
511
- Role: user.Role,
512
- allowedApps: user.AllowedApps,
513
- };
514
- const requestedRedirect = typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : null;
515
- await completeLoginProcess(req, res, userForSession, requestedRedirect);
516
-
517
- } catch (err) {
518
- console.error("[mbkauthe] Error during login process:", err);
519
- res.status(500).json({ success: false, message: "Internal Server Error" });
520
- }
521
- });
522
-
523
- router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
524
- if (!req.session.preAuthUser) {
525
- return res.redirect("/mbkauthe/login");
526
- }
527
-
528
- // Prefer explicit redirect from query string, else from session preAuthUser redirectUrl, else fallback value
529
- let redirectFromQuery = req.query && typeof req.query.redirect === 'string' ? req.query.redirect : null;
530
- let redirectToUse = redirectFromQuery || req.session.preAuthUser.redirectUrl || (mbkautheVar.loginRedirectURL || '/dashboard');
531
-
532
- // Validate redirectToUse to prevent open redirect attacks
533
- if (redirectToUse && !(typeof redirectToUse === 'string' && redirectToUse.startsWith('/') && !redirectToUse.startsWith('//'))) {
534
- redirectToUse = mbkautheVar.loginRedirectURL || '/dashboard';
535
- }
536
-
537
- res.render("2fa.handlebars", {
538
- layout: false,
539
- customURL: redirectToUse,
540
- csrfToken: req.csrfToken(),
541
- appName: mbkautheVar.APP_NAME.toLowerCase(),
542
- DEVICE_TRUST_DURATION_DAYS: mbkautheVar.DEVICE_TRUST_DURATION_DAYS
543
- });
544
- });
545
-
546
- router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req, res) => {
547
- if (!req.session.preAuthUser) {
548
- return res.status(401).json({ success: false, message: "Not authorized. Please login first." });
549
- }
550
-
551
- const { token, trustDevice } = req.body;
552
- const { username, id, role } = req.session.preAuthUser;
553
-
554
- // Validate 2FA token
555
- if (!token || typeof token !== 'string') {
556
- return res.status(400).json({ success: false, message: "2FA token is required" });
557
- }
558
-
559
- // Validate token format (should be 6 digits)
560
- const sanitizedToken = token.trim();
561
- if (!/^\d{6}$/.test(sanitizedToken)) {
562
- return res.status(400).json({ success: false, message: "Invalid 2FA token format" });
563
- }
564
-
565
- // Validate trustDevice parameter if provided
566
- const shouldTrustDevice = trustDevice === true || trustDevice === 'true';
567
-
568
- try {
569
- const query = `SELECT tfa."TwoFASecret", u."AllowedApps" FROM "TwoFA" tfa JOIN "Users" u ON tfa."UserName" = u."UserName" WHERE tfa."UserName" = $1`;
570
- const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
571
-
572
- if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
573
- return res.status(500).json({ success: false, message: "2FA is not configured correctly." });
574
- }
575
-
576
- const sharedSecret = twoFAResult.rows[0].TwoFASecret;
577
- const allowedApps = twoFAResult.rows[0].AllowedApps;
578
- const tokenValidates = speakeasy.totp.verify({
579
- secret: sharedSecret,
580
- encoding: "base32",
581
- token: sanitizedToken,
582
- window: 1,
583
- });
584
-
585
- if (!tokenValidates) {
586
- console.log(`[mbkauthe] Invalid 2FA code for username: ${username}`);
587
- return res.status(401).json({ success: false, message: "Invalid 2FA code" });
588
- }
589
-
590
- // 2FA successful, complete login with optional device trust
591
- const userForSession = { id, username, role, allowedApps };
592
- // Prefer redirect stored in preAuthUser or in query/body, fallback to configured default
593
- let redirectFromSession = req.session.preAuthUser && req.session.preAuthUser.redirectUrl ? req.session.preAuthUser.redirectUrl : null;
594
- if (redirectFromSession && (!(typeof redirectFromSession === 'string') || !redirectFromSession.startsWith('/') || redirectFromSession.startsWith('//'))) {
595
- redirectFromSession = null;
596
- }
597
- const redirectUrl = redirectFromSession || mbkautheVar.loginRedirectURL || '/dashboard';
598
- // Clear preAuthUser after successful login
599
- if (req.session.preAuthUser) delete req.session.preAuthUser;
600
- await completeLoginProcess(req, res, userForSession, redirectUrl, shouldTrustDevice);
601
-
602
- } catch (err) {
603
- console.error("[mbkauthe] Error during 2FA verification:", err);
604
- res.status(500).json({ success: false, message: "Internal Server Error" });
605
- }
606
- });
607
-
608
- router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
609
- if (req.session.user) {
610
- try {
611
- const { id, username } = req.session.user;
612
-
613
- // Run both database operations in parallel
614
- const operations = [
615
- dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] })
616
- ];
617
-
618
- if (req.sessionID) {
619
- operations.push(
620
- dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] })
621
- );
622
- }
623
-
624
- await Promise.all(operations);
625
-
626
- req.session.destroy((err) => {
627
- if (err) {
628
- console.error("[mbkauthe] Error destroying session:", err);
629
- return res.status(500).json({ success: false, message: "Logout failed" });
630
- }
631
-
632
- clearSessionCookies(res);
633
-
634
- console.log(`[mbkauthe] User "${username}" logged out successfully`);
635
- res.status(200).json({ success: true, message: "Logout successful" });
636
- });
637
- } catch (err) {
638
- console.error("[mbkauthe] Database query error during logout:", err);
639
- res.status(500).json({ success: false, message: "Internal Server Error" });
640
- }
641
- } else {
642
- res.status(400).json({ success: false, message: "Not logged in" });
643
- }
644
- });
645
-
646
- router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
647
- return res.render("loginmbkauthe.handlebars", {
648
- layout: false,
649
- githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED,
650
- customURL: mbkautheVar.loginRedirectURL || '/dashboard',
651
- userLoggedIn: !!req.session?.user,
652
- username: req.session?.user?.username || '',
653
- version: packageJson.version,
654
- appName: mbkautheVar.APP_NAME.toLowerCase(),
655
- csrfToken: req.csrfToken(),
656
- });
657
- });
658
-
659
- async function getLatestVersion() {
660
- try {
661
- const response = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkauthe/main/package.json');
662
- if (!response.ok) {
663
- console.error(`GitHub API responded with status ${response.status}`);
664
- return "0.0.0";
665
- }
666
- const latestPackageJson = await response.json();
667
- return latestPackageJson.version;
668
- } catch (error) {
669
- console.error('[mbkauthe] Error fetching latest version from GitHub:', error);
670
- return null;
671
- }
672
- }
673
-
674
- router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
675
- let latestVersion;
676
- const parameters = req.query;
677
- let authorized = false;
678
-
679
- if (parameters.password && mbkautheVar.Main_SECRET_TOKEN) {
680
- authorized = String(parameters.password) === String(mbkautheVar.Main_SECRET_TOKEN);
681
- }
682
-
683
- try {
684
- latestVersion = await getLatestVersion();
685
- //latestVersion = "Under Development"; // Placeholder for the latest version
686
- } catch (err) {
687
- console.error("[mbkauthe] Error fetching package-lock.json:", err);
688
- }
689
-
690
- try {
691
- res.render("info.handlebars", {
692
- layout: false,
693
- mbkautheVar: mbkautheVar,
694
- version: packageJson.version,
695
- latestVersion,
696
- authorized: authorized,
697
- });
698
- } catch (err) {
699
- console.error("[mbkauthe] Error fetching version information:", err);
700
- res.status(500).send(`
701
- <html>
702
- <head>
703
- <title>Error</title>
704
- </head>
705
- <body>
706
- <h1>Error</h1>
707
- <p>Failed to fetch version information. Please try again later.</p>
708
- </body>
709
- </html>
710
- `);
711
- }
712
- });
713
-
714
- // Configure GitHub Strategy for login
715
- passport.use('github-login', new GitHubStrategy({
716
- clientID: mbkautheVar.GITHUB_CLIENT_ID,
717
- clientSecret: mbkautheVar.GITHUB_CLIENT_SECRET,
718
- callbackURL: '/mbkauthe/api/github/login/callback',
719
- scope: ['user:email']
720
- },
721
- async (accessToken, refreshToken, profile, done) => {
722
- try {
723
- // Check if this GitHub account is linked to any user
724
- const githubUser = await dblogin.query({
725
- name: 'github-login-get-user',
726
- text: 'SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps", u."id" FROM user_github ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.github_id = $1',
727
- values: [profile.id]
728
- });
729
-
730
- if (githubUser.rows.length === 0) {
731
- // GitHub account is not linked to any user
732
- const error = new Error('GitHub account not linked to any user');
733
- error.code = 'GITHUB_NOT_LINKED';
734
- return done(error);
735
- }
736
-
737
- const user = githubUser.rows[0];
738
-
739
- // Check if the user account is active
740
- if (!user.Active) {
741
- const error = new Error('Account is inactive');
742
- error.code = 'ACCOUNT_INACTIVE';
743
- return done(error);
744
- }
745
-
746
- // Check if user is authorized for this app (same logic as regular login)
747
- if (user.Role !== "SuperAdmin") {
748
- const allowedApps = user.AllowedApps;
749
- if (!allowedApps || !allowedApps.some(app => app && app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
750
- const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
751
- error.code = 'NOT_AUTHORIZED';
752
- return done(error);
753
- }
754
- }
755
-
756
- // Return user data for login
757
- return done(null, {
758
- id: user.id, // This should be the user ID from the Users table
759
- username: user.UserName,
760
- role: user.Role,
761
- githubId: user.github_id,
762
- githubUsername: user.github_username
763
- });
764
- } catch (err) {
765
- console.error('[mbkauthe] GitHub login error:', err);
766
- err.code = err.code || 'GITHUB_AUTH_ERROR';
767
- return done(err);
768
- }
769
- }
770
- ));
771
-
772
- // Serialize/Deserialize user for GitHub login
773
- passport.serializeUser((user, done) => {
774
- done(null, user);
775
- });
776
-
777
- passport.deserializeUser((user, done) => {
778
- done(null, user);
779
- });
29
+ // Session restoration
30
+ router.use(sessionRestorationMiddleware);
780
31
 
781
32
  // Initialize passport
782
33
  router.use(passport.initialize());
783
34
  router.use(passport.session());
784
35
 
785
- // GitHub login initiation
786
- router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
787
- if (mbkautheVar.GITHUB_LOGIN_ENABLED) {
788
- // Store redirect parameter in session before OAuth flow (validate to prevent open redirect)
789
- const redirect = req.query.redirect;
790
- if (redirect && typeof redirect === 'string') {
791
- // Only allow relative URLs or same-origin URLs to prevent open redirect attacks
792
- if (redirect.startsWith('/') && !redirect.startsWith('//')) {
793
- req.session.oauthRedirect = redirect;
794
- } else {
795
- console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
796
- }
797
- }
798
- passport.authenticate('github-login')(req, res, next);
799
- }
800
- else {
801
- return renderError(res, {
802
- code: '403',
803
- error: 'GitHub Login Disabled',
804
- message: 'GitHub login is currently disabled. Please use your username and password to log in.',
805
- page: '/mbkauthe/login',
806
- pagename: 'Login',
807
- });
808
- }
809
- });
810
-
811
- // GitHub login callback
812
- router.get('/mbkauthe/api/github/login/callback',
813
- GitHubOAuthLimit,
814
- (req, res, next) => {
815
- passport.authenticate('github-login', {
816
- session: false // We'll handle session manually
817
- }, (err, user, info) => {
818
- // Custom error handling for passport authentication
819
- if (err) {
820
- console.error('[mbkauthe] GitHub authentication error:', err);
36
+ // Session cookie sync
37
+ router.use(sessionCookieSyncMiddleware);
821
38
 
822
- // Map error codes to user-friendly messages
823
- switch (err.code) {
824
- case 'GITHUB_NOT_LINKED':
825
- return renderError(res, {
826
- code: '403',
827
- error: 'GitHub Account Not Linked',
828
- 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.',
829
- page: '/mbkauthe/login',
830
- pagename: 'Login'
831
- });
39
+ // Mount routes (rate limiting is applied within each route module)
40
+ router.use('/mbkauthe', authRoutes);
41
+ router.use('/mbkauthe', oauthRoutes);
42
+ router.use('/mbkauthe', miscRoutes);
832
43
 
833
- case 'ACCOUNT_INACTIVE':
834
- return renderError(res, {
835
- code: '403',
836
- error: 'Account Inactive',
837
- message: 'Your account has been deactivated. Please contact your administrator.',
838
- page: '/mbkauthe/login',
839
- pagename: 'Login'
840
- });
841
-
842
- case 'NOT_AUTHORIZED':
843
- return renderError(res, {
844
- code: '403',
845
- error: 'Not Authorized',
846
- message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
847
- page: '/mbkauthe/login',
848
- pagename: 'Login'
849
- });
850
-
851
- default:
852
- return renderError(res, {
853
- code: '500',
854
- error: 'Authentication Error',
855
- message: 'An error occurred during GitHub authentication. Please try again.',
856
- page: '/mbkauthe/login',
857
- pagename: 'Login',
858
- details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
859
- });
860
- }
861
- }
862
-
863
- if (!user) {
864
- console.error('[mbkauthe] GitHub callback: No user data received');
865
- return renderError(res, {
866
- code: '401',
867
- error: 'Authentication Failed',
868
- message: 'GitHub authentication failed. Please try again.',
869
- page: '/mbkauthe/login',
870
- pagename: 'Login'
871
- });
872
- }
873
-
874
- // Authentication successful, attach user to request
875
- req.user = user;
876
- next();
877
- })(req, res, next);
878
- },
879
- async (req, res) => {
880
- try {
881
- const githubUser = req.user;
882
-
883
- // Combined query: fetch user data and 2FA status in one query
884
- const userQuery = `
885
- SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps",
886
- tfa."TwoFAStatus"
887
- FROM "Users" u
888
- LEFT JOIN "TwoFA" tfa ON u."UserName" = tfa."UserName"
889
- WHERE u."UserName" = $1
890
- `;
891
- const userResult = await dblogin.query({
892
- name: 'github-callback-get-user',
893
- text: userQuery,
894
- values: [githubUser.username]
895
- });
896
-
897
- if (userResult.rows.length === 0) {
898
- console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
899
- return renderError(res, {
900
- code: '404',
901
- error: 'User Not Found',
902
- message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
903
- page: '/mbkauthe/login',
904
- pagename: 'Login',
905
- details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
906
- });
907
- }
908
-
909
- const user = userResult.rows[0];
910
-
911
- // Check 2FA if enabled
912
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true" && user.TwoFAStatus) {
913
- // 2FA is enabled, store pre-auth user and redirect to 2FA
914
- // If this was an oauth flow, use oauthRedirect set earlier
915
- const oauthRedirect = req.session.oauthRedirect;
916
- if (oauthRedirect) delete req.session.oauthRedirect;
917
- req.session.preAuthUser = {
918
- id: user.id,
919
- username: user.UserName,
920
- UserName: user.UserName,
921
- role: user.Role,
922
- Role: user.Role,
923
- loginMethod: 'github',
924
- redirectUrl: oauthRedirect || null
925
- };
926
- console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
927
- return res.redirect('/mbkauthe/2fa');
928
- }
929
-
930
- // Complete login process using the shared function
931
- const userForSession = {
932
- id: user.id,
933
- username: user.UserName,
934
- UserName: user.UserName,
935
- role: user.Role,
936
- Role: user.Role,
937
- allowedApps: user.AllowedApps,
938
- };
939
-
940
- // For OAuth redirect flow, we need to handle redirect differently
941
- // Store the redirect URL before calling completeLoginProcess
942
- const oauthRedirect = req.session.oauthRedirect;
943
- delete req.session.oauthRedirect;
944
-
945
- // Custom response handler for OAuth flow - wrap the response object
946
- const originalJson = res.json.bind(res);
947
- const originalStatus = res.status.bind(res);
948
- let statusCode = 200;
949
-
950
- res.status = function (code) {
951
- statusCode = code;
952
- return originalStatus(code);
953
- };
954
-
955
- res.json = function (data) {
956
- if (data.success && statusCode === 200) {
957
- // If login successful, redirect instead of sending JSON
958
- const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/dashboard';
959
- console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
960
- // Restore original methods before redirect
961
- res.json = originalJson;
962
- res.status = originalStatus;
963
- return res.redirect(redirectUrl);
964
- }
965
- // Restore original methods for error responses
966
- res.json = originalJson;
967
- res.status = originalStatus;
968
- return originalJson(data);
969
- };
970
-
971
- await completeLoginProcess(req, res, userForSession);
972
-
973
- } catch (err) {
974
- console.error('[mbkauthe] GitHub login callback error:', err);
975
- return renderError(res, {
976
- code: '500',
977
- error: 'Internal Server Error',
978
- message: 'An error occurred during GitHub authentication. Please try again.',
979
- page: '/mbkauthe/login',
980
- pagename: 'Login',
981
- details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
982
- });
983
- }
984
- }
985
- );
44
+ // Redirect shortcuts for login
45
+ router.get(["/login", "/signin"], async (req, res) => {
46
+ const queryParams = new URLSearchParams(req.query).toString();
47
+ const redirectUrl = `/mbkauthe/login${queryParams ? `?${queryParams}` : ''}`;
48
+ return res.redirect(redirectUrl);
49
+ });
986
50
 
987
- export { getLatestVersion };
51
+ export { getLatestVersion } from "./routes/misc.js";
988
52
  export default router;