mbkauthe 1.4.2 → 2.0.1

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
@@ -5,14 +5,14 @@ import session from "express-session";
5
5
  import pgSession from "connect-pg-simple";
6
6
  const PgSession = pgSession(session);
7
7
  import { dblogin } from "./pool.js";
8
- import { authenticate } from "./validateSessionAndRole.js";
8
+ import { authenticate, validateSession, validateSessionAndRole } from "./validateSessionAndRole.js";
9
9
  import fetch from 'node-fetch';
10
10
  import cookieParser from "cookie-parser";
11
11
  import bcrypt from 'bcrypt';
12
12
  import rateLimit from 'express-rate-limit';
13
13
  import speakeasy from "speakeasy";
14
- //import passport from 'passport';
15
- //import GitHubStrategy from 'passport-github2';
14
+ import passport from 'passport';
15
+ import GitHubStrategy from 'passport-github2';
16
16
 
17
17
  import { createRequire } from "module";
18
18
  import fs from "fs";
@@ -36,16 +36,26 @@ router.get('/mbkauthe/main.js', (req, res) => {
36
36
  });
37
37
 
38
38
  // CSRF protection middleware
39
- const csrfProtection = csurf({ cookie: false });
39
+ const csrfProtection = csurf({ cookie: true });
40
40
 
41
41
  // CORS and security headers
