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