42
42
  router.use((req, res, next) => {
43
43
  const origin = req.headers.origin;
44
- if (origin && origin.endsWith(`.${mbkautheVar.DOMAIN}`)) {
45
- res.header('Access-Control-Allow-Origin', origin);
46
- res.header('Access-Control-Allow-Credentials', 'true');
47
- res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
48
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
44
+ if (origin) {
45
+ try {
46
+ const originUrl = new URL(origin);
47
+ const allowedDomain = `.${mbkautheVar.DOMAIN}`;
48
+ // Exact match or subdomain match (must end with .domain.com, not just domain.com)
49
+ if (originUrl.hostname === mbkautheVar.DOMAIN ||
50
+ (originUrl.hostname.endsWith(allowedDomain) && originUrl.hostname.charAt(originUrl.hostname.length - allowedDomain.length - 1) !== '.')) {
51
+ res.header('Access-Control-Allow-Origin', origin);
52
+ res.header('Access-Control-Allow-Credentials', 'true');
53
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
54
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
55
+ }
56
+ } catch (err) {
57
+ // Invalid origin URL, skip CORS headers
58
+ }
49
59
  }
50
60
  next();
51
61
  });
@@ -71,6 +81,12 @@ const TwoFALimit = rateLimit({
71
81
  message: { success: false, message: "Too many 2FA attempts, please try again later" }
72
82
  });
73
83
 
84
+ const GitHubOAuthLimit = rateLimit({
85
+ windowMs: 5 * 60 * 1000,
86
+ max: 10,
87
+ message: "Too many GitHub login attempts, please try again later"
88
+ });
89
+
74
90
  const sessionConfig = {
75
91
  store: new PgSession({
76
92
  pool: dblogin,
@@ -85,7 +101,7 @@ const sessionConfig = {
85
101
  maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
86
102
  domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
87
103
  httpOnly: true,
88
- secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false,
104
+ secure: mbkautheVar.IS_DEPLOYED === 'true',
89
105
  sameSite: 'lax',
90
106
  path: '/'
91
107
  },
@@ -100,14 +116,16 @@ router.use(async (req, res, next) => {
100
116
  try {
101
117
  const sessionId = req.cookies.sessionId;
102
118
 
103
- // Validate sessionId format (should be 64 hex characters)
119
+ // Validate sessionId format (should be 64 hex characters) and normalize to lowercase
104
120
  if (typeof sessionId !== 'string' || !/^[a-f0-9]{64}$/i.test(sessionId)) {
105
121
  console.warn("[mbkauthe] Invalid sessionId format detected");
106
122
  return next();
107
123
  }
108
124
 
109
- const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE "SessionId" = $1 AND "Active" = true`;
110
- const result = await dblogin.query({ name: 'get-user-by-sessionid', text: query, values: [sessionId] });
125
+ const normalizedSessionId = sessionId.toLowerCase();
126
+
127
+ const query = `SELECT id, "UserName", "Active", "Role", "SessionId", "AllowedApps" FROM "Users" WHERE LOWER("SessionId") = $1 AND "Active" = true`;
128
+ const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [normalizedSessionId] });
111
129
 
112
130
  if (result.rows.length > 0) {
113
131
  const user = result.rows[0];
@@ -117,8 +135,7 @@ router.use(async (req, res, next) => {
117
135
  UserName: user.UserName,
118
136
  role: user.Role,
119
137
  Role: user.Role,
120
- sessionId,
121
- allowedApps: user.AllowedApps,
138
+ sessionId: normalizedSessionId,
122
139
  };
123
140
  }
124
141
  } catch (err) {
@@ -131,7 +148,7 @@ router.use(async (req, res, next) => {
131
148
  const getCookieOptions = () => ({
132
149
  maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
133
150
  domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
134
- secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false,
151
+ secure: mbkautheVar.IS_DEPLOYED === 'true',
135
152
  sameSite: 'lax',
136
153
  path: '/',
137
154
  httpOnly: true
@@ -139,32 +156,66 @@ const getCookieOptions = () => ({
139
156
 
140
157
  const getClearCookieOptions = () => ({
141
158
  domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
142
- secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false,
159
+ secure: mbkautheVar.IS_DEPLOYED === 'true',
143
160
  sameSite: 'lax',
144
161
  path: '/',
145
162
  httpOnly: true
146
163
  });
147
164
 
165
+ router.get('/mbkauthe/test', validateSession, LoginLimit, async (req, res) => {
166
+ if (req.session?.user) {
167
+ return res.send(`
168
+ <head> <script src="/mbkauthe/main.js"></script> </head>
169
+ <p>if you are seeing this page than User is logged in.</p>
170
+ <p>id: '${req.session.user.id}', UserName: '${req.session.user.username}', Role: '${req.session.user.role}', SessionId: '${req.session.user.sessionId}'</p>
171
+ <button onclick="logout()">Logout</button><br>
172
+ <a href="/mbkauthe/info">Info Page</a><br>
173
+ <a href="/mbkauthe/login">Login Page</a><br>
174
+ `);
175
+ }
176
+ });
177
+
148
178
  async function completeLoginProcess(req, res, user, redirectUrl = null) {
149
179
  try {
180
+ // Ensure both username formats are available for compatibility
181
+ const username = user.username || user.UserName;
182
+ if (!username) {
183
+ throw new Error('Username is required in user object');
184
+ }
185
+
150
186
  // smaller session id is sufficient and faster to generate/serialize
151
187
  const sessionId = crypto.randomBytes(32).toString("hex");
152
- console.log(`[mbkauthe] Generated session ID for username: ${user.username}`);
188
+ console.log(`[mbkauthe] Generated session ID for username: ${username}`);
189
+
190
+ // Regenerate session to prevent session fixation attacks
191
+ const oldSessionId = req.sessionID;
192
+ await new Promise((resolve, reject) => {
193
+ req.session.regenerate((err) => {
194
+ if (err) reject(err);
195
+ else resolve();
196
+ });
197
+ });
153
198
 
154
- // Delete old session record for this user
155
- await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.username]);
199
+ // Delete all old sessions for this user from session table
200
+ await dblogin.query({
201
+ name: 'login-delete-old-user-sessions',
202
+ text: 'DELETE FROM "session" WHERE sess::text LIKE $1',
203
+ values: [`%"username":"${username}"%`]
204
+ });
156
205
 
157
- await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [
158
- sessionId,
159
- user.id,
160
- ]);
206
+ await dblogin.query({
207
+ name: 'login-update-session-id', text: `UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, values: [
208
+ sessionId,
209
+ user.id,
210
+ ]
211
+ });
161
212
 
162
213
  req.session.user = {
163
214
  id: user.id,
164
- username: user.username,
165
- UserName: user.UserName,
166
- role: user.role,
167
- Role: user.role,
215
+ username: username,
216
+ UserName: username,
217
+ role: user.role || user.Role,
218
+ Role: user.role || user.Role,
168
219
  sessionId,
169
220
  };
170
221
 
@@ -182,7 +233,7 @@ async function completeLoginProcess(req, res, user, redirectUrl = null) {
182
233
 
183
234
  const cookieOptions = getCookieOptions();
184
235
  res.cookie("sessionId", sessionId, cookieOptions);
185
- console.log(`[mbkauthe] User "${user.username}" logged in successfully`);
236
+ console.log(`[mbkauthe] User "${username}" logged in successfully`);
186
237
 
187
238
  const responsePayload = {
188
239
  success: true,
@@ -216,8 +267,8 @@ router.use(async (req, res, next) => {
216
267
 
217
268
  router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => {
218
269
  try {
219
- await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL`);
220
- await dblogin.query('DELETE FROM "session"');
270
+ await dblogin.query({ name: 'terminate-all-user-sessions', text: `UPDATE "Users" SET "SessionId" = NULL` });
271
+ await dblogin.query({ name: 'terminate-all-db-sessions', text: 'DELETE FROM "session"' });
221
272
 
222
273
  req.session.destroy((err) => {
223
274
  if (err) {
@@ -280,11 +331,11 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
280
331
 
281
332
  try {
282
333
  const userQuery = `SELECT id, "UserName", "Password", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
283
- const userResult = await dblogin.query({ name: 'get-user-by-username', text: userQuery, values: [trimmedUsername] });
334
+ const userResult = await dblogin.query({ name: 'login-get-user', text: userQuery, values: [trimmedUsername] });
284
335
 
285
336
  if (userResult.rows.length === 0) {
286
- console.log(`[mbkauthe] Username does not exist: ${trimmedUsername}`);
287
- return res.status(404).json({ success: false, message: "Incorrect Username Or Password" });
337
+ console.log(`[mbkauthe] Login failed: invalid credentials`);
338
+ return res.status(401).json({ success: false, message: "Invalid credentials" });
288
339
  }
289
340
 
290
341
  const user = userResult.rows[0];
@@ -299,18 +350,18 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
299
350
  try {
300
351
  const result = await bcrypt.compare(password, user.Password);
301
352
  if (!result) {
302
- console.log("[mbkauthe] Incorrect password.");
303
- return res.status(401).json({ success: false, errorCode: 603, message: "Incorrect Username Or Password." });
353
+ console.log("[mbkauthe] Login failed: invalid credentials");
354
+ return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
304
355
  }
305
- console.log("[mbkauthe] Password matches!");
356
+ console.log("[mbkauthe] Password validated successfully");
306
357
  } catch (err) {
307
358
  console.error("[mbkauthe] Error comparing password:", err);
308
359
  return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` });
309
360
  }
310
361
  } else {
311
362
  if (user.Password !== password) {
312
- console.log(`[mbkauthe] Incorrect password for username: ${trimmedUsername}`);
313
- return res.status(401).json({ success: false, errorCode: 603, message: "Incorrect Username Or Password" });
363
+ console.log(`[mbkauthe] Login failed: invalid credentials`);
364
+ return res.status(401).json({ success: false, errorCode: 603, message: "Invalid credentials" });
314
365
  }
315
366
  }
316
367
 
@@ -329,7 +380,7 @@ router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => {
329
380
 
330
381
  if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") {
331
382
  const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
332
- const twoFAResult = await dblogin.query({ name: 'get-2fa-status', text: query, values: [trimmedUsername] });
383
+ const twoFAResult = await dblogin.query({ name: 'login-check-2fa-status', text: query, values: [trimmedUsername] });
333
384
 
334
385
  if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
335
386
  // 2FA is enabled, prompt for token on a separate page
@@ -367,6 +418,7 @@ router.get("/mbkauthe/2fa", csrfProtection, (req, res) => {
367
418
  layout: false,
368
419
  customURL: mbkautheVar.loginRedirectURL || '/home',
369
420
  csrfToken: req.csrfToken(),
421
+ appName: mbkautheVar.APP_NAME.toLowerCase(),
370
422
  });
371
423
  });
372
424
 
@@ -391,7 +443,7 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
391
443
 
392
444
  try {
393
445
  const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`;
394
- const twoFAResult = await dblogin.query(query, [username]);
446
+ const twoFAResult = await dblogin.query({ name: 'verify-2fa-secret', text: query, values: [username] });
395
447
 
396
448
  if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) {
397
449
  return res.status(500).json({ success: false, message: "2FA is not configured correctly." });
@@ -421,15 +473,15 @@ router.post("/mbkauthe/api/verify-2fa", TwoFALimit, csrfProtection, async (req,
421
473
  }
422
474
  });
423
475
 
424
- router.post("/mbkauthe/api/logout", LogoutLimit, csrfProtection, async (req, res) => {
476
+ router.post("/mbkauthe/api/logout", LogoutLimit, async (req, res) => {
425
477
  if (req.session.user) {
426
478
  try {
427
479
  const { id, username } = req.session.user;
428
480
 
429
- await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, [id]);
481
+ await dblogin.query({ name: 'logout-clear-session', text: `UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, values: [id] });
430
482
 
431
483
  if (req.sessionID) {
432
- await dblogin.query('DELETE FROM "session" WHERE sid = $1', [req.sessionID]);
484
+ await dblogin.query({ name: 'logout-delete-session', text: 'DELETE FROM "session" WHERE sid = $1', values: [req.sessionID] });
433
485
  }
434
486
 
435
487
  req.session.destroy((err) => {
@@ -463,7 +515,7 @@ router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => {
463
515
  userLoggedIn: !!req.session?.user,
464
516
  username: req.session?.user?.username || '',
465
517
  version: packageJson.version,
466
- appName: mbkautheVar.APP_NAME.toUpperCase(),
518
+ appName: mbkautheVar.APP_NAME.toLowerCase(),
467
519
  csrfToken: req.csrfToken(),
468
520
  });
469
521
  });
@@ -483,8 +535,18 @@ async function getLatestVersion() {
483
535
  }
484
536
  }
485
537
 
486
- router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
538
+ router.get("/mbkauthe/bg.avif", (req, res) => {
539
+ res.sendFile("bg.avif", { root: path.join(process.cwd(), "public") });
540
+ });
541
+
542
+ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (req, res) => {
487
543
  let latestVersion;
544
+ const parameters = req.query;
545
+ let authorized = false;
546
+
547
+ if (parameters.password && mbkautheVar.Main_SECRET_TOKEN) {
548
+ authorized = String(parameters.password) === String(mbkautheVar.Main_SECRET_TOKEN);
549
+ }
488
550
 
489
551
  try {
490
552
  latestVersion = await getLatestVersion();
@@ -499,6 +561,7 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
499
561
  mbkautheVar: mbkautheVar,
500
562
  version: packageJson.version,
501
563
  latestVersion,
564
+ authorized: authorized,
502
565
  });
503
566
  } catch (err) {
504
567
  console.error("[mbkauthe] Error fetching version information:", err);
@@ -515,64 +578,72 @@ router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => {
515
578
  `);
516
579
  }
517
580
  });
518
- /*
581
+
519
582
  // Configure GitHub Strategy for login
520
583
  passport.use('github-login', new GitHubStrategy({
521
- clientID: process.env.GITHUB_CLIENT_ID,
522
- clientSecret: process.env.GITHUB_CLIENT_SECRET,
523
- callbackURL: '/mbkauthe/api/github/login/callback',
524
- scope: ['user:email']
584
+ clientID: mbkautheVar.GITHUB_CLIENT_ID,
585
+ clientSecret: mbkautheVar.GITHUB_CLIENT_SECRET,
586
+ callbackURL: '/mbkauthe/api/github/login/callback',
587
+ scope: ['user:email']
525
588
  },
526
- async (accessToken, refreshToken, profile, done) => {
527
- try {
528
- // Check if this GitHub account is linked to any user
529
- const githubUser = await dblogin.query(
530
- 'SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps" FROM user_github ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.github_id = $1',
531
- [profile.id]
532
- );
533
-
534
- if (githubUser.rows.length === 0) {
535
- // GitHub account is not linked to any user
536
- return done(new Error('GitHub account not linked to any user'));
537
- }
538
-
539
- const user = githubUser.rows[0];
540
-
541
- // Check if the user account is active
542
- if (!user.Active) {
543
- return done(new Error('Account is inactive'));
544
- }
545
-
546
- // Check if user is authorized for this app (same logic as regular login)
547
- if (user.Role !== "SuperAdmin") {
548
- const allowedApps = user.AllowedApps;
549
- if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
550
- return done(new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`));
551
- }
552
- }
553
-
554
- // Return user data for login
555
- return done(null, {
556
- id: user.id, // This should be the user ID from the Users table
557
- username: user.UserName,
558
- role: user.Role,
559
- githubId: user.github_id,
560
- githubUsername: user.github_username
561
- });
562
- } catch (err) {
563
- console.error('[mbkauthe] GitHub login error:', err);
564
- return done(err);
589
+ async (accessToken, refreshToken, profile, done) => {
590
+ try {
591
+ // Check if this GitHub account is linked to any user
592
+ const githubUser = await dblogin.query({
593
+ name: 'github-login-get-user',
594
+ 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',
595
+ values: [profile.id]
596
+ });
597
+
598
+ if (githubUser.rows.length === 0) {
599
+ // GitHub account is not linked to any user
600
+ const error = new Error('GitHub account not linked to any user');
601
+ error.code = 'GITHUB_NOT_LINKED';
602
+ return done(error);
603
+ }
604
+
605
+ const user = githubUser.rows[0];
606
+
607
+ // Check if the user account is active
608
+ if (!user.Active) {
609
+ const error = new Error('Account is inactive');
610
+ error.code = 'ACCOUNT_INACTIVE';
611
+ return done(error);
612
+ }
613
+
614
+ // Check if user is authorized for this app (same logic as regular login)
615
+ if (user.Role !== "SuperAdmin") {
616
+ const allowedApps = user.AllowedApps;
617
+ if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) {
618
+ const error = new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`);
619
+ error.code = 'NOT_AUTHORIZED';
620
+ return done(error);
565
621
  }
622
+ }
623
+
624
+ // Return user data for login
625
+ return done(null, {
626
+ id: user.id, // This should be the user ID from the Users table
627
+ username: user.UserName,
628
+ role: user.Role,
629
+ githubId: user.github_id,
630
+ githubUsername: user.github_username
631
+ });
632
+ } catch (err) {
633
+ console.error('[mbkauthe] GitHub login error:', err);
634
+ err.code = err.code || 'GITHUB_AUTH_ERROR';
635
+ return done(err);
566
636
  }
637
+ }
567
638
  ));
568
639
 
569
640
  // Serialize/Deserialize user for GitHub login
570
641
  passport.serializeUser((user, done) => {
571
- done(null, user);
642
+ done(null, user);
572
643
  });
573
644
 
574
645
  passport.deserializeUser((user, done) => {
575
- done(null, user);
646
+ done(null, user);
576
647
  });
577
648
 
578
649
  // Initialize passport
@@ -580,103 +651,228 @@ router.use(passport.initialize());
580
651
  router.use(passport.session());
581
652
 
582
653
  // GitHub login initiation
583
- router.get('/mbkauthe/api/github/login', passport.authenticate('github-login'));
654
+ router.get('/mbkauthe/api/github/login', GitHubOAuthLimit, (req, res, next) => {
655
+ if (mbkautheVar.GITHUB_LOGIN_ENABLED) {
656
+ // Store redirect parameter in session before OAuth flow (validate to prevent open redirect)
657
+ const redirect = req.query.redirect;
658
+ if (redirect && typeof redirect === 'string') {
659
+ // Only allow relative URLs or same-origin URLs to prevent open redirect attacks
660
+ if (redirect.startsWith('/') && !redirect.startsWith('//')) {
661
+ req.session.oauthRedirect = redirect;
662
+ } else {
663
+ console.warn(`[mbkauthe] Invalid redirect parameter rejected: ${redirect}`);
664
+ }
665
+ }
666
+ passport.authenticate('github-login')(req, res, next);
667
+ }
668
+ else {
669
+ res.status(403).render('Error/dError.handlebars', {
670
+ layout: false,
671
+ code: '403',
672
+ error: 'GitHub Login Disabled',
673
+ message: 'GitHub login is currently disabled. Please use your username and password to log in.',
674
+ page: '/mbkauthe/login',
675
+ pagename: 'Login',
676
+ version: packageJson.version,
677
+ app: mbkautheVar.APP_NAME
678
+ });
679
+ }
680
+ });
584
681
 
585
682
  // GitHub login callback
586
683
  router.get('/mbkauthe/api/github/login/callback',
587
- passport.authenticate('github-login', {
588
- failureRedirect: '/mbkauthe/login?error=github_auth_failed',
589
- session: false // We'll handle session manually
590
- }),
591
- async (req, res) => {
592
- try {
593
- const githubUser = req.user;
594
-
595
- // Find the actual user record
596
- const userQuery = `SELECT * FROM "Users" WHERE "UserName" = $1`;
597
- const userResult = await dblogin.query(userQuery, [githubUser.username]);
598
-
599
- if (userResult.rows.length === 0) {
600
- console.log(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
601
- return res.redirect('/mbkauthe/login?error=user_not_found');
602
- }
603
-
604
- const user = userResult.rows[0];
605
-
606
- // Check 2FA if enabled
607
- if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
608
- const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
609
- const twoFAResult = await dblogin.query(twoFAQuery, [githubUser.username]);
610
-
611
- if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
612
- // 2FA is enabled, store pre-auth user and redirect to 2FA
613
- req.session.preAuthUser = {
614
- id: user.id,
615
- username: user.UserName,
616
- role: user.Role,
617
- loginMethod: 'github'
618
- };
619
- console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
620
- return res.redirect('/mbkauthe/2fa');
621
- }
622
- }
623
-
624
- // Complete login process
625
- const userForSession = {
626
- id: user.id,
627
- username: user.UserName,
628
- role: user.Role,
629
- };
630
-
631
- // Generate session and complete login
632
- const sessionId = crypto.randomBytes(32).toString("hex");
633
- console.log(`[mbkauthe] GitHub login: Generated session ID for username: ${user.UserName}`);
634
-
635
- // Delete old session record for this user
636
- await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.UserName]);
637
-
638
- await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [
639
- sessionId,
640
- user.id,
641
- ]);
642
-
643
- req.session.user = {
644
- id: user.id,
645
- username: user.UserName,
646
- role: user.Role,
647
- sessionId,
648
- };
649
-
650
- req.session.save(async (err) => {
651
- if (err) {
652
- console.log("[mbkauthe] GitHub login session save error:", err);
653
- return res.redirect('/mbkauthe/login?error=session_error');
654
- }
655
-
656
- try {
657
- await dblogin.query(
658
- 'UPDATE "session" SET username = $1 WHERE sid = $2',
659
- [user.UserName, req.sessionID]
660
- );
661
- } catch (e) {
662
- console.log("[mbkauthe] GitHub login: Failed to update username in session table:", e);
663
- }
664
-
665
- const cookieOptions = getCookieOptions();
666
- res.cookie("sessionId", sessionId, cookieOptions);
667
- console.log(`[mbkauthe] GitHub login: User "${user.UserName}" logged in successfully`);
668
-
669
- // Redirect to the configured URL or home
670
- const redirectUrl = mbkautheVar.loginRedirectURL || '/home';
671
- res.redirect(redirectUrl);
684
+ GitHubOAuthLimit,
685
+ (req, res, next) => {
686
+ passport.authenticate('github-login', {
687
+ session: false // We'll handle session manually
688
+ }, (err, user, info) => {
689
+ // Custom error handling for passport authentication
690
+ if (err) {
691
+ console.error('[mbkauthe] GitHub authentication error:', err);
692
+
693
+ // Map error codes to user-friendly messages
694
+ switch (err.code) {
695
+ case 'GITHUB_NOT_LINKED':
696
+ return res.status(403).render('Error/dError.handlebars', {
697
+ layout: false,
698
+ code: '403',
699
+ error: 'GitHub Account Not Linked',
700
+ message: 'Your GitHub account is not linked to any user in our system. To link your GitHub account, a User must connect their GitHub account to mbktech account through the user settings.',
701
+ page: '/mbkauthe/login',
702
+ pagename: 'Login',
703
+ version: packageJson.version,
704
+ app: mbkautheVar.APP_NAME
705
+ });
706
+
707
+ case 'ACCOUNT_INACTIVE':
708
+ return res.status(403).render('Error/dError.handlebars', {
709
+ layout: false,
710
+ code: '403',
711
+ error: 'Account Inactive',
712
+ message: 'Your account has been deactivated. Please contact your administrator.',
713
+ page: '/mbkauthe/login',
714
+ pagename: 'Login',
715
+ version: packageJson.version,
716
+ app: mbkautheVar.APP_NAME
717
+ });
718
+
719
+ case 'NOT_AUTHORIZED':
720
+ return res.status(403).render('Error/dError.handlebars', {
721
+ layout: false,
722
+ code: '403',
723
+ error: 'Not Authorized',
724
+ message: `You are not authorized to access ${mbkautheVar.APP_NAME}. Please contact your administrator.`,
725
+ page: '/mbkauthe/login',
726
+ pagename: 'Login',
727
+ version: packageJson.version,
728
+ app: mbkautheVar.APP_NAME
672
729
  });
673
730
 
674
- } catch (err) {
675
- console.error('[mbkauthe] GitHub login callback error:', err);
676
- res.redirect('/mbkauthe/login?error=internal_error');
731
+ default:
732
+ return res.status(500).render('Error/dError.handlebars', {
733
+ layout: false,
734
+ code: '500',
735
+ error: 'Authentication Error',
736
+ message: 'An error occurred during GitHub authentication. Please try again.',
737
+ page: '/mbkauthe/login',
738
+ pagename: 'Login',
739
+ version: packageJson.version,
740
+ app: mbkautheVar.APP_NAME,
741
+ details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
742
+ });
677
743
  }
744
+ }
745
+
746
+ if (!user) {
747
+ console.error('[mbkauthe] GitHub callback: No user data received');
748
+ return res.status(401).render('Error/dError.handlebars', {
749
+ layout: false,
750
+ code: '401',
751
+ error: 'Authentication Failed',
752
+ message: 'GitHub authentication failed. Please try again.',
753
+ page: '/mbkauthe/login',
754
+ pagename: 'Login',
755
+ version: packageJson.version,
756
+ app: mbkautheVar.APP_NAME
757
+ });
758
+ }
759
+
760
+ // Authentication successful, attach user to request
761
+ req.user = user;
762
+ next();
763
+ })(req, res, next);
764
+ },
765
+ async (req, res) => {
766
+ try {
767
+ const githubUser = req.user;
768
+
769
+ // Find the actual user record with named query
770
+ const userQuery = `SELECT id, "UserName", "Active", "Role", "AllowedApps" FROM "Users" WHERE "UserName" = $1`;
771
+ const userResult = await dblogin.query({
772
+ name: 'github-callback-get-user',
773
+ text: userQuery,
774
+ values: [githubUser.username]
775
+ });
776
+
777
+ if (userResult.rows.length === 0) {
778
+ console.error(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`);
779
+ return res.status(404).render('Error/dError.handlebars', {
780
+ layout: false,
781
+ code: '404',
782
+ error: 'User Not Found',
783
+ message: 'Your GitHub account is linked, but the user account no longer exists in our system.',
784
+ page: '/mbkauthe/login',
785
+ pagename: 'Login',
786
+ version: packageJson.version,
787
+ app: mbkautheVar.APP_NAME,
788
+ details: `GitHub username: ${githubUser.username}\nPlease contact your administrator.`
789
+ });
790
+ }
791
+
792
+ const user = userResult.rows[0];
793
+
794
+ // Check 2FA if enabled
795
+ if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") {
796
+ const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`;
797
+ const twoFAResult = await dblogin.query({
798
+ name: 'github-check-2fa-status',
799
+ text: twoFAQuery,
800
+ values: [githubUser.username]
801
+ });
802
+
803
+ if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) {
804
+ // 2FA is enabled, store pre-auth user and redirect to 2FA
805
+ req.session.preAuthUser = {
806
+ id: user.id,
807
+ username: user.UserName,
808
+ UserName: user.UserName,
809
+ role: user.Role,
810
+ Role: user.Role,
811
+ loginMethod: 'github'
812
+ };
813
+ console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`);
814
+ return res.redirect('/mbkauthe/2fa');
815
+ }
816
+ }
817
+
818
+ // Complete login process using the shared function
819
+ const userForSession = {
820
+ id: user.id,
821
+ username: user.UserName,
822
+ UserName: user.UserName,
823
+ role: user.Role,
824
+ Role: user.Role
825
+ };
826
+
827
+ // For OAuth redirect flow, we need to handle redirect differently
828
+ // Store the redirect URL before calling completeLoginProcess
829
+ const oauthRedirect = req.session.oauthRedirect;
830
+ delete req.session.oauthRedirect;
831
+
832
+ // Custom response handler for OAuth flow - wrap the response object
833
+ const originalJson = res.json.bind(res);
834
+ const originalStatus = res.status.bind(res);
835
+ let statusCode = 200;
836
+
837
+ res.status = function (code) {
838
+ statusCode = code;
839
+ return originalStatus(code);
840
+ };
841
+
842
+ res.json = function (data) {
843
+ if (data.success && statusCode === 200) {
844
+ // If login successful, redirect instead of sending JSON
845
+ const redirectUrl = oauthRedirect || mbkautheVar.loginRedirectURL || '/home';
846
+ console.log(`[mbkauthe] GitHub login: Redirecting to ${redirectUrl}`);
847
+ // Restore original methods before redirect
848
+ res.json = originalJson;
849
+ res.status = originalStatus;
850
+ return res.redirect(redirectUrl);
851
+ }
852
+ // Restore original methods for error responses
853
+ res.json = originalJson;
854
+ res.status = originalStatus;
855
+ return originalJson(data);
856
+ };
857
+
858
+ await completeLoginProcess(req, res, userForSession);
859
+
860
+ } catch (err) {
861
+ console.error('[mbkauthe] GitHub login callback error:', err);
862
+ return res.status(500).render('Error/dError.handlebars', {
863
+ layout: false,
864
+ code: '500',
865
+ error: 'Internal Server Error',
866
+ message: 'An error occurred during GitHub authentication. Please try again.',
867
+ page: '/mbkauthe/login',
868
+ pagename: 'Login',
869
+ version: packageJson.version,
870
+ app: mbkautheVar.APP_NAME,
871
+ details: process.env.NODE_ENV === 'development' ? `${err.message}\n${err.stack}` : 'Error details hidden in production'
872
+ });
678
873
  }
874
+ }
679
875
  );
680
- */
876
+
681
877
  export { getLatestVersion };
682
878
  export default router;